Microsoft Entra ID Hardening Guide
Identity Provider hardening for Azure Active Directory, Conditional Access, PIM, and Zero Trust
Overview
Microsoft Entra ID (formerly Azure Active Directory) is the cloud identity platform for over 720 million users across enterprises worldwide. As the authentication backbone for Microsoft 365, Azure, and thousands of SaaS applications, Entra ID security is foundational to Zero Trust architecture. The January 2024 Midnight Blizzard breach of Microsoft’s corporate environment demonstrated how a single misconfigured test account without MFA can cascade into widespread compromise.
Intended Audience
- Security engineers managing identity infrastructure
- IT administrators configuring Entra ID tenants
- GRC professionals assessing IAM compliance
- Third-party risk managers evaluating SSO integrations
How to Use This Guide
- L1 (Baseline): Essential controls for all organizations
- L2 (Hardened): Enhanced controls for security-sensitive environments
- L3 (Maximum Security): Strictest controls for regulated industries
Scope
This guide covers Microsoft Entra ID security configurations including authentication policies, Conditional Access, Privileged Identity Management, application security, and Zero Trust identity architecture. Microsoft 365 and Azure infrastructure are covered in separate guides.
Table of Contents
- Authentication & Access Controls
- Conditional Access
- Privileged Identity Management
- Application Security
- Monitoring & Detection
- Third-Party Integration Security
- Compliance Quick Reference
1. Authentication & Access Controls
1.1 Enforce Phishing-Resistant MFA
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 6.3, 6.5 |
| NIST 800-53 | IA-2(1), IA-2(6) |
| CIS Azure | 1.1.1 |
Description
Require phishing-resistant MFA (FIDO2 security keys, Windows Hello for Business, or certificate-based authentication) for all users. Microsoft reports that MFA blocks over 99.9% of automated attacks.
Rationale
Why This Matters:
- Password spray and credential stuffing remain top attack vectors
- Traditional MFA (SMS, voice) vulnerable to SIM swapping
- Phishing-resistant MFA eliminates real-time phishing attacks
Attack Prevented: Password spray, phishing, credential theft, MFA fatigue
Real-World Incidents:
- Midnight Blizzard (2024): Test account without MFA led to Microsoft corporate email compromise
- CVE-2025-55241: Critical Entra ID privilege escalation vulnerability (CVSS 10.0) could compromise any tenant
Prerequisites
- Microsoft Entra ID P1 or P2 license
- FIDO2 security keys for privileged users
- Security Administrator or Global Administrator role
ClickOps Implementation
Step 1: Enable Security Defaults (Basic Tenants)
- Navigate to: Microsoft Entra admin center → Identity → Overview → Properties
- Click Manage security defaults
- Set to Enabled
- Click Save
Note: Security Defaults provide basic MFA but lack granular control. Enterprise environments should use Conditional Access instead.
Step 2: Configure Authentication Methods
- Navigate to: Protection → Authentication methods → Policies
- Enable desired methods:
- FIDO2 security key: Enable for all users
- Microsoft Authenticator: Enable with number matching and location display
- Temporary Access Pass: Enable for initial onboarding
- Disable weak methods:
- SMS/Voice: Disable or restrict to recovery only
Step 3: Create Authentication Strength
- Navigate to: Protection → Authentication methods → Authentication strengths
- Click + New authentication strength
- Name: “Phishing-Resistant MFA”
- Select:
- FIDO2 security key
- Windows Hello for Business
- Certificate-based authentication (CBA)
- Save and use in Conditional Access policies
Time to Complete: ~45 minutes
Code Implementation
Code Pack: CLI Script
# Connect to Microsoft Graph
Connect-MgGraph -Scopes "Policy.ReadWrite.AuthenticationMethod"
# Get current authentication method policy
$policy = Get-MgPolicyAuthenticationMethodPolicy
# Enable FIDO2
$fido2Config = @{
id = "fido2"
state = "enabled"
includeTargets = @(
@{
targetType = "group"
id = "all_users"
}
)
}
Update-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration `
-AuthenticationMethodConfigurationId "fido2" `
-BodyParameter $fido2Config
# Configure Microsoft Authenticator with number matching
$authAppConfig = @{
id = "microsoftAuthenticator"
state = "enabled"
featureSettings = @{
displayAppInformationRequiredState = @{
state = "enabled"
}
displayLocationInformationRequiredState = @{
state = "enabled"
}
numberMatchingRequiredState = @{
state = "enabled"
}
}
}
Update-MgPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration `
-AuthenticationMethodConfigurationId "microsoftAuthenticator" `
-BodyParameter $authAppConfig
Validation & Testing
How to verify the control is working:
- Sign in as test user - MFA prompt should appear
- Verify number matching in Microsoft Authenticator
- Review sign-in logs: Monitoring → Sign-in logs
- Check Identity Secure Score for MFA adoption
Expected result: All users require MFA, phishing-resistant methods preferred
Compliance Mappings
| Framework | Control ID | Control Description |
|---|---|---|
| SOC 2 | CC6.1 | Logical access security |
| NIST 800-53 | IA-2(1), IA-2(6) | Multi-factor authentication |
| ISO 27001 | A.9.4.2 | Secure log-on procedures |
| CIS Azure | 1.1.1 | Ensure MFA is enabled for all users |
1.2 Configure Emergency Access (Break-Glass) Accounts
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 5.1 |
| NIST 800-53 | AC-2 |
| CIS Azure | 1.1.5 |
Description
Create highly protected emergency access accounts excluded from Conditional Access and MFA policies to ensure tenant access during outages or lockout scenarios.
Rationale
Why This Matters:
- Conditional Access misconfiguration can lock out all admins
- Federation or MFA provider outages can prevent authentication
- Break-glass accounts provide last-resort access
Best Practice:
- Minimum 2 cloud-only accounts (no federation dependency)
- Long, complex passwords stored securely offline
- Excluded from all Conditional Access policies
- Monitored for any sign-in activity
Prerequisites
- Global Administrator access
- Secure offline storage (safe, vault)
- Alerting configured for emergency account usage
ClickOps Implementation
Step 1: Create Emergency Accounts
- Navigate to: Microsoft Entra admin center → Users → All users
- Click + New user → Create new user
- Configure:
- Username:
emergency-admin-01@yourdomain.onmicrosoft.com - Use
.onmicrosoft.comdomain (cloud-only, no federation) - Password: Generate 64+ character random password
- Username:
- Assign Global Administrator role
- Create second account (emergency-admin-02)
Step 2: Exclude from Conditional Access
- Navigate to: Protection → Conditional Access → Policies
- Edit each policy
- Under Users → Exclude, add emergency accounts
- Save all policies
Step 3: Configure Monitoring
- Navigate to: Monitoring → Diagnostic settings
- Create alert rule:
- Condition: Sign-in logs where User = emergency accounts
- Action: Email Security team, create incident
Step 4: Store Credentials Securely
- Print credentials on paper (no digital storage)
- Store in physically secure location (safe, vault)
- Split credentials between multiple custodians if possible
- Document access procedures
Time to Complete: ~45 minutes
Code Implementation
Code Pack: Terraform
# Create emergency access (break-glass) accounts
resource "azuread_user" "emergency_admin" {
count = var.emergency_account_count
user_principal_name = "${var.emergency_account_upn_prefix}-${format("%02d", count.index + 1)}@${var.domain_name}"
display_name = "Emergency Admin ${format("%02d", count.index + 1)}"
mail_nickname = "${var.emergency_account_upn_prefix}-${format("%02d", count.index + 1)}"
account_enabled = true
# Password is managed outside Terraform -- generate a 64+ character
# random password and store it in a physically secure location (safe/vault).
# Terraform manages the account lifecycle, not the credential.
password = random_password.emergency[count.index].result
force_password_change = false
disable_password_expiration = true
disable_strong_password = false
lifecycle {
ignore_changes = [password]
}
}
# Generate initial passwords for emergency accounts
resource "random_password" "emergency" {
count = var.emergency_account_count
length = 64
special = true
override_special = "!@#$%&*()-_=+[]{}|;:,.<>?"
}
# Look up the Global Administrator role
data "azuread_directory_role" "global_admin" {
display_name = "Global Administrator"
}
# Assign Global Administrator role to emergency accounts
resource "azuread_directory_role_assignment" "emergency_global_admin" {
count = var.emergency_account_count
role_id = data.azuread_directory_role.global_admin.template_id
principal_object_id = azuread_user.emergency_admin[count.index].object_id
}
# Create a group for emergency accounts (used for Conditional Access exclusions)
resource "azuread_group" "emergency_access" {
display_name = "HTH Emergency Access Accounts"
description = "Break-glass accounts excluded from Conditional Access policies"
security_enabled = true
mail_enabled = false
members = azuread_user.emergency_admin[*].object_id
}
Code Pack: CLI Script
# Create emergency access account
$passwordProfile = @{
password = [System.Web.Security.Membership]::GeneratePassword(64, 10)
forceChangePasswordNextSignIn = $false
}
$params = @{
accountEnabled = $true
displayName = "Emergency Admin 01"
mailNickname = "emergency-admin-01"
userPrincipalName = "emergency-admin-01@yourdomain.onmicrosoft.com"
passwordProfile = $passwordProfile
}
$user = New-MgUser -BodyParameter $params
# Assign Global Administrator role
$roleId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'Global Administrator'").Id
New-MgRoleManagementDirectoryRoleAssignment -BodyParameter @{
"@odata.type" = "#microsoft.graph.unifiedRoleAssignment"
roleDefinitionId = $roleId
principalId = $user.Id
directoryScopeId = "/"
}
# Output password (store securely)
Write-Host "Password: $($passwordProfile.password)" -ForegroundColor Yellow
Write-Host "STORE THIS SECURELY AND DELETE FROM TERMINAL HISTORY"
Validation & Testing
- Test sign-in with emergency account (then immediately change password)
- Verify bypasses all Conditional Access policies
- Confirm alert triggers on sign-in
- Document and secure credentials
2. Conditional Access
2.1 Block Legacy Authentication
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 4.2 |
| NIST 800-53 | IA-2, AC-17 |
| CIS Azure | 1.1.2 |
Description
Block legacy authentication protocols (Basic Auth, POP, IMAP, SMTP AUTH) that cannot enforce MFA and are commonly exploited in password spray attacks.
Rationale
Why This Matters:
- Legacy protocols bypass MFA completely
- Password spray attacks frequently target these endpoints
- Microsoft has deprecated Basic Auth
Attack Prevented: Password spray via legacy protocols, credential replay
ClickOps Implementation
Step 1: Create Block Legacy Auth Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Block legacy authentication
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions → Client apps: Select “Exchange ActiveSync clients” and “Other clients”
- Grant: Block access
- Enable policy: On
- Click Create
Time to Complete: ~15 minutes
Code Implementation
Code Pack: Terraform
# Conditional Access policy to block legacy authentication protocols
# (Basic Auth, POP, IMAP, SMTP AUTH) that cannot enforce MFA
resource "azuread_conditional_access_policy" "block_legacy_auth" {
display_name = "HTH: Block legacy authentication"
state = var.legacy_auth_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["exchangeActiveSync", "other"]
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
Code Pack: CLI Script
# Create policy to block legacy auth
$params = @{
displayName = "Block legacy authentication"
state = "enabled"
conditions = @{
users = @{
includeUsers = @("All")
excludeUsers = @("EMERGENCY_ACCOUNT_1_ID", "EMERGENCY_ACCOUNT_2_ID")
}
applications = @{
includeApplications = @("All")
}
clientAppTypes = @("exchangeActiveSync", "other")
}
grantControls = @{
operator = "OR"
builtInControls = @("block")
}
}
New-MgIdentityConditionalAccessPolicy -BodyParameter $params
2.2 Require MFA for All Users
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 6.3 |
| NIST 800-53 | IA-2(1) |
| CIS Azure | 1.1.3 |
Description
Create Conditional Access policy requiring MFA for all interactive sign-ins to all cloud applications.
ClickOps Implementation
Step 1: Create MFA Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Require MFA for all users
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions: None (any condition)
- Grant: Require multifactor authentication
- Enable policy: On
- Click Create
Code Implementation
Code Pack: Terraform
# Conditional Access policy requiring MFA for all interactive sign-ins
resource "azuread_conditional_access_policy" "require_mfa_all_users" {
display_name = "HTH: Require MFA for all users"
state = var.mfa_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
}
grant_controls {
operator = "OR"
built_in_controls = ["mfa"]
}
}
2.3 Require Compliant Devices for Admins
Profile Level: L2 (Hardened)
| Framework | Control |
|---|---|
| CIS Controls | 4.1, 6.4 |
| NIST 800-53 | AC-2(11), AC-6(1) |
Description
Require privileged users to access admin portals only from Intune-compliant or Hybrid Azure AD joined devices.
ClickOps Implementation
Step 1: Create Admin Device Compliance Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Require compliant device for admins
- Users: Select directory roles → All admin roles
- Cloud apps: Microsoft Admin Portals (or all apps)
- Grant: Require device to be marked as compliant OR Require Hybrid Azure AD joined device
- Enable policy
Code Implementation
Code Pack: Terraform
# Conditional Access policy requiring compliant or Hybrid Azure AD joined
# devices for all privileged admin access to Microsoft admin portals
resource "azuread_conditional_access_policy" "require_compliant_device_admins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Require compliant device for admins"
state = "enabled"
conditions {
users {
included_roles = length(var.admin_role_ids) > 0 ? var.admin_role_ids : [
# Default: target common privileged roles
data.azuread_directory_role.global_admin.template_id,
]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
}
grant_controls {
operator = "OR"
built_in_controls = ["compliantDevice", "domainJoinedDevice"]
}
}
2.4 Block High-Risk Sign-Ins
Profile Level: L2 (Hardened)
| Framework | Control |
|---|---|
| CIS Controls | 6.4 |
| NIST 800-53 | SI-4 |
Description
Use Entra ID Protection to automatically block sign-ins classified as high risk based on machine learning detection of suspicious patterns.
Prerequisites
- Microsoft Entra ID P2 license
ClickOps Implementation
Step 1: Create Risk-Based Policy
- Navigate to: Protection → Conditional Access → Policies
- Click + New policy
- Configure:
- Name: Block high-risk sign-ins
- Users: All users (exclude emergency accounts)
- Cloud apps: All cloud apps
- Conditions → Sign-in risk: High
- Grant: Block access
- Enable policy
Step 2: Create Medium-Risk MFA Policy
- Create another policy for medium risk
- Conditions → Sign-in risk: Medium
- Grant: Require MFA + Require password change
Code Implementation
Code Pack: Terraform
# Conditional Access policy to block high-risk sign-ins using
# Entra ID Protection machine learning detection (requires P2 license)
resource "azuread_conditional_access_policy" "block_high_risk_signins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Block high-risk sign-ins"
state = var.high_risk_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
sign_in_risk_levels = ["high"]
}
grant_controls {
operator = "OR"
built_in_controls = ["block"]
}
}
# Conditional Access policy requiring MFA + password change for medium-risk sign-ins
resource "azuread_conditional_access_policy" "remediate_medium_risk_signins" {
count = var.profile_level >= 2 ? 1 : 0
display_name = "HTH: Require MFA for medium-risk sign-ins"
state = var.high_risk_policy_state
conditions {
users {
included_users = ["All"]
excluded_groups = [azuread_group.emergency_access.object_id]
}
applications {
included_applications = ["All"]
}
client_app_types = ["browser", "mobileAppsAndDesktopClients"]
sign_in_risk_levels = ["medium"]
}
grant_controls {
operator = "AND"
built_in_controls = ["mfa", "passwordChange"]
}
}
3. Privileged Identity Management
3.1 Enable Just-In-Time Access for Admin Roles
Profile Level: L2 (Hardened)
| Framework | Control |
|---|---|
| CIS Controls | 5.4, 6.8 |
| NIST 800-53 | AC-2(7), AC-6(1) |
| CIS Azure | 1.1.4 |
Description
Implement Privileged Identity Management (PIM) to eliminate standing admin privileges. Require just-in-time activation with MFA, justification, and optional approval for privileged role access.
Rationale
Why This Matters:
- Standing privileges create persistent attack surface
- Compromised accounts with permanent admin have unlimited access duration
- PIM provides audit trail for all privilege elevation
- Time-limited access reduces blast radius
Attack Prevented: Privilege persistence, lateral movement, insider threats
Real-World Incidents:
- Midnight Blizzard: Time-limited OAuth permissions would have reduced attack duration
Prerequisites
- Microsoft Entra ID P2 license
- Global Administrator or Privileged Role Administrator
ClickOps Implementation
Step 1: Access PIM
- Navigate to: Microsoft Entra admin center → Identity governance → Privileged Identity Management
- Click Microsoft Entra roles
Step 2: Configure Role Settings
- Click Settings → Roles
- Select Global Administrator
- Click Edit
- Configure:
- Activation maximum duration: 2 hours (or 8 hours max)
- On activation, require: MFA
- Require justification on activation: Yes
- Require ticket information: Optional
- Require approval to activate: Yes (for highest privilege roles)
- Approvers: Security team members
- Click Update
- Repeat for other privileged roles (Security Admin, Exchange Admin, etc.)
Step 3: Convert Permanent to Eligible
- Navigate to Assignments → Eligible assignments
- For each permanent Global Admin:
- Click Update
- Change assignment type to Eligible
- Set eligibility period (e.g., 1 year with renewal)
- Keep only emergency accounts as permanent
Step 4: Configure Activation Requirements
- In role settings, configure:
- Maximum activation duration
- MFA requirement
- Approval workflow
Time to Complete: ~1-2 hours
Code Implementation
Code Pack: Terraform
# Create PIM eligible assignments for Global Administrator role.
# Eliminates standing admin privileges by requiring just-in-time activation
# with MFA, justification, and optional approval.
#
# NOTE: Full PIM role settings (activation duration, approval workflow,
# MFA on activation) require Microsoft Graph API or the admin center.
# Terraform manages eligible assignments; configure role settings via
# the Entra admin center or PowerShell.
resource "azuread_directory_role_eligibility_schedule_request" "pim_global_admin" {
count = var.profile_level >= 2 ? length(var.pim_eligible_user_ids) : 0
role_definition_id = data.azuread_directory_role.global_admin.template_id
principal_id = var.pim_eligible_user_ids[count.index]
directory_scope_id = "/"
justification = "HTH: PIM eligible assignment for Just-In-Time access"
schedule_info {
expiration {
duration = "P${var.pim_eligibility_duration_days}D"
type = "afterDuration"
}
}
}
Code Pack: CLI Script
# Connect with PIM permissions
Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Directory", "RoleEligibilitySchedule.ReadWrite.Directory"
# Get role definitions
$globalAdminRole = Get-MgRoleManagementDirectoryRoleDefinition -Filter "displayName eq 'Global Administrator'"
# Create eligible assignment (convert permanent to eligible)
$params = @{
action = "adminAssign"
justification = "Converting to PIM eligible assignment"
roleDefinitionId = $globalAdminRole.Id
directoryScopeId = "/"
principalId = "USER_OBJECT_ID"
scheduleInfo = @{
startDateTime = (Get-Date).ToUniversalTime().ToString("o")
expiration = @{
type = "afterDuration"
duration = "P365D" # 1 year eligibility
}
}
}
New-MgRoleManagementDirectoryRoleEligibilityScheduleRequest -BodyParameter $params
# Configure role settings (requires beta endpoint)
# Use Microsoft Entra admin center for full settings configuration
Validation & Testing
How to verify the control is working:
- Verify no permanent Global Admin assignments (except emergency accounts)
- Test PIM activation as eligible admin
- Confirm MFA required on activation
- Verify justification is captured in audit log
- Check activation expires after configured duration
Expected result: Admins activate roles on-demand, access expires automatically
3.2 Configure Access Reviews
Profile Level: L2 (Hardened)
| Framework | Control |
|---|---|
| CIS Controls | 5.1, 5.3 |
| NIST 800-53 | AC-2(3) |
Description
Enable recurring access reviews for privileged roles and group memberships to ensure continued business need for access.
ClickOps Implementation
Step 1: Create Access Review
- Navigate to: Identity governance → Access reviews
- Click + New access review
- Configure:
- Review type: Teams + Groups or Azure AD roles
- Scope: Global Administrator (and other privileged roles)
- Reviewers: Manager or Self-review
- Recurrence: Monthly or Quarterly
- Upon completion: Remove access for denied users
- Start review
Code Implementation
4. Application Security
4.1 Restrict User Consent to Applications
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 2.5 |
| NIST 800-53 | AC-3, CM-7 |
| CIS Azure | 2.1 |
Description
Prevent users from granting OAuth consent to third-party applications. Require admin approval for all new application access requests.
Rationale
Why This Matters:
- OAuth consent phishing is a growing attack vector
- Users often grant excessive permissions without understanding risks
- Admin review ensures only vetted applications are authorized
Attack Prevented: OAuth consent phishing, malicious app installation
Real-World Incidents:
- Midnight Blizzard: Leveraged malicious OAuth applications with full_access_as_app to access mailboxes
ClickOps Implementation
Step 1: Disable User Consent
- Navigate to: Applications → Enterprise applications → Consent and permissions
- Click User consent settings
- Select Do not allow user consent
- Click Save
Step 2: Configure Admin Consent Workflow
- Click Admin consent settings
- Enable Users can request admin consent to apps they are unable to consent to
- Add reviewers (Security team members)
- Configure notification settings
- Click Save
Time to Complete: ~15 minutes
Code Implementation
Code Pack: CLI Script
# Disable user consent
$params = @{
defaultUserRolePermissions = @{
permissionGrantPoliciesAssigned = @()
}
}
Update-MgPolicyAuthorizationPolicy -BodyParameter $params
# Note: Configure admin consent workflow through admin center
4.2 Review and Restrict Application Permissions
Profile Level: L2 (Hardened)
| Framework | Control |
|---|---|
| CIS Controls | 2.6 |
| NIST 800-53 | AC-6 |
Description
Regularly audit enterprise applications for excessive permissions, especially high-risk permissions like Mail.ReadWrite, Directory.ReadWrite.All, and full_access_as_app.
ClickOps Implementation
Step 1: Audit Applications
- Navigate to: Applications → App registrations → All applications
- For each app, click API permissions
- Flag apps with dangerous permissions:
Mail.ReadWrite- Read/write all mailFiles.ReadWrite.All- Access all filesDirectory.ReadWrite.All- Modify directoryApplication.ReadWrite.All- Manage appsRoleManagement.ReadWrite.Directory- Manage roles
Step 2: Remove Unnecessary Permissions
- For flagged apps, review business justification
- Remove permissions not required for functionality
- Or delete unused applications entirely
Code Implementation
Code Pack: Terraform
# Data source to enumerate all service principals (enterprise applications)
# for permission auditing. Use this to identify apps with dangerous permissions
# such as Mail.ReadWrite, Directory.ReadWrite.All, full_access_as_app.
#
# NOTE: Terraform is not the ideal tool for ongoing permission auditing.
# This control provides data sources for initial discovery; recurring
# audits should use PowerShell scripts or SSPM tooling.
# Retrieve all service principals for audit
data "azuread_service_principals" "all_apps" {
count = var.profile_level >= 2 ? 1 : 0
return_all = true
}
# Identify high-risk Microsoft Graph permissions for audit output
locals {
high_risk_permissions = var.profile_level >= 2 ? [
"Mail.ReadWrite",
"Mail.ReadWrite.All",
"Files.ReadWrite.All",
"Directory.ReadWrite.All",
"Application.ReadWrite.All",
"RoleManagement.ReadWrite.Directory",
"full_access_as_app",
] : []
app_permission_audit = var.profile_level >= 2 ? {
status = "AUDIT_DATA_AVAILABLE"
total_service_principals = length(try(data.azuread_service_principals.all_apps[0].service_principals, []))
high_risk_permissions = local.high_risk_permissions
instructions = "Review each app's API permissions in Entra admin center: Applications > App registrations > [App] > API permissions"
remediation = "Remove unnecessary permissions or delete unused applications"
} : null
}
5. Monitoring & Detection
5.1 Enable Sign-In and Audit Logging
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 8.2 |
| NIST 800-53 | AU-2, AU-3, AU-6 |
Description
Enable and export Entra ID sign-in and audit logs for security monitoring, threat detection, and compliance.
ClickOps Implementation
Step 1: Configure Diagnostic Settings
- Navigate to: Monitoring → Diagnostic settings
- Click + Add diagnostic setting
- Configure:
- Name: Send to Log Analytics (or SIEM)
- Logs: SignInLogs, AuditLogs, NonInteractiveUserSignInLogs, ServicePrincipalSignInLogs
- Destination: Log Analytics workspace / Event Hub / Storage Account
- Click Save
Step 2: Create Alert Rules
- Navigate to: Monitoring → Alerts
- Create alerts for:
- Global Admin role assignment
- Conditional Access policy changes
- New OAuth app registration
- Risky sign-in detected
Code Implementation
Code Pack: Terraform
# Configure diagnostic settings to export Entra ID sign-in and audit logs.
#
# NOTE: Diagnostic settings for Entra ID require the Azure Monitor provider
# (azurerm), not the AzureAD provider. This control documents the configuration
# and provides a reference implementation. If you are also managing Azure
# resources, add the azurerm provider to providers.tf and uncomment below.
#
# Uncomment and configure when using the azurerm provider:
#
# resource "azurerm_monitor_aad_diagnostic_setting" "entra_id_logs" {
# name = "hth-entra-id-logging"
# log_analytics_workspace_id = var.log_analytics_workspace_id
#
# enabled_log {
# category = "SignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "AuditLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "NonInteractiveUserSignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "ServicePrincipalSignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "ManagedIdentitySignInLogs"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "RiskyUsers"
# retention_policy {
# enabled = true
# days = 90
# }
# }
#
# enabled_log {
# category = "UserRiskEvents"
# retention_policy {
# enabled = true
# days = 90
# }
# }
# }
locals {
logging_config = {
status = var.log_analytics_workspace_id != "" ? "REQUIRES_AZURERM_PROVIDER" : "NOT_CONFIGURED"
workspace_id = var.log_analytics_workspace_id
recommended_log_categories = [
"SignInLogs",
"AuditLogs",
"NonInteractiveUserSignInLogs",
"ServicePrincipalSignInLogs",
"ManagedIdentitySignInLogs",
"RiskyUsers",
"UserRiskEvents",
]
recommended_retention_days = 90
instructions = var.log_analytics_workspace_id != "" ? "Add azurerm provider and uncomment the diagnostic setting resource" : "Set log_analytics_workspace_id variable and add azurerm provider"
alert_rules = [
"Global Admin role assignment",
"Conditional Access policy changes",
"New OAuth app registration",
"Risky sign-in detected",
"Emergency account sign-in",
]
}
}
5.2 Monitor Identity Secure Score
Profile Level: L1 (Baseline)
| Framework | Control |
|---|---|
| CIS Controls | 4.1 |
| NIST 800-53 | CA-7 |
Description
Regularly review Identity Secure Score to track security posture and identify improvement opportunities.
ClickOps Implementation
- Navigate to: Protection → Identity Secure Score
- Review current score and recommendations
- Target score above 70%
- Implement high-impact recommendations:
- Enable MFA for all users
- Block legacy authentication
- Enable risk policies
- Use PIM for admin roles
5.3 Key Events to Monitor
| Event | Log Source | Detection Use Case |
|---|---|---|
Add member to role |
Audit | Privilege escalation |
Update conditional access policy |
Audit | Security control bypass |
Consent to application |
Audit | Malicious app installation |
User risk detected |
Sign-in | Account compromise |
Sign-in from anonymous IP |
Sign-in | Suspicious access |
Impossible travel |
Sign-in | Credential theft |
KQL Queries for Azure Sentinel
Code Pack: DB Query
// Privileged role assignments
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to role"
| where TargetResources[0].modifiedProperties[0].newValue contains "Global Administrator"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, TargetResources[0].userPrincipalName
// Conditional Access policy changes
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName has_any ("Add policy", "Update policy", "Delete policy")
| where TargetResources[0].type == "Policy"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, OperationName, TargetResources[0].displayName
// High-risk sign-ins
SigninLogs
| where TimeGenerated > ago(24h)
| where RiskLevelDuringSignIn == "high"
| project TimeGenerated, UserPrincipalName, IPAddress, Location, RiskDetail
6. Third-Party Integration Security
6.1 Integration Risk Assessment
| Risk Factor | Low | Medium | High |
|---|---|---|---|
| Data Access | Directory read-only | User profile + groups | Mail, files, directory write |
| OAuth Scopes | User.Read | User.ReadWrite, Group.Read | Mail.ReadWrite, Application.ReadWrite.All |
| Token Duration | Short-lived (1 hour) | Refresh tokens (90 days) | Long-lived service principal |
| Vendor Security | SOC 2 Type II + ISO | SOC 2 Type I | No certification |
6.2 Common Integrations
Obsidian Security
Data Access: Read (directory, sign-in logs, audit logs) Recommended Controls:
- ✅ Use dedicated service principal
- ✅ Grant minimum required Graph API permissions
- ✅ Monitor service principal sign-ins
- ✅ Review permissions quarterly
7. Compliance Quick Reference
SOC 2 Trust Services Criteria Mapping
| Control ID | Entra ID Control | Guide Section |
|---|---|---|
| CC6.1 | MFA for all users | 1.1 |
| CC6.1 | Block legacy auth | 2.1 |
| CC6.2 | Privileged Identity Management | 3.1 |
| CC6.3 | Application consent controls | 4.1 |
| CC7.2 | Audit logging | 5.1 |
NIST 800-53 Rev 5 Mapping
| Control | Entra ID Control | Guide Section |
|---|---|---|
| IA-2(1) | MFA enforcement | 1.1 |
| IA-2(6) | Phishing-resistant MFA | 1.1 |
| AC-2(7) | Privileged account management | 3.1 |
| AC-2(3) | Access reviews | 3.2 |
| AU-2 | Audit logging | 5.1 |
CIS Microsoft Azure Foundations Benchmark Mapping
| Recommendation | Entra ID Control | Guide Section |
|---|---|---|
| 1.1.1 | Ensure MFA is enabled | 1.1 |
| 1.1.2 | Block legacy authentication | 2.1 |
| 1.1.4 | Ensure PIM is used | 3.1 |
| 1.1.5 | Emergency access accounts | 1.2 |
| 2.1 | Restrict user consent | 4.1 |
Appendix A: License Compatibility
| Control | Free | P1 | P2 | Microsoft 365 E5 |
|---|---|---|---|---|
| Security Defaults | ✅ | ✅ | ✅ | ✅ |
| Conditional Access | ❌ | ✅ | ✅ | ✅ |
| Privileged Identity Management | ❌ | ❌ | ✅ | ✅ |
| Identity Protection (risk policies) | ❌ | ❌ | ✅ | ✅ |
| Access Reviews | ❌ | ❌ | ✅ | ✅ |
| Entitlement Management | ❌ | ❌ | ✅ | ✅ |
Appendix B: References
Official Microsoft Documentation:
- Microsoft Trust Center
- Microsoft Entra ID Product Documentation
- Best Practices to Secure with Microsoft Entra ID
- Require MFA for All Users with Conditional Access
- Plan Conditional Access Deployment
- Conditional Access - Zero Trust Policy Engine
- Privileged Identity Management
API Documentation:
- Microsoft Graph Identity and Network Access Overview
- Microsoft Graph API Reference
- Microsoft Graph PowerShell SDK
Compliance Frameworks:
- SOC 1, SOC 2, SOC 3, ISO 27001, ISO 27017, ISO 27018, ISO 27701, FedRAMP — via Microsoft Service Trust Portal
- Microsoft Entra Identity Standards Overview
Hardening Benchmarks:
Security Incidents:
- Midnight Blizzard Attack Guidance (January 2024) — Test account without MFA led to corporate email compromise via password spray
- CVE-2025-55241: Critical Entra ID privilege escalation vulnerability (CVSS 10.0) potentially impacting any tenant
Changelog
| Date | Version | Maturity | Changes | Author |
|---|---|---|---|---|
| 2025-02-05 | 0.1.0 | draft | Initial guide with authentication, Conditional Access, PIM, and monitoring | Claude Code (Opus 4.5) |
Contributing
Found an issue or want to improve this guide?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide