Skip to main content

Conditional Access

What is Conditional Access?

Conditional Access is the policy engine that enforces organizational access controls for Azure AD (Entra ID) authentication. It's the foundation of Zero Trust security by enforcing "never trust, always verify" principles.

How it works:

User Sign-in Attempt

┌───────────────────┐
│ IF Conditions Met │ (user, location, device, app, risk)
└────────┬──────────┘

┌────┴────┐
▼ ▼
┌───────┐ ┌───────┐
│ GRANT │ │ BLOCK │
└───┬───┘ └───┬───┘
│ │
▼ ▼
Access Denied

Key Principles:

  • Signal-driven: Uses identity signals (who, where, what, how, risk)
  • Intelligent access control: Automates decisions based on policies
  • Zero Trust: Verify explicitly, use least privilege, assume breach

Conditional Access Policy Components

1. Assignments (Who/What/When)

ComponentDescriptionExample
Users/GroupsWho the policy applies toAll users, specific groups, guest users
Cloud AppsWhat apps are protectedOffice 365, Azure Portal, all apps
ConditionsWhen the policy triggersLocations, device platforms, sign-in risk

2. Access Controls (Action)

ControlDescription
GrantAllow access with requirements (MFA, compliant device)
BlockDeny access completely
SessionLimit experience (app restrictions, sign-in frequency)

Common Conditional Access Policies

1. Require MFA for All Users

Scenario: Enforce multi-factor authentication for every sign-in

Using Azure Portal

  1. Navigate to Entra ID > Security > Conditional Access
  2. Click + New policy
  3. Name: CA001 - Require MFA for All Users
  4. Assignments:
    • Users: All users
    • Exclude: Emergency access accounts (break-glass)
    • Cloud apps: All cloud apps
  5. Access controls > Grant:
    • Select Require multifactor authentication
  6. Enable policy: Report-only (test first, then switch to On)
  7. Click Create

Using Azure CLI

# Create Conditional Access policy via Graph API
az rest --method POST \
--url \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \
--headers \"Content-Type=application/json\" \
--body '{
\"displayName\": \"CA001 - Require MFA for All Users\",
\"state\": \"enabledForReportingButNotEnforced\",
\"conditions\": {
\"users\": {
\"includeUsers\": [\"All\"],
\"excludeUsers\": [\"emergency-access-account-object-id\"]
},
\"applications\": {
\"includeApplications\": [\"All\"]
}
},
\"grantControls\": {
\"operator\": \"OR\",
\"builtInControls\": [\"mfa\"]
}
}'

2. Require MFA for Admins

# Higher priority policy for admin accounts
az rest --method POST \
--url \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \
--headers \"Content-Type=application/json\" \
--body '{
\"displayName\": \"CA002 - Require MFA for Admins\",
\"state\": \"enabled\",
\"conditions\": {
\"users\": {
\"includeRoles\": [
\"62e90394-69f5-4237-9190-012177145e10\", # Global Administrator
\"f28a1f50-f6e7-4571-818b-6a12f2af6b6c\", # Privileged Role Administrator
\"29232cdf-9323-42fd-ade2-1d097af3e4de\" # User Administrator
]
},
\"applications\": {
\"includeApplications\": [\"All\"]
}
},
\"grantControls\": {
\"operator\": \"OR\",
\"builtInControls\": [\"mfa\"]
}
}'

3. Block Legacy Authentication

Legacy auth (IMAP, POP3, SMTP) doesn't support MFA and is a common attack vector.

Portal Configuration:

  • Name: CA003 - Block Legacy Authentication
  • Users: All users (exclude service accounts if needed)
  • Cloud apps: All cloud apps
  • Conditions > Client apps:
    • Exchange ActiveSync clients
    • Other clients (IMAP, POP, SMTP, older Office clients)
  • Access controls: Block access

4. Require Compliant or Hybrid Azure AD Joined Device

az rest --method POST \
--url \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \
--headers \"Content-Type=application/json\" \
--body '{
\"displayName\": \"CA004 - Require Compliant Device for Office 365\",
\"state\": \"enabled\",
\"conditions\": {
\"users\": {
\"includeUsers\": [\"All\"]
},
\"applications\": {
\"includeApplications\": [\"Office365\"]
}
},
\"grantControls\": {
\"operator\": \"OR\",
\"builtInControls\": [\"compliantDevice\", \"domainJoinedDevice\"]
}
}'

5. Block Access from Specific Locations

# First, create a named location (untrusted countries)
az rest --method POST \
--url \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations\" \
--headers \"Content-Type=application/json\" \
--body '{
\"@odata.type\": \"#microsoft.graph.countryNamedLocation\",
\"displayName\": \"Blocked Countries\",
\"countriesAndRegions\": [\"KP\", \"IR\", \"SY\"],
\"includeUnknownCountriesAndRegions\": false
}'

# Then create policy to block from those locations
az rest --method POST \
--url \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \
--headers \"Content-Type=application/json\" \
--body '{
\"displayName\": \"CA005 - Block Access from High-Risk Countries\",
\"state\": \"enabled\",
\"conditions\": {
\"users\": {
\"includeUsers\": [\"All\"]
},
\"applications\": {
\"includeApplications\": [\"All\"]
},
\"locations\": {
\"includeLocations\": [\"named-location-id-from-above\"]
}
},
\"grantControls\": {
\"operator\": \"OR\",
\"builtInControls\": [\"block\"]
}
}'

6. Require MFA for Azure Management

Critical for protecting control plane operations.

Portal:

  • Name: CA006 - Require MFA for Azure Management
  • Users: All users
  • Cloud apps: Microsoft Azure Management (includes Portal, PowerShell, CLI)
  • Grant: Require MFA

Terraform Example

# Named Location: Corporate Network
resource \"azuread_named_location\" \"corporate_network\" {\n display_name = \"Corporate Network\"

ip {
ip_ranges = [
\"203.0.113.0/24\", # Office public IP range
\"198.51.100.0/24\"
]
trusted = true # Considered trusted location
}
}

# Named Location: Blocked Countries
resource \"azuread_named_location\" \"blocked_countries\" {\n display_name = \"High-Risk Countries\"

country {
countries_and_regions = [
\"KP\", # North Korea
\"IR\", # Iran
\"SY\" # Syria
]
include_unknown_countries_and_regions = false
}
}

# CA Policy: Require MFA for All Users
resource \"azuread_conditional_access_policy\" \"require_mfa_all\" {\n display_name = \"CA001 - Require MFA for All Users\"
state = \"enabledForReportingButNotEnforced\" # Start with report-only

conditions {
users {
included_users = [\"All\"]
excluded_users = [
azuread_user.break_glass.object_id # Emergency access account
]
}

applications {
included_applications = [\"All\"]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"mfa\"]
}
}

# CA Policy: Block Legacy Auth
resource \"azuread_conditional_access_policy\" \"block_legacy_auth\" {\n display_name = \"CA003 - Block Legacy Authentication\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
}

applications {
included_applications = [\"All\"]
}

client_app_types = [
\"exchangeActiveSync\",
\"other\" # IMAP, POP3, SMTP, legacy Office clients
]
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"block\"]
}
}

# CA Policy: Require Compliant Device
resource \"azuread_conditional_access_policy\" \"require_compliant_device\" {\n display_name = \"CA004 - Require Compliant Device for Office 365\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
}

applications {
included_applications = [\"Office365\"]
}

platforms {
included_platforms = [\"windows\", \"macOS\", \"iOS\", \"android\"]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [
\"compliantDevice\", # Intune-managed compliant
\"domainJoinedDevice\" # Hybrid Azure AD joined
]
}
}

# CA Policy: Require MFA from Untrusted Locations
resource \"azuread_conditional_access_policy\" \"mfa_untrusted_locations\" {\n display_name = \"CA007 - Require MFA from Untrusted Locations\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
}

applications {
included_applications = [\"All\"]
}

locations {
included_locations = [\"All\"]
excluded_locations = [
azuread_named_location.corporate_network.id,
\"AllTrusted\" # All trusted named locations
]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"mfa\"]
}
}

# CA Policy: Block Access from High-Risk Countries
resource \"azuread_conditional_access_policy\" \"block_high_risk_countries\" {\n display_name = \"CA005 - Block High-Risk Countries\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
excluded_groups = [
azuread_group.global_travelers.object_id # Employees who travel
]
}

applications {
included_applications = [\"All\"]
}

locations {
included_locations = [azuread_named_location.blocked_countries.id]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"block\"]
}
}

# CA Policy: Sign-in Risk-Based (Requires Azure AD P2)
resource \"azuread_conditional_access_policy\" \"block_high_risk_signin\" {\n display_name = \"CA008 - Block High-Risk Sign-ins\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
}

applications {
included_applications = [\"All\"]
}

sign_in_risk_levels = [\"high\"] # Requires Azure AD P2
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"block\"]
}
}

CI/CD Integration

Deploy Policies with GitHub Actions

name: Deploy Conditional Access Policies

on:
push:
paths:
- 'conditional-access/**'

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Azure Login (Service Principal)
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Get Access Token
id: get_token
run: |\n TOKEN=$(az account get-access-token --resource https://graph.microsoft.com --query accessToken -o tsv)
echo \"::add-mask::$TOKEN\"
echo \"TOKEN=$TOKEN\" >> $GITHUB_ENV

- name: Deploy Conditional Access Policies
run: |\n for policy in conditional-access/*.json; do
POLICY_NAME=$(jq -r '.displayName' $policy)
echo \"Deploying policy: $POLICY_NAME\"

curl -X POST \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d @$policy
done

Test Policies in Report-Only Mode

- name: Enable Report-Only Mode
run: |\n # Always deploy new policies in report-only mode first
jq '.state = \"enabledForReportingButNotEnforced\"' policy.json > policy-report-only.json

curl -X POST \"https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies\" \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d @policy-report-only.json

Best Practices

1. Use Report-Only Mode First

Always test with report-only:

state = \"enabledForReportingButNotEnforced\"

Workflow:

  1. Deploy policy in report-only mode
  2. Monitor for 1-2 weeks in Sign-in logs
  3. Identify and fix issues (e.g., exclude service accounts)
  4. Switch to enabled state

2. Exclude Emergency Access Accounts

Always exclude break-glass accounts:

  • Create 2 cloud-only admin accounts
  • Store credentials in physical safe
  • Exclude from all Conditional Access policies
conditions {
users {
included_users = [\"All\"]
excluded_users = [
\"emergency-admin-1@contoso.com\",
\"emergency-admin-2@contoso.com\"
]
}
}

3. Prioritize Policies with Naming Convention

Use prefix to indicate priority/category:

  • CA001-CA099: Foundation policies (MFA, block legacy auth)
  • CA100-CA199: App-specific policies
  • CA200-CA299: Location-based policies
  • CA300-CA399: Device compliance policies
  • CA400-CA499: Risk-based policies (P2)

4. Start with Template Policies

Microsoft provides Zero Trust policy templates:

  • Portal: Entra ID > Security > Conditional Access > Policies > Templates

Secure Foundation templates:

  1. Require MFA for admins
  2. Require MFA for all users
  3. Block legacy authentication
  4. Require MFA for Azure management
  5. Securing security info registration

5. Monitor Policy Impact

// Sign-in logs with Conditional Access details
SigninLogs
| where TimeGenerated > ago(24h)
| where ConditionalAccessStatus != \"notApplied\"
| project TimeGenerated, UserPrincipalName, AppDisplayName, ConditionalAccessStatus,
ConditionalAccessPolicies
| order by TimeGenerated desc

// Policies causing failures
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType != 0 // Failed sign-in
| mv-expand ConditionalAccessPolicies
| where ConditionalAccessPolicies.result == \"failure\"
| summarize FailureCount = count() by tostring(ConditionalAccessPolicies.displayName)
| order by FailureCount desc

Common Use Cases

1. Require MFA for External Users (Guests)

resource \"azuread_conditional_access_policy\" \"mfa_guests\" {\n  display_name = \"CA010 - Require MFA for Guest Users\"
state = \"enabled\"

conditions {
users {
included_guest_or_external_user_types = [\"b2bCollaborationGuest\"]
}

applications {
included_applications = [\"All\"]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"mfa\"]
}
}

2. Limit Session for Unmanaged Devices

resource \"azuread_conditional_access_policy\" \"limit_session_unmanaged\" {\n  display_name = \"CA011 - Limited Session for Unmanaged Devices\"
state = \"enabled\"

conditions {
users {
included_users = [\"All\"]
}

applications {
included_applications = [\"Office365\"]
}
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"mfa\"]
}

session_controls {
sign_in_frequency {
value = 1
type = \"hours\"
}

persistent_browser {
mode = \"never\" # Don't remember session
}
}
}

3. Block Downloads on Unmanaged Devices

Requires App Protection Policies integration:

session_controls {
application_enforced_restrictions {
is_enabled = true # Block download/print/sync for SharePoint/OneDrive
}
}

Things to Avoid

Don't enable policies without testing in report-only mode first ❌ Don't forget to exclude emergency access accounts ❌ Don't create conflicting policies (e.g., one requires device, another blocks it) ❌ Don't apply blocking policies to service accounts (break automation) ❌ Don't use only cloud-based exclusions (have break-glass procedure) ❌ Don't ignore sign-in logs after policy deployment ❌ Don't require device compliance without Intune deployment ❌ Don't block all legacy auth without migrating apps first

Do test with report-only mode (1-2 weeks minimum) ✅ Do exclude 2+ break-glass accounts from all policies ✅ Do use naming conventions (CA001, CA002, etc.) ✅ Do start with Microsoft's template policies ✅ Do monitor sign-in logs for policy impact ✅ Do document policy intent and scope ✅ Do use groups for user assignments (easier to manage) ✅ Do combine MFA with compliant device where possible

Troubleshooting

User Cannot Sign In

# Check sign-in logs for specific user
az ad signed-in-user list-owned-objects # In portal: Azure AD > Sign-in logs

# Look for:
# - ResultType: 0 = success, non-zero = failure
# - ConditionalAccessStatus: success, failure, notApplied
# - Failure reason

Policy Not Applying

Common causes:

  1. User excluded: Check exclusions in policy
  2. App not targeted: Verify "Cloud apps" includes the app
  3. Conditions not met: Review location, platform, client app conditions
  4. Policy disabled: Check state is enabled

Testing Policies

Use What If tool:

  • Portal: Conditional Access > What If
  • Select user, app, IP, device to simulate
  • Shows which policies would apply
# Via Graph API
az rest --method POST \
--url \"https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate\" \
--body '{
\"user\": {
\"id\": \"user-object-id\"
},
\"application\": {
\"id\": \"office365-app-id\"
},
\"ipAddress\": \"203.0.113.50\"
}'

Security Defaults vs Conditional Access

FeatureSecurity DefaultsConditional Access
LicenseFree (all tenants)Azure AD P1 required
MFARequired for admins, prompted for usersGranular policies
Legacy AuthBlockedCustomizable blocking
FlexibilityNone (all-or-nothing)Highly customizable
Best ForSmall orgs, basic securityEnterprises, compliance

Recommendation: Use Conditional Access if you have P1/P2 licenses

Advanced Scenarios (Azure AD P2)

Risk-Based Policies

Requires Azure AD Identity Protection:

# Block high-risk sign-ins
conditions {
sign_in_risk_levels = [\"high\", \"medium\"]
}

grant_controls {
operator = \"OR\"
built_in_controls = [\"block\"]
}

# Require password change for high-risk users
conditions {
user_risk_levels = [\"high\"]
}

grant_controls {
operator = \"AND\"
built_in_controls = [\"mfa\", \"passwordChange\"]
}

Terms of Use

grant_controls {
operator = \"AND\"
built_in_controls = [\"mfa\"]
terms_of_use = [\"terms-of-use-id\"]
}