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)
| Component | Description | Example |
|---|---|---|
| Users/Groups | Who the policy applies to | All users, specific groups, guest users |
| Cloud Apps | What apps are protected | Office 365, Azure Portal, all apps |
| Conditions | When the policy triggers | Locations, device platforms, sign-in risk |
2. Access Controls (Action)
| Control | Description |
|---|---|
| Grant | Allow access with requirements (MFA, compliant device) |
| Block | Deny access completely |
| Session | Limit 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
- Navigate to Entra ID > Security > Conditional Access
- Click + New policy
- Name:
CA001 - Require MFA for All Users - Assignments:
- Users: All users
- Exclude: Emergency access accounts (break-glass)
- Cloud apps: All cloud apps
- Access controls > Grant:
- Select Require multifactor authentication
- Enable policy: Report-only (test first, then switch to On)
- 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:
- Deploy policy in report-only mode
- Monitor for 1-2 weeks in Sign-in logs
- Identify and fix issues (e.g., exclude service accounts)
- Switch to
enabledstate
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:
- Require MFA for admins
- Require MFA for all users
- Block legacy authentication
- Require MFA for Azure management
- 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:
- User excluded: Check exclusions in policy
- App not targeted: Verify "Cloud apps" includes the app
- Conditions not met: Review location, platform, client app conditions
- 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
| Feature | Security Defaults | Conditional Access |
|---|---|---|
| License | Free (all tenants) | Azure AD P1 required |
| MFA | Required for admins, prompted for users | Granular policies |
| Legacy Auth | Blocked | Customizable blocking |
| Flexibility | None (all-or-nothing) | Highly customizable |
| Best For | Small orgs, basic security | Enterprises, 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\"]
}