CoursesAzure securityEntra ID, Conditional Access & PIM

Entra ID, Conditional Access & PIM

Two identity planes, adaptive sign-in, just-in-time roles.

Advanced35 min · lesson 1 of 15

A big company runs two separate front desks. One desk hires and fires. It issues badges, edits the org chart, and decides who exists as an employee at all. The other desk decides which rooms your badge opens once you are already inside. Confuse the two, or hand people master keys that never expire, and a single stolen badge turns into the whole building. Azure is built the same way. It keeps two separate identity systems (Azure calls them two 'planes'), and in the cloud the real perimeter is identity, not the network firewall. This lesson lives in the first system, Microsoft Entra ID (the current name for what used to be Azure Active Directory, or Azure AD). You will gate every sign-in with Conditional Access, and replace permanent admin power with power that switches on only when it is needed.

Two identity planes, one perimeter

The first plane is Entra directory roles. Global Administrator, Privileged Role Administrator, and Application Administrator run the tenant itself (your organization's own directory inside Entra): the users, the groups, the app registrations (the identities you hand to software and scripts), and the Conditional Access policies you are about to write. The second plane is Azure RBAC (Role-Based Access Control), the Owner, Contributor, and Reader roles that run Azure resources like virtual machines, storage accounts, and Key Vaults. Those are next lesson's problem. The thing to hold onto is that these are two different authorization systems with two different sets of assignments. A Global Administrator has no automatic power over your resource groups, and an Azure Owner has no automatic power over the directory. There is exactly one bridge between the two, and it is the one an attacker reaches for first (see the warning below).

The one bridge between the planes is a known escalation
A Global Administrator is not automatically an Azure resource admin, with a single exception. Any Global Admin can flip a tenant switch called 'Access management for Azure resources' in Entra properties and instantly gain User Access Administrator at the root scope '/', which sits above every subscription. From there they can grant themselves Owner anywhere. The toggle is recorded as a Microsoft.Authorization/elevateAccess/action entry in the Activity Log, so alert on it. After any legitimate use, remove the root-scope grant with: az role assignment delete --scope "/" --role "User Access Administrator".

Before you harden anything, count who holds the master keys today. You cannot protect a role if you do not know who is in it. Two quick reads with the Azure CLI (the az command) cover both planes.

terminal
# PLANE 1 - who ACTIVELY holds Global Administrator?
# (roleTemplateId is a fixed GUID, a globally unique identifier, the same in every tenant)
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/directoryRoles(roleTemplateId='62e90394-69f5-4237-9190-012177145e10')/members?\$select=userPrincipalName" \
--query "value[].userPrincipalName" -o tsv
# PLANE 2 - who Owns the subscription? (covered fully next lesson)
az role assignment list --role Owner --scope /subscriptions/$SUB \
--query "[].principalName" -o tsv

Three standing Global Admins and one subscription Owner. The breakglass-01 account is deliberate, and we come back to it at the end. The other two are the whole question this lesson answers: why are human beings holding the highest role in the tenant around the clock, and how do you take that standing power away without locking yourself out of your own building?

Conditional Access: the bouncer with a checklist

Conditional Access (CA) is a bouncer standing at the door of every sign-in, reading from a checklist. Each policy is one if/then rule. If these users reach these apps from this context (a device that is or is not managed, a named location, a level of sign-in risk, a client app), then require these things before you let them in: multi-factor authentication (MFA, proving who you are with a second factor like a security key), a compliant or company-joined device, or a specific authentication strength. Authentication strength is the grown-up replacement for the old 'require MFA' checkbox. The old checkbox accepted any second factor, including a text message or a phone tap, both of which a good phishing page steals in real time. The built-in Phishing-resistant MFA strength (a fixed, well-known ID) accepts only FIDO2 security keys (hardware keys you tap or plug in), Windows Hello for Business, or certificate-based sign-in, and turns SMS and push away. There is no dedicated az verb for Conditional Access. It lives in Microsoft Graph (the programming interface behind Entra), so you drive it with az rest, which means the whole policy is text you can review in Git.

One rule of rollout matters more than the rest: start in report-only. Set the policy state to enabledForReportingButNotEnforced, and CA evaluates the rule on every sign-in and writes down who it would have blocked, without blocking anyone. You read the sign-in logs, find the service account nobody remembered, fix it, and only then flip the state to enabled. Skip this step and you learn about that service account when production breaks on a Friday night. Two more facts keep you out of trouble. Across policies, Conditional Access is strict AND: every policy that matches a sign-in must be satisfied, so a second policy can block a user the first one allowed. And the license line matters. Conditional Access needs Entra ID P1 (the paid license tier that turns these policies on), while the risk-based signals, and the PIM you are about to use, need the higher Entra ID P2.

terminal
# Require phishing-resistant MFA for admins. state = report-only, so day one is safe.
# excludeUsers keeps your break-glass account OUT of this policy, always.
az rest --method POST \
--url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
--headers "Content-Type=application/json" \
--body '{
"displayName": "CA001-Admins-PhishResistant-MFA",
"state": "enabledForReportingButNotEnforced",
"conditions": {
"users": {
"includeRoles": ["62e90394-69f5-4237-9190-012177145e10"],
"excludeUsers": ["8be9f6a1-1c2d-4c2f-9e11-a7b3c0d5e6f2"]
},
"applications": { "includeApplications": ["All"] }
},
"grantControls": {
"operator": "OR",
"builtInControls": [],
"authenticationStrength": { "id": "00000000-0000-0000-0000-000000000004" }
}
}'
output
{
"id": "8f3c1d20-2b4e-4a17-9c30-6d1e5b7c7a10",
"displayName": "CA001-Admins-PhishResistant-MFA",
"state": "enabledForReportingButNotEnforced",
"createdDateTime": "2026-07-22T09:14:02Z"
}

Read it back before you trust it. The one field that matters right now is state. It must still say report-only until you have actually read the sign-in logs and confirmed nobody legitimate gets caught.

terminal
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies?\$select=displayName,state" \
--query "value[?starts_with(displayName,'CA001')].[displayName,state]" -o tsv
output
CA001-Admins-PhishResistant-MFA enabledForReportingButNotEnforced
A CA policy on 'All users' can lock out every admin, including you
Conditional Access is evaluated on the very next sign-in. A policy that requires, say, a compliant device for all users instantly blocks every account that does not have one, and admins are first out the door because they rarely sign in from managed laptops. Two safeguards are not optional. Always roll out in report-only (enabledForReportingButNotEnforced) and read the sign-in logs before you flip to enabled. And always exclude your break-glass accounts from every CA policy. Without an excluded emergency path, one bad policy leaves you with no way back into your own tenant.

PIM: eligible instead of standing

Standing privilege is a fire axe bolted to every wall, all day, every day. Privileged Identity Management (PIM) puts the axe behind glass. A normal role assignment is active: the person holds the permissions 24 hours a day, so a session stolen at 3 a.m. is a stolen Global Admin. PIM adds a second state called eligible. An eligible user is allowed to hold the role but carries zero permission until they break the glass and activate it. Activation is meant to cost something. It asks for a justification (usually a change-ticket number), a fresh MFA challenge, sometimes a second person's approval, and it grants the role for a bounded window like two hours, after which it switches itself off. Every activation lands in the Entra audit log, so you get a clean record of who used god-mode, when, and why. The target to aim for is zero permanent assignments on privileged roles. Make people eligible, and let them elevate only when they need it.

One subtlety is worth knowing before you run the commands. The two-hour window, and whether MFA or an approver is required, do not come from the activation request. They come from the role's PIM policy (the Expiration and Enablement rules attached to that role). The request below asks for two hours, but the role's policy is both the ceiling and the gatekeeper. If that policy says activation needs approval, your selfActivate call comes back as PendingApproval and no permission arrives until someone clicks approve. Set the policy once, and every future activation inherits it.

terminal
# 1) An admin makes a user ELIGIBLE for Global Admin (principalId = the user's object ID).
# 180 days of eligibility, ZERO standing access.
az rest --method POST \
--url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleRequests" \
--headers "Content-Type=application/json" \
--body '{
"action": "adminAssign",
"principalId": "'$USER_OID'",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
"directoryScopeId": "/",
"justification": "On-call platform engineer",
"scheduleInfo": {
"startDateTime": "2026-07-22T00:00:00Z",
"expiration": { "type": "afterDuration", "duration": "P180D" }
}
}'
output
{
"id": "b21f4d8c-8f0a-4a5e-93a2-1c77e2d40b19",
"status": "Provisioned",
"action": "adminAssign",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
"directoryScopeId": "/"
}
terminal
# 2) Later, that user ELEVATES themselves. MFA is re-challenged; 2-hour window.
# ticketInfo satisfies a "require ticket" rule if the role's PIM policy demands one.
az rest --method POST \
--url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests" \
--headers "Content-Type=application/json" \
--body '{
"action": "selfActivate",
"principalId": "'$USER_OID'",
"roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10",
"directoryScopeId": "/",
"justification": "CHG-4471: rotate break-glass credentials",
"ticketInfo": { "ticketNumber": "CHG-4471", "ticketSystem": "ServiceNow" },
"scheduleInfo": {
"startDateTime": "2026-07-22T16:30:00Z",
"expiration": { "type": "afterDuration", "duration": "PT2H" }
}
}'
output
{
"id": "7c9a1e55-3b26-4d71-8f4a-9e0b2c6d5f31",
"status": "Provisioned",
"action": "selfActivate",
"scheduleInfo": {
"startDateTime": "2026-07-22T16:30:00Z",
"expiration": { "type": "afterDuration", "duration": "PT2H" }
}
}

Status Provisioned means the two hours have started. Had the role's policy required an approver, that same call would have returned PendingApproval, and the user would sit there powerless until someone approved. Either way, you now have a paper trail. Which flips the real security question. Forget who could be admin. Who is permanently admin, right this second?

Prove it: standing versus activated

Graph answers that directly. The roleAssignmentScheduleInstances endpoint lists everyone who currently holds a role and tags each one with an assignmentType. Activated means the access came through a PIM activation and carries an endDateTime, so it expires on its own. Assigned with no end date is a permanent, standing grant, exactly the account you want to convert to eligible or delete. Run this after every access review, and treat any unexpected Assigned row on Global Administrator as an incident to investigate, not a chore for next sprint.

terminal
# Every current Global Admin, and HOW they hold the role:
az rest --method GET \
--url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances?\$filter=roleDefinitionId%20eq%20'62e90394-69f5-4237-9190-012177145e10'" \
--query "value[].{who:principalId, type:assignmentType, ends:endDateTime}" -o table
output
Who Type Ends
------------------------------------ --------- --------------------
5f2b8c14-9d3a-4e77-b0a1-2c6f8e1d4a90 Activated 2026-07-22T18:30:00Z
9a41d0e2-7b55-4c31-8f90-3e1a7d2c6b40 Assigned

Read the two rows like a defender. The first holder is fine: Activated, with a clock ticking down to 18:30, which is the two-hour window from the activation above. The second is the problem: Assigned, no end date, standing Global Admin around the clock. Unless that principal is a documented break-glass account, this is your next ticket. Convert it to eligible with the same adminAssign call you saw earlier, or remove it. Do not quietly tidy it up and move on. A standing Global Admin that nobody can explain is exactly what an attacker's persistence looks like from the inside.

How just-in-time admin actually flows
1Eligible
authorized, zero standing permission
2Activate request
justification + change-ticket number
3MFA + optional approval
fresh challenge, gated by the role policy
4Active for a bounded window
e.g. 2 hours, then switches off
5Audit log entry
who / when / why, streamed to Sentinel

Break-glass and shipping the evidence

Two accounts break every rule above on purpose. Break-glass accounts (also called emergency-access accounts) are the spare key in the lockbox by the front door. They are cloud-only (a name at your-tenant.onmicrosoft.com, never synced from an on-premises directory), they use a long random secret stored offline, they stay standing Global Admins, and they are excluded from every Conditional Access policy. Their entire job is to get you back in when a misconfigured CA policy or a PIM outage has locked out every normal human. Because a real break-glass sign-in should be rare, you alert loudly on every single one. For everyone else the checklist is short: keep the count of permanent Global Admins tiny (Microsoft's own guidance is fewer than five), require a phishing-resistant authentication strength, make privileged roles eligible-only, require approval on the most sensitive activations, and ship the Entra audit log and sign-in log to Microsoft Sentinel (Azure's SIEM, or Security Information and Event Management system, the tool that collects security logs and raises alerts).

That last step is what turns configuration into detection. Every PIM activation, every CA block, every break-glass sign-in is already written to the Entra logs. Streamed into Sentinel, they become alerts a human actually sees: a Global Admin activated outside a change window, a phishing-resistant policy that suddenly denies a real admin, a break-glass account waking up at 2 a.m. Left in Entra alone, they are history nobody reads. Next lesson crosses into the second plane, Azure RBAC, its scope hierarchy, and the deny assignments that overrule even an Owner.

Quick check
01A user is made eligible for Global Administrator in PIM but has not activated the role. What can they do right now, before activating?
Incorrect — That confuses eligibility with an active, time-bound session; eligible on its own grants nothing.
Correct — eligible means authorized but holding zero permission until an audited activation.
Incorrect — That is the Assigned state, which is precisely what PIM is designed to remove.
Incorrect — There is no read-only shadow of the role; an un-activated role grants no permission at all.
02You create a Conditional Access policy with state enabledForReportingButNotEnforced. A risky admin sign-in matches it and would fail its phishing-resistant MFA requirement. What happens to that sign-in?
Incorrect — Report-only never blocks; refusing to enforce is the entire point of the state.
Incorrect — Report-only does not apply grant controls; it only records what the outcome would have been.
Correct — report-only evaluates and logs the outcome without enforcing it.
Incorrect — The state applies uniformly; in report-only it enforces on nobody, admin or not.
03A post-review check on Global Administrator returns two rows: one 'Activated' with an Ends timestamp, and one 'Assigned' with an empty Ends field, for a principal that is not on your break-glass list. What does the second row mean, and what do you do?
Correct — Assigned with no end date is standing privilege, the exact exposure to eliminate.
Incorrect — Activations show type Activated with an end time; this row has neither.
Incorrect — The scenario states this principal is not on the break-glass list, so that exception does not apply.
Incorrect — This endpoint reports role assignments, not CA, and the row reflects real standing access.

Try this

Work through “Break-glass and shipping the evidence” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: the one bridge between the planes is a known escalation. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related