IAM & Access Control
TSC mapping: CC5 (Control Activities), CC6.1 (Logical Access Security), CC6.2 (Access Provisioning), CC6.3 (Access Removal)
Identity is the primary control plane in Azure. SOC 2 auditors will review Entra ID sign-in and audit logs, Conditional Access policies, PIM role assignments, and RBAC policy exports. Get this layer right before anything else.
1. Global Administrator Account Hardening
Global Administrators have unrestricted access to the entire Entra ID tenant and Azure estate. They must be locked down and used only when necessary.
Enforce phishing-resistant MFA for Global Admins:
# List current Global Administrator assignments
az ad directory-role list --query "[?displayName=='Global Administrator'].id" -o tsv | \
xargs -I{} az ad directory-role member list --id {}
# Verify MFA registration status (requires Microsoft Graph)
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/reports/credentialUserRegistrationDetails" \
--query "value[?isMfaRegistered==false].userPrincipalName"
Best practices:
- Use 2–5 Global Admin accounts maximum; all must be cloud-only (not synced from on-premises AD).
- Assign Global Admin only when needed via PIM; remove the permanent assignment.
- Enable Entra ID Privileged Identity Management for all privileged roles.
Reference: Entra ID admin role best practices →
2. Multi-Factor Authentication via Conditional Access
Enforce MFA for all users via a Conditional Access policy. Do not rely on per-user MFA settings — they are legacy and less auditable.
Create a baseline MFA policy (all users, all cloud apps):
# Requires az rest or Graph API — example using az rest
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
--body '{
"displayName": "Require MFA for all users",
"state": "enabled",
"conditions": {
"users": { "includeUsers": ["All"], "excludeUsers": [] },
"applications": { "includeApplications": ["All"] }
},
"grantControls": {
"operator": "OR",
"builtInControls": ["mfa"]
}
}'
Audit MFA compliance:
# List users without MFA registered
az rest --method GET \
--uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails" \
--query "value[?isMfaRegistered==false].[userPrincipalName,userType]" \
--output table
Key Conditional Access policies to implement:
| Policy | Condition | Grant control |
|---|---|---|
| Require MFA — all users | All users, all apps | MFA |
| Require compliant device | All users, all apps | Compliant device OR MFA |
| Block legacy authentication | Legacy auth clients | Block |
| Require phishing-resistant MFA for admins | Directory roles | FIDO2 / certificate-based |
| Sign-in risk policy | Sign-in risk ≥ Medium | MFA |
| User risk policy | User risk ≥ High | MFA + password change |
Reference: Conditional Access documentation →
3. Privileged Identity Management (PIM)
PIM enforces just-in-time (JIT) access to privileged Azure AD roles and Azure RBAC roles. Auditors expect privileged access to be time-limited, approved, and logged.
Enable PIM for the most sensitive roles:
# List eligible PIM role assignments for the current user
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules" \
--query "value[*].[roleDefinition.displayName,principal.userPrincipalName,scheduleInfo.expiration.type]"
Recommended PIM configuration:
| Role | Activation duration | Approval required | MFA on activation |
|---|---|---|---|
| Global Administrator | 1 hour | Yes (2 approvers) | Yes |
| Privileged Role Administrator | 2 hours | Yes | Yes |
| Security Administrator | 4 hours | Yes | Yes |
| Subscription Owner | 4 hours | Yes | Yes |
| Contributor (production) | 8 hours | No | Yes |
Review PIM audit logs:
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?\$filter=category eq 'RoleManagement'" \
--query "value[*].[activityDateTime,activityDisplayName,initiatedBy.user.userPrincipalName,targetResources[0].displayName]" \
--output table
Reference: PIM documentation →
4. Azure RBAC — Least Privilege
Auditors will sample Azure role assignments. Overly broad assignments (Owner at subscription scope, custom roles with * actions) are the most common SOC 2 finding.
Audit all Owner/Contributor assignments at subscription scope:
# List Owner assignments at subscription scope (flag all for review)
az role assignment list \
--role Owner \
--scope /subscriptions/<subscription-id> \
--query "[*].[principalName,principalType,roleDefinitionName,scope]" \
--output table
# List all role assignments across a subscription
az role assignment list \
--all \
--subscription <subscription-id> \
--query "[*].[principalName,principalType,roleDefinitionName,scope]" \
--output table
Assign least-privilege built-in roles:
# Assign Reader to a user at resource group scope (not subscription)
az role assignment create \
--assignee [email protected] \
--role Reader \
--scope /subscriptions/<sub>/resourceGroups/<rg>
# Remove an overly permissive Owner assignment
az role assignment delete \
--assignee [email protected] \
--role Owner \
--scope /subscriptions/<subscription-id>
Recommended role pattern:
| Persona | Role | Scope |
|---|---|---|
| All engineers (default) | Reader | Subscription |
| Developers | Contributor | Dev/staging resource groups |
| SRE/Ops | Contributor | Production resource groups (via PIM) |
| Security team | Security Reader | Subscription |
| DBA | SQL DB Contributor | Database resource groups |
| Break-glass admin | Owner | Subscription (PIM, approval required) |
Reference: Azure RBAC documentation →
5. Password Policy and Entra ID Password Protection
Enable Entra ID Password Protection to ban weak passwords and enforce a custom banned-password list.
# Enable Entra ID Password Protection (via portal or Graph)
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/domains/<domain>/federationConfiguration" \
--body '{"passwordNotificationWindowInDays": 14, "passwordValidityPeriodInDays": 90}'
Recommended settings:
- Minimum length: 14 characters
- Enable Entra ID Smart Lockout (default: 10 failed attempts)
- Enable custom banned password list with company name variations
- Enforce password expiry: 90 days for standard users (or use SSPR with passkeys)
Reference: Entra ID Password Protection →
6. Entra ID Access Reviews
SOC 2 CC6.3 requires evidence that access is removed when no longer needed. Entra ID Access Reviews provide automated, auditable quarterly access reviews.
Create a recurring access review for privileged roles:
# Create an access review for Global Administrator role
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions" \
--body '{
"displayName": "Quarterly Global Admin Review",
"descriptionForAdmins": "Quarterly review of Global Administrator assignments",
"scope": {
"query": "/roleManagement/directory/roleAssignments?$filter=roleDefinition/id eq '<role-id>'",
"queryType": "MicrosoftGraph"
},
"reviewers": [{"query": "/users/<reviewer-id>", "queryType": "MicrosoftGraph"}],
"settings": {
"mailNotificationsEnabled": true,
"reminderNotificationsEnabled": true,
"justificationRequiredOnApproval": true,
"defaultDecisionEnabled": true,
"defaultDecision": "Deny",
"instanceDurationInDays": 14,
"recurrence": {
"pattern": {"type": "absoluteMonthly", "interval": 3},
"range": {"type": "noEnd"}
},
"autoApplyDecisionsEnabled": true
}
}'
Review process:
- Schedule quarterly reviews for all privileged roles (Owner, Contributor, Security Admin).
- Reviewers confirm whether access is still required within 14 days.
- Unchallenged access is automatically revoked (
defaultDecision: Deny). - Export review results and retain for audit evidence.
Reference: Entra ID Access Reviews →
7. Service Principal and Workload Identity Management
Service principals are the Azure equivalent of IAM service accounts. Long-lived client secrets are a common audit finding.
Audit expiring and expired credentials:
# List all service principals with expiring client secrets
az ad app list --all \
--query "[*].{App:displayName, AppId:appId}" \
--output table
# List credentials for a specific app (check expiry dates)
az ad app credential list \
--id <app-id> \
--query "[*].[displayName,endDateTime,keyId]" \
--output table
Best practices:
- Use Managed Identities instead of service principals wherever possible — no credential management required.
- If a service principal is needed, use certificate credentials instead of client secrets.
- Set a maximum client secret lifetime of 90 days; rotate before expiry.
- Use Workload Identity Federation for CI/CD pipelines (GitHub Actions, Azure DevOps) — eliminates stored secrets entirely.
# Assign a managed identity to an Azure VM
az vm identity assign \
--name my-vm \
--resource-group my-rg \
--identities [system]
# Grant the VM's managed identity access to a Key Vault
az keyvault set-policy \
--name my-keyvault \
--object-id <vm-principal-id> \
--secret-permissions get list
Reference: Managed Identities → · Workload Identity Federation →
SOC 2 Evidence Checklist for IAM
| Evidence item | How to export |
|---|---|
| Entra ID user list with MFA status | Graph API: credentialUserRegistrationDetails |
| Conditional Access policy export | Entra portal → CA policies → Export JSON |
| PIM role assignment history | Graph API: privilegedAccess/azureResources/roleAssignments |
| Azure RBAC assignment report | az role assignment list --all |
| Access Review results | Entra portal → Identity Governance → Access Reviews → Export |
| Service principal credential expiry list | az ad app credential list per app |
| Entra ID audit log (role changes, user provisioning) | Log Analytics workspace or Entra portal export |
| Password policy configuration | Entra portal → Authentication Methods → Password Protection |
Official references: