Entra ID, Conditional Access & PIM
Two identity planes, adaptive sign-in, just-in-time roles.
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).
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.
# 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.
# 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" }}}'
{"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.
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
CA001-Admins-PhishResistant-MFA enabledForReportingButNotEnforced
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.
# 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" }}}'
{"id": "b21f4d8c-8f0a-4a5e-93a2-1c77e2d40b19","status": "Provisioned","action": "adminAssign","roleDefinitionId": "62e90394-69f5-4237-9190-012177145e10","directoryScopeId": "/"}
# 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" }}}'
{"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.
# 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
Who Type Ends------------------------------------ --------- --------------------5f2b8c14-9d3a-4e77-b0a1-2c6f8e1d4a90 Activated 2026-07-22T18:30:00Z9a41d0e2-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.
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.
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.