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.
Every customer: tenant and connectors
Section titled “Every customer: tenant and connectors”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.
# T4: how many Global Administratorsaz rest --method get --url 'https://graph.microsoft.com/v1.0/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members' --query "length(value)" -o tsv# T6, T7, T8: consent, app registration and guest invitationsaz rest --method get --url 'https://graph.microsoft.com/v1.0/policies/authorizationPolicy' --query "{consent:defaultUserRolePermissions.permissionGrantPoliciesAssigned,createApps:defaultUserRolePermissions.allowedToCreateApps,invites:allowInvitesFrom}"# C1, C2, C3: every app registration, its audience and its secrets' datesaz ad app list --all --query "[].{name:displayName,audience:signInAudience,secrets:passwordCredentials[].{from:startDateTime,to:endDateTime}}" -o json# C4: the Graph permissions each app in your tenant holds, by nameROLES=$(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/^/ /'; donedonerm "$ROLES"| ID | Passes when | If it fails |
|---|---|---|
| T1 | Portal: Security Defaults is on, or an enabled policy requires MFA for all users | High |
| T2 | Portal: an enabled policy requires a phishing-resistant strength for admin roles | High |
| T3 | Portal: Security Defaults is on, or an enabled policy blocks legacy clients | High |
| T4 | 2 to 4, and the break-glass accounts sign in only when tested | High |
| T5 | Portal: Entra → Privileged Identity Management shows eligible, not active, assignments | Medium |
| T6 | consent has no entry starting ManagePermissionGrantsForSelf | Medium |
| T7 | createApps is false | Medium |
| T8 | invites is adminsAndGuestInviters or none | Low |
| C1 | Connector apps show AzureADMyOrg | High |
| C2 | Each secret’s to is within 90 days of its from, and none has passed | High |
| C3 | One secret per app, two only during a rotation | Medium |
| C4 | Each connector app holds no more than its row in the table. Any *.ReadWrite.All or Sites.FullControl.All has a named workflow behind it | High |
| S1 | Portal: the MFA policy covers All resources or names the Demiton enterprise app, and is not in report-only mode | High |
| S2 | Portal: the Demiton enterprise app shows Assignment required as Yes | Medium |
Dedicated deployment only: subscription
Section titled “Dedicated deployment only: subscription”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"# R1, R2, R3: network, ingress, and any plain environment variable that looks like a secretaz containerapp env show -g "$RG" -n "$ENVIRONMENT" --query "properties.vnetConfiguration.infrastructureSubnetId" -o tsvaz containerapp list -g "$RG" --query "[].{name:name,external:properties.configuration.ingress.external,insecure:properties.configuration.ingress.allowInsecure,identity:identity.type}" -o tableaz containerapp show -g "$RG" -n "$API" --query "properties.template.containers[0].env[?value].name" -o tsv | grep -iE "secret|password|key|token|connection"# R4, R5: PostgreSQLaz 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 tableaz postgres flexible-server parameter show -g "$RG" --server-name "$PG" --name require_secure_transport --query value -o tsvaz postgres flexible-server parameter show -g "$RG" --server-name "$PG" --name ssl_min_protocol_version --query value -o tsv# R6: storageaz 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}"# R7, R8: registry and Azure OpenAIaz acr show -n "$ACR" --query "{admin:adminUserEnabled}"az cognitiveservices account show -g "$RG" -n "$AI" --query "{public:properties.publicNetworkAccess,netDefault:properties.networkAcls.defaultAction}"# R9: who holds what, including inherited from the subscriptionaz role assignment list -g "$RG" --include-inherited --query "[].{who:principalName,type:principalType,role:roleDefinitionName,scope:scope}" -o table# R11: activity log export, retention and capaz monitor diagnostic-settings subscription list --query "value[].name" -o tsvaz monitor log-analytics workspace show -g "$RG" -n "$LAW" --query "{retention:retentionInDays,capGb:workspaceCapping.dailyQuotaGb}"# R12: resource diagnosticsaz monitor diagnostic-settings list --resource "$(az postgres flexible-server show -g "$RG" -n "$PG" --query id -o tsv)" --query "[].name" -o tsvaz monitor diagnostic-settings list --resource "$(az storage account show -g "$RG" -n "$ST" --query id -o tsv)/blobServices/default" --query "[].name" -o tsv# R13, R14: Defender and locksaz security pricing list --query "value[].{name:name,tier:pricingTier}" -o tableaz security secure-scores list --query "[].{current:score.current,max:score.max}" -o tableaz lock list -g "$RG" -o table| ID | Passes when | If it fails |
|---|---|---|
| R1 | A subnet ID is returned | High, as an accepted risk with a date until the rebuild |
| R2 | Only the API shows external as True, and insecure is False on all | High |
| R3 | The grep returns nothing, or only names you have confirmed hold no secret | High |
| R4 | public 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 place | High |
| R5 | on, TLSv1.2 or later, and backupDays of 14 or more | High |
| R6 | TLS1_2, anonymous false, httpsOnly true, and all three recovery settings true | High |
| R7 | admin is false | Medium |
| R8 | public is Disabled, or netDefault is Deny, once R1 is done | Medium |
| R9 | No Owner or User Access Administrator outside your own administrators. Deploy identity’s scope ends at the resource group | High |
| R10 | Portal: the deploy identity’s secret is inside 90 days, or it uses a federated credential | Medium |
| R11 | At least one setting named, retention of 90 or more, and capGb of -1 or high enough never to stop ingestion | High |
| R12 | Each command names at least one setting | Medium |
| R13 | Both commands answer. Subscription Not Registered is a fail: register Microsoft.Security | Medium |
| R14 | A CanNotDelete lock on PostgreSQL and on storage | Medium |
Recording the result
Section titled “Recording the result”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.
Next steps
Section titled “Next steps”- Securing Azure - the tenant, connector app registrations and sign-in
- Azure in your own subscription - only if Demiton runs in your subscription
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.