Skip to content
Demiton

Auditing Azure

This checks Securing Azure and Azure in your own subscription. Run it quarterly, and after any new app registration, role change or network change. The commands only read, and return settings rather than secret values. They were tested with Azure CLI 2.87. The auditor needs Global Reader in Entra and Reader plus Security Reader on the subscription, and no write role.

Passing means your configuration matches this page. It does not amount to a CIS, ISO 27001 or SOC 2 attestation. For a wider independent score, use the Microsoft cloud security benchmark in Defender for Cloud’s regulatory compliance view, and the current CIS Microsoft Azure Foundations Benchmark.

Terminal window
az login --tenant "<tenant-id>"

Azure CLI’s token cannot read Security Defaults or be relied on for Conditional Access, so check T1, T2, T3 and S1 in the portal: Entra → Properties → Manage security defaults, and Entra → Conditional Access → Policies.

Terminal window
# T4: how many Global Administrators
az rest --method get --url 'https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members' --query "length(value)" -o tsv
Terminal window
# T6, T7, T8: consent, app registration and guest invitations
az rest --method get --url 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy' --query "{consent:defaultUserRolePermissions.permissionGrantPoliciesAssigned,createApps:defaultUserRolePermissions.allowedToCreateApps,invites:allowInvitesFrom}"
Terminal window
# C1, C2, C3: every app registration, its audience and its secrets' dates
az ad app list --all --query "[].{name:displayName,audience:signInAudience,secrets:passwordCredentials[].{from:startDateTime,to:endDateTime}}" -o json
Terminal window
# C4: the Graph permissions each app in your tenant holds, by name
ROLES=$(mktemp)
TENANT=$(az account show --query tenantId -o tsv)
az ad sp show --id 00000003-0000-0000-c000-000000000000 --query "appRoles[].[id,value]" -o tsv > "$ROLES"
az ad sp list --all --query "[?appOwnerOrganizationId=='$TENANT'].[id,displayName]" -o tsv | while IFS=$'\t' read -r id name; do
echo "$name"
az rest --method get --url "https://graph.microsoft.com/v1.0/servicePrincipals/$id/appRoleAssignments" --query "value[].appRoleId" -o tsv | while read -r role; do grep "$role" "$ROLES" | cut -f2 | sed 's/^/ /'; done
done
rm "$ROLES"
Every customer: tenant and connectors
IDPasses whenIf it fails
T1Portal: Security Defaults is on, or an enabled policy requires MFA for all usersHigh
T2Portal: an enabled policy requires a phishing-resistant strength for admin rolesHigh
T3Portal: Security Defaults is on, or an enabled policy blocks legacy clientsHigh
T42 to 4, and the break-glass accounts sign in only when testedHigh
T5Portal: Entra → Privileged Identity Management shows eligible, not active, assignmentsMedium
T6consent has no entry starting ManagePermissionGrantsForSelfMedium
T7createApps is falseMedium
T8invites is adminsAndGuestInviters or noneLow
C1Connector apps show AzureADMyOrgHigh
C2Each secret’s to is within 90 days of its from, and none has passedHigh
C3One secret per app, two only during a rotationMedium
C4Each connector app holds no more than its row in the table. Any *.ReadWrite.All or Sites.FullControl.All has a named workflow behind itHigh
S1Portal: the MFA policy covers All resources or names the Demiton enterprise app, and is not in report-only modeHigh
S2Portal: the Demiton enterprise app shows Assignment required as YesMedium
Terminal window
SUB="<subscription-id>"; RG="<resource-group>"; ENVIRONMENT="<container-apps-environment>"; API="<api-container-app>"; PG="<postgres-server>"; ST="<storage-account>"; ACR="<registry>"; AI="<azure-openai-account>"; LAW="<log-analytics-workspace>"
az account set --subscription "$SUB"
Terminal window
# R1, R2, R3: network, ingress, and any plain environment variable that looks like a secret
az containerapp env show -g "$RG" -n "$ENVIRONMENT" --query "properties.vnetConfiguration.infrastructureSubnetId" -o tsv
az containerapp list -g "$RG" --query "[].{name:name,external:properties.configuration.ingress.external,insecure:properties.configuration.ingress.allowInsecure,identity:identity.type}" -o table
az containerapp show -g "$RG" -n "$API" --query "properties.template.containers[0].env[?value].name" -o tsv | grep -iE "secret|password|key|token|connection"
Terminal window
# R4, R5: PostgreSQL
az postgres flexible-server show -g "$RG" -n "$PG" --query "{public:network.publicNetworkAccess,subnet:network.delegatedSubnetResourceId,backupDays:backup.backupRetentionDays}"
az postgres flexible-server firewall-rule list -g "$RG" --server-name "$PG" -o table
az postgres flexible-server parameter show -g "$RG" --server-name "$PG" --name require_secure_transport --query value -o tsv
az postgres flexible-server parameter show -g "$RG" --server-name "$PG" --name ssl_min_protocol_version --query value -o tsv
Terminal window
# R6: storage
az storage account show -g "$RG" -n "$ST" --query "{tls:minimumTlsVersion,anonymous:allowBlobPublicAccess,httpsOnly:enableHttpsTrafficOnly}"
az storage account blob-service-properties show -g "$RG" --account-name "$ST" --query "{softDelete:deleteRetentionPolicy.enabled,containers:containerDeleteRetentionPolicy.enabled,versioning:isVersioningEnabled}"
Terminal window
# R7, R8: registry and Azure OpenAI
az acr show -n "$ACR" --query "{admin:adminUserEnabled}"
az cognitiveservices account show -g "$RG" -n "$AI" --query "{public:properties.publicNetworkAccess,netDefault:properties.networkAcls.defaultAction}"
Terminal window
# R9: who holds what, including inherited from the subscription
az role assignment list -g "$RG" --include-inherited --query "[].{who:principalName,type:principalType,role:roleDefinitionName,scope:scope}" -o table
Terminal window
# R11: activity log export, retention and cap
az monitor diagnostic-settings subscription list --query "value[].name" -o tsv
az monitor log-analytics workspace show -g "$RG" -n "$LAW" --query "{retention:retentionInDays,capGb:workspaceCapping.dailyQuotaGb}"
Terminal window
# R12: resource diagnostics
az monitor diagnostic-settings list --resource "$(az postgres flexible-server show -g "$RG" -n "$PG" --query id -o tsv)" --query "[].name" -o tsv
az monitor diagnostic-settings list --resource "$(az storage account show -g "$RG" -n "$ST" --query id -o tsv)/blobServices/default" --query "[].name" -o tsv
Terminal window
# R13, R14: Defender and locks
az security pricing list --query "value[].{name:name,tier:pricingTier}" -o table
az security secure-scores list --query "[].{current:score.current,max:score.max}" -o table
az lock list -g "$RG" -o table
Dedicated deployment only: subscription
IDPasses whenIf it fails
R1A subnet ID is returnedHigh, as an accepted risk with a date until the rebuild
R2Only the API shows external as True, and insecure is False on allHigh
R3The grep returns nothing, or only names you have confirmed hold no secretHigh
R4public is Disabled, in which case the firewall command returns an error saying rules are not supported. If Enabled, no 0.0.0.0 rule and a private endpoint in placeHigh
R5on, TLSv1.2 or later, and backupDays of 14 or moreHigh
R6TLS1_2, anonymous false, httpsOnly true, and all three recovery settings trueHigh
R7admin is falseMedium
R8public is Disabled, or netDefault is Deny, once R1 is doneMedium
R9No Owner or User Access Administrator outside your own administrators. Deploy identity’s scope ends at the resource groupHigh
R10Portal: the deploy identity’s secret is inside 90 days, or it uses a federated credentialMedium
R11At least one setting named, retention of 90 or more, and capGb of -1 or high enough never to stop ingestionHigh
R12Each command names at least one settingMedium
R13Both commands answer. Subscription Not Registered is a fail: register Microsoft.SecurityMedium
R14A CanNotDelete lock on PostgreSQL and on storageMedium

Keep one row per ID: pass, fail or accepted risk, with the date, who ran it and the command output. An accepted risk needs a named owner and a review date. Send the fails on anything marked here as breaking the application to your Customer Success Manager before you fix them, so the fix does not become an outage.


Talk to the team

Tell us what you run - your ERP, field, payroll and document systems - and we will show you the registers Demiton would fill from them, on your data, in a 30-minute call.

Book a 30-minute callStart free