Skip to content

Appendix

Manual connection and CLI validation

Some change-control processes need the commands recorded before the access is granted. This is that path, with the az probes that prove it worked.

Validated against the Azure portal on 27 July 2026.

Use this page for one of three reasons:

  • your change-control process needs the exact commands or portal path recorded before the access is granted;
  • your security review wants a transcript proving the granted access works, taken by your own team rather than reported by a wizard;
  • an admin cannot reach the wizard, and Infralign has to register the connection for you.

Everything here produces the same result as the wizard: an app registration holding Reader and Cost Management Reader, proven against Azure. The wizard still has to run afterwards, because it is what stores the credential.

Azure CLI alternative for the app registration

Section titled “Azure CLI alternative for the app registration”

Use this Bash procedure instead of the portal procedure for app creation and role assignment. Create the client secret in the portal.

Sign in as a user who can register applications and assign roles. The following block is runnable after you replace the example subscription ID.

Terminal window
SUBSCRIPTION_ID="replace-with-subscription-id"
# Create the app registration and its service principal, capturing the appId
appId=$(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 appId
spObjectId=$(az ad sp show --id "$appId" --query id -o tsv)
# Grant the two required roles, per subscription
az 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 landed
az role assignment list --assignee "$appId" --all --output table

Repeat both assignments for every subscription you want analysed. To cover the whole estate in two commands, replace the scope with a management group:

Terminal window
MG_ID="replace-with-management-group-id"
--scope "/providers/Microsoft.Management/managementGroups/$MG_ID"

Done when the final command lists Cost Management Reader and Reader for each subscription.

  1. On each subscription, open Access control (IAM) → Role assignments. Confirm infralign-reader appears twice, once as Cost Management Reader and once as Reader.
  2. On the app registration, open Certificates & secrets. Confirm a client secret is listed with an expiry in the future. An expired secret authenticates nothing.

The wizard runs the equivalent of probes 1 to 4 automatically and shows a green row for each. Run them by hand when you need the output for a change record, or when you want to separate an Azure problem from a wizard problem.

Set these Bash variables before starting. The block is runnable after you replace the example values.

Terminal window
APP_ID="replace-with-application-client-id"
TENANT_ID="replace-with-tenant-id"
SUBSCRIPTION_ID="replace-with-subscription-id"
RESOURCE_GROUP="replace-with-resource-group"
VM_NAME="replace-with-vm-name"
START_DATE="2026-07-01"
END_DATE="2026-07-07"

Probes 1 to 4 are the gate. Run the optional diagnostics only when a dashboard tile is empty.

If a probe returns AuthorizationFailed, the corresponding role is missing or still propagating. After a new assignment, wait up to ten minutes before retrying.

  1. Sign in as the service principal. Read the secret into a variable so it does not enter shell history.

    Terminal window
    read -rsp "client secret: " SP_SECRET; echo
    az login --service-principal -u "$APP_ID" -p "$SP_SECRET" --tenant "$TENANT_ID"

    Done when the output lists each granted subscription. This output is illustrative:

    [ { "cloudName": "AzureCloud", "id": "00000000-0000-0000-0000-000000000000", "state": "Enabled",
    "user": { "name": "11111111-1111-1111-1111-111111111111", "type": "servicePrincipal" } } ]
  2. Set the subscription context. Run the probes against one subscription at a time.

    Terminal window
    az account set --subscription "$SUBSCRIPTION_ID"

    Done when the command returns without an error.

  3. Check the management token. A future expiresOn confirms the credential can call Azure Resource Manager.

    Terminal window
    az account get-access-token --resource https://management.azure.com --query expiresOn

    Done when the command returns a future expiry.

  4. List the role assignments. Both roles should appear, scoped to the subscription.

    Terminal window
    az role assignment list --assignee "$APP_ID" --all -o table
    Principal Role Scope
    ----------------- ---------------------- -------------------------------------
    infralign-reader Cost Management Reader /subscriptions/00000000-0000-0000-0000-000000000000
    infralign-reader Reader /subscriptions/00000000-0000-0000-0000-000000000000

    Done when both rows appear for the selected subscription.

Per-source probes: run these if a dashboard tile comes up empty
  1. Probe cost (Cost Management Reader). This validates the cost data-plane. The nightly run calls generateCostDetailsReport (api-version 2024-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"}}}}'

    The following successful output is illustrative:

    { "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 endpoint the nightly run 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\":\"$START_DATE\",\"end\":\"$END_DATE\"}}"

    Expected result: HTTP 202 with a Location header to poll.

  2. Probe Resource Graph (Reader). The extension-free az rest call works anywhere; az graph query is the alternative if you have the resource-graph extension.

    Terminal window
    # Extension-free: call the REST endpoint directly
    az 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-graph
    az 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.

  3. 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 --offset to 7d or more before reading it as a failure.

  4. 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 the probe still proves read access.

  5. 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/$RESOURCE_GROUP/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 timeseries on a stopped or idle VM is valid. Pick a running VM to see data.

When probes 1 to 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 sign-in returns an AADSTS error, exclude infralign-reader from the blocking workload-identity policy. AADSTS53003 is one example.

If the policy is IP-bound, ask Infralign for the ingestion runtime’s egress IPs.

Done when the sign-in probe in step 1 succeeds.

Email [email protected] with the tenant ID, the application ID, and the subscription IDs.

Infralign will agree a transfer method with you in that case. On the normal path nothing is transferred out of band at all: your admin pastes the secret into the wizard over TLS, and it is stored encrypted. Security and data handling records how the stored credential is held.


The access above is only half the job. The wizard is what stores the connection, so finish at connect Azure. If a probe here failed and the cause is not obvious, troubleshooting is keyed by the error code.