Getting started
Entra app registration
Create the read-only service principal Infralign uses to pull your Azure cost and resource data, and grant it the two read-only RBAC roles the daily lane needs.
Validated against the Azure portal on 27 July 2026.
Click any image to enlarge.
Infralign reads your Azure estate through a service principal — an app registration in your own Microsoft Entra tenant that you create, control, and can revoke at any time.
The one-time admin consent your tenant grants is for the separate Infralign Portal sign-in app — see Dashboard sign-in. The roles granted here are the ones set out in the permissions reference.
Create the app registration
Section titled “Create the app registration”Figure 1 shows who does each step and which four values leave your tenant. The steps below are the portal path.
- Sign in to the Microsoft Entra admin center as a user who can register applications.
- Go to Identity → Applications → App registrations → New registration.
- Name it something recognisable, for example
infralign-reader. - Under Supported account types, choose single tenant (accounts in this organisational directory only).
- Leave Redirect URI empty — the app authenticates as a service principal, not through a browser sign-in, so it needs no redirect.
- Select Register.
- On the app’s Overview blade, record the Application (client) ID and the Directory (tenant) ID. Infralign needs both.



The infralign-reader app in these captures was deleted afterwards; the IDs and secret shown are no longer valid.
Create a client secret
Section titled “Create a client secret”- On the app, open Certificates & secrets → Client secrets → New client secret.
- Add a description (for example
infralign) and an expiry. We recommend 6–12 months, and rotate on that schedule. For security-sensitive tenants, ask Infralign about certificate credentials or workload-identity federation instead. - Select Add, then copy the secret Value immediately — Azure shows it only once and it cannot be retrieved later.
- Share the value only through the one-time password-manager link Infralign sends you — see what Infralign needs from you. Never send it by email or chat in plain text.


Once shared, the secret is held in access-controlled configuration on Infralign’s infrastructure — encrypted at rest, scoped to the ingestion runtime, and never logged. See security and data handling for how credentials are held.
Grant the read-only RBAC roles
Section titled “Grant the read-only RBAC roles”This step is Azure role-based access control — role assignments on your resources — not API permissions. The default Cost API lane needs exactly two built-in roles, both assigned per subscription on every subscription you want analysed:
- Cost Management Reader — the daily scrape calls
POST …/providers/Microsoft.CostManagement/generateCostDetailsReport, which is an action, not a read, and plain Reader returns 403 on it. Report blobs download over Cost-Management-signed SAS links, so no storage role is involved. The billing-account scope is never used. - Reader — its
*/readcovers everything else: Azure Resource Graph inventory, the Activity Log, Azure Monitor metrics, and Azure Advisor recommendations.
Reader grants control-plane metadata only. It can see that resources exist and how they are configured, never what is inside them. If your security team wants the detail, or a stricter alternative, see Why Reader, and what it cannot see.
For each subscription, the portal path is the same:
- Open the subscription’s Access control (IAM) blade.
- Select Add → Add role assignment.
- Choose the role, then Next.
- On Members, keep User, group, or service principal, select Select members, and search for the app by name (
infralign-reader). - Select Review + assign.
Repeat both assignments on every subscription you include — Infralign fans out across all granted subscriptions.
What each role unlocks
Section titled “What each role unlocks”| Data source | Endpoint · api-version | Role | What breaks without it |
|---|---|---|---|
| Cost Details report | POST …/subscriptions/{sub}/providers/Microsoft.CostManagement/generateCostDetailsReport · 2024-08-01 (ActualCost + AmortizedCost) | Cost Management Reader | 403 on the report action — no daily spend, no dashboards, reports, or audit baselines |
| Resource Graph inventory | POST …/providers/Microsoft.ResourceGraph/resources · 2022-10-01 (also AdvisorResources + ResourceChanges tables) | Reader | No inventory, no cost-to-resource mapping, no orphaned-disk detection |
| Activity Log | GET …/subscriptions/{sub}/providers/Microsoft.Insights/eventtypes/management/values · 2015-04-01 | Reader | No control-plane change history behind cost movements |
| Azure Monitor metrics | GET {resourceId}/providers/microsoft.insights/metrics · 2023-10-01 | Reader | No CPU utilisation — idle-VM and rightsizing confidence collapse |
| Azure Advisor | Microsoft.Advisor/recommendations/read (via Resource Graph) | Reader | No Advisor cost recommendations as an input signal |
Set up the az CLI
Section titled “Set up the az CLI”The CLI equivalent of the two sections above — use this or the portal path, not both.
Sign in as a user who can register applications and assign roles (not the service principal), then replace the placeholders with your own IDs.
# Create the app registration and its service principal, capturing the appIdappId=$(az ad app create --display-name infralign-reader --query appId -o tsv)az ad sp create --id "$appId"
# Role assignments target the service principal's object id, not the appIdspObjectId=$(az ad sp show --id "$appId" --query id -o tsv)
# Grant the two read-only roles, per subscriptionaz role assignment create \ --assignee-object-id "$spObjectId" \ --assignee-principal-type ServicePrincipal \ --role "Cost Management Reader" \ --scope /subscriptions/<subscription-id>
az role assignment create \ --assignee-object-id "$spObjectId" \ --assignee-principal-type ServicePrincipal \ --role "Reader" \ --scope /subscriptions/<subscription-id>
# Confirm the assignments landedaz role assignment list --assignee "$appId" --all --output tableRepeat both assignments for every subscription you want analysed.
Validate the setup
Section titled “Validate the setup”Once the two roles are assigned, confirm the service principal can actually read each source before handing the credentials over.
Portal checks
Section titled “Portal checks”- On each subscription, open Access control (IAM) → Role assignments and confirm
infralign-readerappears twice — once as Cost Management Reader and once as Reader. - On the app registration, open Certificates & secrets and confirm a client secret is listed with an expiry in the future. An expired secret authenticates nothing.
az CLI validation runbook
Section titled “az CLI validation runbook”Paste these in order, replacing <appId>, <tenant-id>, <subscription-id>, <rg>, and <vm-name> with your own values. Probes 1–4 are the pass/fail gate — if all four pass, the credential authenticates and both roles are live, which is everything the handover needs. The per-source probes after them are diagnostics, folded away below. A probe that fails with AuthorizationFailed means the role behind it is missing. RBAC changes take up to ten minutes to propagate, so a fresh assignment may return 403 until it takes effect — wait, then re-run.
-
Sign in as the service principal. Proves the client id + secret pair is valid. Read the secret into a variable rather than typing it — a plain
-p <secret>lands in shell history, which both bash and PSReadLine persist.Terminal window read -rsp "client secret: " SP_SECRET; echoaz login --service-principal -u <appId> -p "$SP_SECRET" --tenant <tenant-id>The output lists one object per subscription the service principal can see:
[ { "cloudName": "AzureCloud", "id": "<subscription-id>", "state": "Enabled","user": { "name": "<appId>", "type": "servicePrincipal" } } ] -
Set the subscription context. Run the probes against one subscription at a time.
Terminal window az account set --subscription <subscription-id> -
Check the management token. A future
expiresOnconfirms the credential can call Azure Resource Manager.Terminal window az account get-access-token --resource https://management.azure.com --query expiresOn -
List the role assignments. Both roles should appear, scoped to the subscription.
Terminal window az role assignment list --assignee <appId> --all -o tablePrincipal Role Scope----------------- ---------------------- -------------------------------------infralign-reader Cost Management Reader /subscriptions/<subscription-id>infralign-reader Reader /subscriptions/<subscription-id>
Per-source probes — run these if a dashboard tile comes up empty
-
Probe cost (Cost Management Reader). This validates the cost data-plane. The nightly lane calls
generateCostDetailsReport(api-version2024-08-01), covered by the same role.Terminal window az rest --method post \--url "https://management.azure.com/subscriptions/<subscription-id>/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \--body '{"type":"ActualCost","timeframe":"MonthToDate","dataset":{"granularity":"Daily","aggregation":{"totalCost":{"name":"Cost","function":"Sum"}}}}'{ "properties": { "columns": [ {"name":"Cost","type":"Number"},{"name":"UsageDate","type":"Number"}, {"name":"Currency","type":"String"} ],"rows": [ [3.14729643469101, 20260710, "EUR"],[6.3827424306758, 20260711, "EUR"], … ] } }A failure here names the action
Microsoft.CostManagement/query/action.Optionally, exercise the exact ingestion endpoint the nightly lane uses:
Terminal window az rest --method post \--url "https://management.azure.com/subscriptions/<subscription-id>/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2024-08-01" \--body '{"metric":"ActualCost","timePeriod":{"start":"<yyyy-mm-01>","end":"<yyyy-mm-07>"}}'Expected result: HTTP 202 with a
Locationheader to poll. -
Probe Resource Graph (Reader). The extension-free
az restcall works anywhere;az graph queryis the alternative if you have theresource-graphextension.Terminal window # Extension-free: call the REST endpoint directlyaz rest --method post \--url "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01" \--body '{"subscriptions":["<subscription-id>"],"query":"Resources | summarize count()"}'Terminal window # Alternative: the extension route (idempotent add)az extension add --name resource-graphaz graph query --subscriptions <subscription-id> -q "Resources | summarize count()"{ "count": 1, "data": [ { "count_": 17 } ], "skip_token": null, "total_records": 1 }A failure here names
Microsoft.ResourceGraph/resources/read. -
Probe the Activity Log (Reader).
Terminal window az monitor activity-log list --subscription <subscription-id> --offset 24h --max-events 1 -o json[ { "caller": "Microsoft.Advisor", "category": { "value": "Recommendation" },"eventTimestamp": "2026-07-24T03:05:11.1570623Z", "level": "Informational", … } ]An empty list on a quiet subscription is valid — widen
--offsetto7dor more before reading it as a failure. -
Probe Advisor (Reader). Advisor recommendations are read through Resource Graph — the same path the platform uses.
Terminal window az graph query --subscriptions <subscription-id> \-q "advisorresources | where type == 'microsoft.advisor/recommendations' | limit 1"{ "count": 1, "data": [ { "properties": { "category": "Cost","impact": "Medium", "impactedField": "Microsoft.Compute/virtualMachines" } } ],"total_records": 1 }Empty output with HTTP 200 is valid — Advisor may not have generated recommendations yet — and still proves read access.
-
Probe Monitor metrics (Reader). Point it at any cost-significant VM that is running.
Terminal window az monitor metrics list \--resource "/subscriptions/<subscription-id>/resourceGroups/<rg>/providers/Microsoft.Compute/virtualMachines/<vm-name>" \--metric "Percentage CPU" --interval PT1H --offset 3h -o json{ "interval": "PT1H", "namespace": "Microsoft.Compute/virtualMachines","value": [ { "name": { "value": "Percentage CPU" }, "timeseries": [ { "data": [{ "average": 36.88, "timeStamp": "2026-07-24T04:43:00Z" },{ "average": 5.98, "timeStamp": "2026-07-24T05:43:00Z" } ] } ], "unit": "Percent" } ] }An empty
timeserieson a stopped or idle VM is valid — pick a running VM to see data.
When probes 1–4 pass, repeat from step 2 for the next subscription.
If sign-in is blocked by Conditional Access
Section titled “If sign-in is blocked by Conditional Access”If the service-principal sign-in fails with an AADSTS error — for example AADSTS53003 — a workload-identity Conditional Access policy is blocking it. Exclude the infralign-reader app from that policy, or scope an exception for it. If the policy is IP-bound, Infralign can supply the egress IPs the ingestion runtime signs in from.
What Infralign needs from you
Section titled “What Infralign needs from you”To start ingestion, email [email protected] with:
- the Directory (tenant) ID;
- the Application (client) ID;
- the subscription IDs to analyse.
Infralign replies with a one-time password-manager share link for the fourth value — the client secret. Paste the secret into that link, not into the email body, an attachment, or a chat message.
Rotate the secret at any time — add a new one, share it, and let the old one expire. Access is revocable instantly: delete the role assignments or the app registration and ingestion stops the same moment.
The service principal above is for data ingestion only. Your own users sign in to the dashboard through the multi-tenant Infralign Portal app, with their existing Microsoft work account.
Next: Dashboard sign-in — the one-time admin consent, the user list, and sign-in troubleshooting.