# Azure Cloud Architecture — Part 2: Identity & Access

> **Series:** Azure Cloud Architecture (2 of 16)
> **Part 1:** `01-fundamentals-and-governance.md` — Fundamentals & Governance
> **Part 2:** This file — Identity & Access
> **Part 3:** `03-compute-vms-and-scale-sets.md` — Compute: Virtual Machines & Scale Sets
> **Part 4:** `04-networking-foundations-vnets-ip-and-dns.md` — Networking Foundations: VNets, IP & DNS
> **Part 5:** `05-networking-hybrid-connectivity.md` — Networking: Hybrid Connectivity
> **Part 6:** `06-networking-application-delivery.md` — Networking: Application Delivery
> **Part 7:** `07-networking-private-access-and-security.md` — Networking: Private Access & Security
> **Part 8:** `08-storage-blob-files-and-disks.md` — Storage: Blob, Files & Disks
> **Part 9:** `09-databases-and-data-services.md` — Databases & Data Services
> **Part 10:** `10-containers-and-serverless.md` — Containers & Serverless
> **Part 11:** `11-application-architecture-and-messaging.md` — Application Architecture & Messaging
> **Part 12:** `12-security-and-compliance.md` — Security & Compliance
> **Part 13:** `13-monitoring-logging-and-observability.md` — Monitoring, Logging & Observability
> **Part 14:** `14-business-continuity-backup-dr-and-migration.md` — Business Continuity: Backup, DR & Migration
> **Part 15:** `15-cicd-and-iac.md` — CI/CD & Infrastructure as Code
> **Part 16:** `16-multi-region-cost-optimization-and-cheatsheet.md` — Multi-Region, Cost Optimization & Cheat Sheet
> **Questions:** `questions.md`

Assumes you're comfortable with Part 1's tenant/management-group/subscription hierarchy and the RBAC-vs-Policy distinction — this chapter goes deep on the identity platform that hierarchy sits on top of.

## Table of Contents

1. [Why Identity Sits at Azure's Root](#why-identity-sits-at-azures-root)
2. [Microsoft Entra ID vs. Windows Server Active Directory — Not the Same Thing](#microsoft-entra-id-vs-windows-server-active-directory--not-the-same-thing)
3. [Users and Groups — Creating and Managing Identities](#users-and-groups--creating-and-managing-identities)
4. [External Identities — Bringing Partners and Guests In Safely](#external-identities--bringing-partners-and-guests-in-safely)
5. [Cross-Tenant Access Settings — Governing Collaboration in Both Directions](#cross-tenant-access-settings--governing-collaboration-in-both-directions)
6. [Licensing Users in Entra ID](#licensing-users-in-entra-id)
7. [Self-Service Password Reset (SSPR)](#self-service-password-reset-sspr)
8. [Authentication Methods and Multi-Factor Authentication](#authentication-methods-and-multi-factor-authentication)
9. [Conditional Access — Policy-Driven Access Control](#conditional-access--policy-driven-access-control)
10. [Microsoft Entra ID Protection — Risk-Based Access](#microsoft-entra-id-protection--risk-based-access)
11. [Privileged Identity Management (PIM) — Just-in-Time Elevation](#privileged-identity-management-pim--just-in-time-elevation)
12. [Microsoft Entra Roles vs. Azure RBAC Roles — Two Separate Systems](#microsoft-entra-roles-vs-azure-rbac-roles--two-separate-systems)
13. [Azure RBAC Deep Dive — Role Definitions, Assignments & Scope](#azure-rbac-deep-dive--role-definitions-assignments--scope)
14. [Custom Roles and Deny Assignments](#custom-roles-and-deny-assignments)
15. [App Registrations vs. Enterprise Applications](#app-registrations-vs-enterprise-applications)
16. [Service Principals — Identities for Automation](#service-principals--identities-for-automation)
17. [Managed Identities — System-Assigned vs. User-Assigned](#managed-identities--system-assigned-vs-user-assigned)
18. [Workload Identity Federation — Secretless Authentication From Outside Azure](#workload-identity-federation--secretless-authentication-from-outside-azure)
19. [Access Reviews and Identity Governance](#access-reviews-and-identity-governance)
20. [Administrative Units — Delegating Identity Administration Without Full Directory Access](#administrative-units--delegating-identity-administration-without-full-directory-access)
21. [Hybrid Identity — Microsoft Entra Connect and Hybrid Join](#hybrid-identity--microsoft-entra-connect-and-hybrid-join)
22. [A Full Worked Identity Bootstrap for Meridian Freight](#a-full-worked-identity-bootstrap-for-meridian-freight)
23. [Part 2 CLI Cheat Sheet](#part-2-cli-cheat-sheet)
24. [Common Mistakes and Interview Traps](#common-mistakes-and-interview-traps)
25. [Worked Practice Problems](#worked-practice-problems)
26. [Summary and What's Next](#summary-and-whats-next)

## Why Identity Sits at Azure's Root

Part 1 established that a Microsoft Entra **tenant** sits above every management group and subscription in Azure's hierarchy — this chapter is about that tenant's own internal machinery: **Microsoft Entra ID** (the product formerly, and still commonly, called Azure Active Directory). Every RBAC role assignment made anywhere in Azure ultimately binds to an identity object that lives here — a user, a group, a service principal, or a managed identity — which makes this chapter's content load-bearing for essentially everything else in this series, not just a standalone topic.

```mermaid
graph TD
    Tenant["Microsoft Entra Tenant"] --> Users["Users<br/>(humans)"]
    Tenant --> Groups["Groups<br/>(collections of users/apps)"]
    Tenant --> Apps["App Registrations<br/>(application identity definitions)"]
    Tenant --> SP["Service Principals<br/>(an app's usable identity<br/>in THIS tenant)"]
    Tenant --> MI["Managed Identities<br/>(Azure-managed service principals)"]
    Users --> RBAC["Bound to RBAC role<br/>assignments across every<br/>subscription/resource"]
    Groups --> RBAC
    SP --> RBAC
    MI --> RBAC
```

For Meridian Freight, this chapter is where the company's engineers, the `shipment-api`'s own runtime identity, and the CI/CD pipeline that deploys it (Part 15) all get modeled as first-class identity objects — every access decision made anywhere else in this series traces back to one of them.

---

## Microsoft Entra ID vs. Windows Server Active Directory — Not the Same Thing

A genuinely common point of confusion, worth resolving precisely and early: **despite the shared branding history, Microsoft Entra ID is not "Active Directory in the cloud."** They are architecturally distinct systems solving related but different problems.

| | Windows Server Active Directory (AD DS) | Microsoft Entra ID |
|---|---|---|
| Protocol model | LDAP, Kerberos, NTLM | OAuth 2.0, OpenID Connect, SAML — REST/HTTP-based |
| Structure | Hierarchical Organizational Units (OUs), Group Policy Objects | Flat directory of users/groups, Conditional Access policies |
| Typical use | Domain-joining on-premises Windows machines, legacy line-of-business auth | Cloud/SaaS app sign-in, Azure resource RBAC, modern MFA |
| Where it runs | Self-hosted domain controllers (on-premises or IaaS VMs) | A fully managed Microsoft service — no domain controllers to patch |

**Why this distinction is worth stating precisely rather than glossing over, a genuinely common interview trap: an organization can run BOTH simultaneously, connected by Microsoft Entra Connect (covered later in this chapter) — on-premises AD DS remains the source of truth for domain-joined Windows machines and legacy Kerberos-based applications, while Entra ID separately handles cloud app sign-in and Azure RBAC, synchronized from AD DS rather than replacing it outright.** A design that assumes migrating to Azure means AD DS simply "becomes" Entra ID misses this — many enterprises run both in parallel for years, which is exactly Meridian Freight's own situation with its legacy on-premises freight-routing servers introduced in Part 1.

---

## Users and Groups — Creating and Managing Identities

The basic building blocks: a **user** object represents one human identity in the tenant; a **group** is a collection of users (and, for security groups, other objects) used to assign access once instead of per-person.

```bash
# Create a user directly in Entra ID (a "cloud-only" identity,
# as opposed to one synced from on-premises AD DS)
az ad user create \
  --display-name "Priya Shah" \
  --user-principal-name "priya.shah@meridianfreight.onmicrosoft.com" \
  --password "<temporary-password>" \
  --force-change-password-next-sign-in true

# Create a security group and add a member
az ad group create --display-name "Platform Engineers" --mail-nickname "platform-engineers"
az ad group member add --group "Platform Engineers" --member-id "<user-object-id>"
```

| Group type | Purpose |
|---|---|
| Security group | Grants access to resources — RBAC assignments, Conditional Access policies, and application access all target these |
| Microsoft 365 group | Collaboration-focused (shared mailbox, calendar, SharePoint site) — not primarily an access-control mechanism |
| Dynamic membership group | Membership computed automatically from a rule against user/device attributes (e.g., `department eq "Platform"`) rather than manually maintained |

**Assigning RBAC roles (Part 1) to groups rather than individual users, worth stating as a hard best practice, not a stylistic preference: a group-based assignment survives personnel changes without touching Azure's RBAC configuration at all** — onboarding or offboarding an engineer becomes a group-membership change in Entra ID, not an RBAC review across every subscription and resource group they might have touched.

---

## External Identities — Bringing Partners and Guests In Safely

Meridian Freight doesn't operate in isolation — it works with independent trucking carriers who need limited access to the `driver-portal`, and occasionally brings in outside consultants. **Microsoft Entra External ID** (the current name for what was historically "Azure AD B2B collaboration") handles exactly this: inviting an identity from another organization's own Entra tenant (or even a personal Microsoft/Google account) as a **guest** in Meridian Freight's tenant, without creating and managing a separate password for them.

```bash
# Invite an external partner as a guest user
az ad user invite \
  --invited-user-email-address "ops@carrierpartner.example.com" \
  --invite-redirect-url "https://portal.azure.com" \
  --display-name "Carrier Partner Ops"
```

**Why this matters concretely, worth stating explicitly: a guest user authenticates with THEIR home organization's own credentials and MFA — Meridian Freight never stores or resets a password for them at all**, and revoking access is as simple as removing the guest object or their group membership, without coordinating a credential change with an organization outside Meridian's control. Guest access should still be scoped narrowly (typically via Conditional Access policies specifically targeting guest accounts, covered later in this chapter) rather than granted the same default access a full-time employee gets.

### Granting Guests Real Azure RBAC Access

A guest object created through External Identities is a genuine object in Meridian Freight's own tenant directory — which means it can be assigned an Azure RBAC role exactly like any other user, once invited and accepted.

```bash
# Grant an accepted guest scoped, narrow access — e.g. Reader on
# one specific resource group, not broad Contributor access
az role assignment create \
  --assignee "ops_carrierpartner.example.com#EXT#@meridianfreight.onmicrosoft.com" \
  --role "Reader" \
  --scope "/subscriptions/<sub-id>/resourceGroups/rg-driver-portal-prod"
```

**Worth stating as a deliberate default, not an oversight: a guest's RBAC assignment should almost always be narrower than the equivalent full-time employee's** — `Reader` rather than `Contributor`, scoped to one resource group rather than a subscription — since a guest's account security posture (MFA strength, device compliance) is governed by their HOME tenant's policies, which Meridian Freight cannot fully verify or enforce directly. Combining a narrow RBAC grant with the Conditional Access policies covered later in this chapter (targeting the guest population specifically) is the correct layered approach, rather than relying on either control alone.

---

## Cross-Tenant Access Settings — Governing Collaboration in Both Directions

External Identities covers *inviting* guests in; **cross-tenant access settings** govern the relationship at a policy level, in both directions — both how Meridian Freight treats guests coming from a specific partner tenant, and how Meridian Freight's own users are allowed to collaborate as guests in someone else's tenant.

```mermaid
graph TD
    Meridian["Meridian Freight tenant"] -->|"Inbound settings:<br/>trust this partner's MFA?<br/>which groups can be invited?"| Partner["Carrier Partner tenant"]
    Meridian -->|"Outbound settings:<br/>can OUR users accept<br/>invitations to external tenants?"| OtherOrg["Any external tenant"]
```

```bash
# Cross-tenant access settings are managed via Microsoft Graph;
# view the default inbound/outbound policy for the tenant
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/default"
```

**Two settings worth calling out specifically, since they're genuinely easy to get wrong in either direction: "trust multi-factor authentication from this specific partner tenant" avoids forcing a guest to re-register MFA in Meridian Freight's tenant when their home tenant already enforces it just as strongly — but only when that trust is deliberately configured for a specific, vetted partner, not left as a blanket default.** Outbound settings deserve equal attention: without an explicit restriction, an employee could accept a guest invitation into an entirely unrelated, unvetted external tenant, which is exactly the kind of shadow-IT-adjacent risk a security-conscious organization should scope deliberately rather than leave wide open by default.

---

## Licensing Users in Entra ID

Many Entra ID capabilities used throughout this chapter — Conditional Access, PIM, Identity Protection's risk-based policies — require a **Microsoft Entra ID P1 or P2** license assigned to the users those features apply to; they are not available on the free tier that comes bundled with an Azure subscription.

```bash
# List available license SKUs in the tenant
az ad sp list --filter "servicePrincipalType eq 'Application'" --query "[?displayName=='Microsoft Entra ID P2']"

# Assign a license to a user (via Microsoft Graph, since license
# assignment isn't exposed directly through az ad user commands)
az rest --method post \
  --uri "https://graph.microsoft.com/v1.0/users/priya.shah@meridianfreight.onmicrosoft.com/assignLicense" \
  --body '{"addLicenses": [{"skuId": "<p2-sku-id>"}], "removeLicenses": []}'
```

| Tier | Unlocks |
|---|---|
| Free (bundled) | Basic user/group management, SSO to a limited number of apps |
| Microsoft Entra ID P1 | Conditional Access, dynamic groups, Entra Connect hybrid features, SSPR with on-premises writeback |
| Microsoft Entra ID P2 | Everything in P1, plus Privileged Identity Management and Identity Protection's risk-based Conditional Access |

A common early design mistake worth flagging here specifically because it recurs later in this chapter: planning to use PIM or Identity Protection without confirming the relevant users already hold a P2 license leads to a late, expensive surprise during rollout — checking licensing coverage is a prerequisite step, not an afterthought.

---

## Self-Service Password Reset (SSPR)

A small but genuinely high-leverage feature worth covering on its own: **SSPR** lets a user reset their own forgotten password through a registered set of verification methods (an authenticator app, a phone number, security questions), without opening a help-desk ticket.

```bash
# SSPR policy is configured tenant-wide via the portal or Microsoft Graph;
# check current registration status for a user's authentication methods
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/users/priya.shah@meridianfreight.onmicrosoft.com/authentication/methods"
```

**Why this is worth calling out as a real operational win, not just a convenience feature: password-reset tickets are consistently one of the highest-volume categories of help-desk load in any organization of meaningful size**, and SSPR removes essentially all of it for users who've completed method registration — directly reducing toil, the same category of unnecessary manual work the SRE Fundamentals series' Toil discussion flags as a prime automation target.

---

## Authentication Methods and Multi-Factor Authentication

Beyond passwords, Entra ID supports a range of authentication methods, with a clear current best-practice ranking worth internalizing rather than treating every method as equally strong.

```mermaid
graph TD
    Weakest["Password alone<br/>(weakest — phishable,<br/>reusable if leaked)"] --> SMS["SMS/voice call MFA<br/>(better, but vulnerable<br/>to SIM-swap attacks)"]
    SMS --> Authenticator["Microsoft Authenticator<br/>push/OTP<br/>(strong, widely deployed)"]
    Authenticator --> Passwordless["Passwordless: Windows Hello,<br/>FIDO2 security keys,<br/>Authenticator passwordless sign-in<br/>(STRONGEST — phishing-resistant)"]
```

| Method | Phishing-resistant? | Typical fit |
|---|---|---|
| Password only | No | Never sufficient alone for any meaningful access |
| SMS/voice MFA | No — vulnerable to SIM-swap and MFA-fatigue attacks | Acceptable fallback, not a primary control |
| Microsoft Authenticator (push/OTP) | Partial — still vulnerable to MFA-fatigue prompt-bombing | Solid default for most users |
| FIDO2 security key / Windows Hello for Business | Yes | Recommended for privileged accounts and any PIM-eligible role |

> **From the Trenches:** An organization relying solely on Microsoft Authenticator push notifications for MFA experienced a real MFA-fatigue attack: an attacker with a stolen password repeatedly triggered push prompts late at night until a tired user approved one by reflex, granting the attacker a valid session. The fix wasn't abandoning Authenticator — it was enabling **number matching** (the user must enter a number shown on the sign-in screen into the Authenticator app, not just tap "Approve") and requiring phishing-resistant methods specifically for any account eligible for privileged role activation via PIM, covered later in this chapter.

---

## Conditional Access — Policy-Driven Access Control

**Conditional Access** is Entra ID's if-this-then-that access engine: a policy evaluates signals about a sign-in attempt (user, group, application, device state, network location, sign-in risk) and applies a control (block, require MFA, require a compliant device) before the sign-in completes.

```mermaid
sequenceDiagram
    participant User
    participant Entra as Microsoft Entra ID
    participant CA as Conditional Access Engine
    participant App as Target application
    User->>Entra: Sign-in attempt
    Entra->>CA: Evaluate applicable policies
    CA->>CA: Check user, app, device, location, risk signals
    alt Policy conditions met
        CA-->>Entra: Require MFA + compliant device
        Entra-->>User: Prompt for MFA, verify device compliance
        User->>Entra: Satisfies requirements
        Entra-->>App: Grant access
    else High-risk sign-in detected
        CA-->>Entra: Block
        Entra-->>User: Access denied
    end
```

```bash
# Conditional Access policies are managed via Microsoft Graph or the portal;
# list existing policies
az rest --method get --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies"
```

| Signal evaluated | Example condition |
|---|---|
| User/group | "Applies to the Platform Engineers group" |
| Application | "Applies to the Azure Management API, not every app" |
| Device state | "Require the device be marked compliant by Intune" |
| Network location | "Block sign-in from outside named trusted IP ranges" |
| Sign-in risk (requires Identity Protection, next section) | "Require MFA when sign-in risk is Medium or higher" |

**A hard operational rule worth stating with real force, since getting this wrong has locked entire organizations out of their own tenant: always configure at least one "break-glass" emergency access account, excluded from every Conditional Access policy, with a very strong (ideally FIDO2-based) credential stored securely offline.** A Conditional Access policy misconfiguration that accidentally blocks every administrator — a real, documented failure mode across many organizations — is unrecoverable without a break-glass account that policies were never allowed to touch in the first place.

### Authentication Context — Requiring Stronger Verification for Specific Actions

A more granular Conditional Access capability worth knowing: **authentication context** lets a policy require re-authentication with a *specific* method for a *specific* sensitive action — not just at initial sign-in. This is exactly what makes it possible to require a phishing-resistant method specifically at the moment of PIM role activation (next section), even for a user who already has an otherwise-valid, less-strong session.

### Named Locations and Report-Only Mode

Two smaller Conditional Access mechanics worth knowing precisely, since both prevent real, common mistakes. **Named locations** let a policy reference a human-readable label ("Corporate HQ," "Approved VPN Egress") backed by a defined IP range, instead of hardcoding raw CIDR blocks into every policy — a maintenance win when an office's public IP range changes, since it's updated once in the named location rather than hunted down across every policy that references it.

```bash
az rest --method post \
  --uri "https://graph.microsoft.com/v1.0/identity/conditionalAccess/namedLocations" \
  --body '{"@odata.type": "#microsoft.graph.ipNamedLocation", "displayName": "Corporate HQ", "isTrusted": true, "ipRanges": [{"@odata.type": "#microsoft.graph.iPv4CidrRange", "cidrAddress": "203.0.113.0/24"}]}'
```

**Report-only mode**, already referenced earlier in this chapter's break-glass discussion, is the single most important safety mechanism for rolling out a NEW Conditional Access policy: it evaluates every sign-in against the policy and logs what WOULD have happened, without actually blocking or challenging anyone. Every new policy this chapter recommends — device-compliance requirements, phishing-resistant MFA for privileged roles — should spend real time in report-only mode against production sign-in traffic before being switched to enforced, precisely because staging-tenant testing alone cannot fully predict how a policy behaves against an organization's actual, messier population of real devices and sign-in patterns.

### Reading a Sign-In Log When a User Reports Being Blocked

The **Sign-in logs** blade (also queryable via Microsoft Graph, and exportable into Log Analytics per Part 13) records exactly which Conditional Access policies applied to any given sign-in and why each one succeeded or failed — the correct first stop for any "I can't log in" report, before guessing at a cause.

```bash
# Query recent sign-in logs for a specific user, including which
# Conditional Access policies were evaluated and their individual result
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=userPrincipalName eq 'priya.shah@meridianfreight.onmicrosoft.com'&\$top=5"
```

The response's `appliedConditionalAccessPolicies` array shows every policy considered for that sign-in and its individual outcome (`success`, `failure`, `notApplied`) — the fastest way to identify which SPECIFIC policy blocked a user, rather than treating "Conditional Access" as one opaque gate when several policies may have been evaluated simultaneously.

---

## Microsoft Entra ID Protection — Risk-Based Access

**Identity Protection** (a P2 feature) continuously evaluates sign-in and user risk using Microsoft's own threat-intelligence signals — impossible travel, leaked credentials found in a breach dataset, sign-ins from anonymized IP addresses — and feeds that risk score directly into Conditional Access as a condition.

```mermaid
graph TD
    Signals["Risk signals: leaked credentials,<br/>impossible travel, anonymous IP,<br/>unfamiliar sign-in properties"] --> Score["Sign-in risk score:<br/>Low / Medium / High"]
    Score --> CA["Fed into Conditional Access<br/>as a policy condition"]
    CA --> Action["e.g. Require MFA at Medium risk,<br/>Block entirely at High risk"]
```

**Why feeding risk into Conditional Access, rather than treating risk detection as a standalone alert an analyst reviews later, matters concretely: it converts detection into real-time prevention** — a High-risk sign-in gets blocked or challenged the moment it happens, rather than surfacing as a SIEM alert (Part 12/13) an analyst investigates hours later, by which point a genuine compromise may have already caused damage.

---

## Privileged Identity Management (PIM) — Just-in-Time Elevation

A genuinely important operational security pattern, worth understanding precisely: **PIM lets a role assignment be "eligible" rather than permanently "active"** — a user can be eligible for the Owner role on a subscription without holding it standing, activating it only when actually needed, for a limited time window, optionally requiring approval and MFA.

```bash
# List roles a user is currently ELIGIBLE for (not necessarily active)
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilityScheduleInstances?\$filter=principalId eq '<user-object-id>'"

# A user activates an eligible role for a limited window (typically
# done via the portal's PIM blade, or the Microsoft Graph API directly)
```

| Assignment type | What it means | Risk if compromised |
|---|---|---|
| Permanent active | The role is granted and usable at all times, no activation step | Full, standing exposure — a compromised credential has immediate, unlimited access to that role |
| Eligible (PIM) | The role is dormant until explicitly activated, time-boxed, optionally requiring approval/MFA | An attacker with just the password still can't use the role without also passing activation's MFA/approval gate |

**Why "eligible, not permanent" is worth treating as the default posture for every genuinely privileged role — Subscription Owner, Global Administrator, User Access Administrator — worth stating with real force: a standing, permanently active Global Administrator assignment is one of the single highest-value targets in the entire tenant for an attacker, and PIM's time-boxing plus MFA-on-activation directly shrinks that exposure window from "always" to "only while actively in use, and provably re-authenticated at that moment."** Combined with Conditional Access's authentication context (previous section), a 2026-current PIM deployment can require phishing-resistant reauthentication specifically at the moment of activation — even for a user with an otherwise valid, already-MFA'd session.

> **From the Trenches:** An organization's audit revealed twelve permanent, standing Global Administrator assignments accumulated over several years — mostly former incident responders and consultants whose emergency access was never revoked once the standing assignment stopped being actively questioned. Converting all twelve to PIM-eligible assignments with a 4-hour maximum activation window and mandatory justification/approval didn't remove anyone's ability to do their job during a genuine incident — it just meant the role was dormant, and provably logged, the other 99% of the time it wasn't actively needed.

### PIM for Groups — Extending Just-in-Time Elevation Beyond Directory Roles

A newer PIM capability worth knowing: **PIM for Groups** applies the exact same eligible/time-boxed/approval-gated activation model to membership or ownership of a specific security group — not just to Entra or Azure RBAC roles directly. This matters because a security group is frequently what actually carries an RBAC assignment (this chapter's own earlier recommendation): making membership in that group PIM-eligible, rather than making a dozen individual role assignments eligible one at a time, lets one PIM configuration govern access consistently for everyone who is ever added to that group in the future.

```bash
# PIM for Groups is configured via Microsoft Graph's
# privilegedAccessGroupEligibilitySchedule endpoints — check
# current eligible group memberships for a user
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/identityGovernance/privilegedAccess/group/eligibilitySchedules?\$filter=principalId eq '<user-object-id>'"
```

For Meridian Freight, this is the cleaner long-term design for the `Platform Engineers` group introduced earlier in this chapter: rather than granting that group's members standing `Contributor` access, making membership in the group ITSELF PIM-eligible means every future platform engineer automatically inherits the same just-in-time elevation behavior, with no per-person PIM configuration required as the team grows.

### Auditing PIM Activations After the Fact

Every PIM activation is logged, and reviewing that log periodically is worth treating as a standard, recurring platform-team task rather than something only consulted reactively during an incident.

```bash
# Review every role activation in the last 30 days — who activated
# what, when, for how long, and with what justification
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances?\$filter=activatedUsing/id ne null"

# Cross-reference against the access review schedule (introduced
# later in this chapter) to confirm dormant eligible roles that are
# never actually activated get questioned, not just active ones
```

A role that is eligible but genuinely never activated over a long period is itself worth flagging during the next access review — it's either evidence the assignment is no longer needed at all, or evidence a workflow depends on it in a way nobody has documented, both of which are worth resolving deliberately rather than leaving the eligible assignment sitting indefinitely.

---

## Microsoft Entra Roles vs. Azure RBAC Roles — Two Separate Systems

A genuinely important, frequently misunderstood distinction, worth stating as precisely as this chapter has stated every other one: **Microsoft Entra roles and Azure RBAC roles are two entirely separate permission systems, with separate role definitions, separate assignment mechanisms, and no automatic overlap between them.**

```mermaid
graph TD
    EntraRoles["Microsoft Entra roles<br/>(e.g. Global Administrator,<br/>User Administrator)"] --> EntraScope["Govern the DIRECTORY:<br/>users, groups, app registrations,<br/>tenant-wide identity settings"]
    AzureRBAC["Azure RBAC roles<br/>(e.g. Owner, Contributor)"] --> AzureScope["Govern AZURE RESOURCES:<br/>VMs, storage accounts,<br/>networking, databases"]
    EntraRoles -.->|"NOT automatic —<br/>requires explicit elevation"| AzureScope
```

**The single fact worth memorizing here, a genuinely strong interview line: being a Global Administrator grants NO access to Azure resources by default** — a Global Administrator cannot read, create, or delete a single VM or storage account purely by virtue of that role, since Entra roles and Azure RBAC roles are permission systems with completely different role definitions that don't share custom-role permissions with each other either.

```bash
# A Global Administrator can EXPLICITLY elevate their own access to
# gain the Azure RBAC "User Access Administrator" role at the tenant root —
# a deliberate, auditable, one-time action, not an automatic grant
az rest --method post \
  --uri "https://management.azure.com/providers/Microsoft.Authorization/elevateAccess?api-version=2016-07-01"
```

| | Microsoft Entra roles | Azure RBAC roles |
|---|---|---|
| Governs | The directory itself: users, groups, app registrations, tenant settings | Azure resources: VMs, storage, networking, databases |
| Example roles | Global Administrator, User Administrator, Application Administrator | Owner, Contributor, Reader, Key Vault Secrets User |
| Assigned via | Entra ID's own role assignment blade / Microsoft Graph | `az role assignment create`, scoped to a management group/subscription/resource group/resource |
| Can a custom role mix both? | No — a custom Entra role and a custom Azure role are defined and stored entirely separately | No — same answer, the other direction |

**Why this matters concretely for Meridian Freight's own access design: the platform engineer managing Entra ID's guest-user policy and Conditional Access rules does NOT automatically gain the ability to touch the `shipment-api` production resource group, and vice versa — a person or team needing both identity-administration and resource-administration duties needs BOTH an Entra role assignment AND a separate Azure RBAC assignment, deliberately, rather than assuming one implies the other.**

```bash
# List a user's current Microsoft Entra role assignments —
# a SEPARATE query from az role assignment list, which only
# shows Azure RBAC assignments
az rest --method get \
  --uri "https://graph.microsoft.com/v1.0/users/priya.shah@meridianfreight.onmicrosoft.com/memberOf/microsoft.graph.directoryRole"
```

---

## Azure RBAC Deep Dive — Role Definitions, Assignments & Scope

Part 1 introduced Azure RBAC's basic shape; this section covers the mechanics precisely enough to reason about a real, multi-layered assignment.

```mermaid
graph TD
    RoleDef["Role Definition<br/>(e.g. 'Contributor') —<br/>a named set of Actions/NotActions"] --> Assignment["Role Assignment —<br/>binds a Principal to this<br/>Role Definition at a Scope"]
    Assignment --> Principal["Security Principal:<br/>user, group, service principal,<br/>or managed identity"]
    Assignment --> Scope["Scope: management group,<br/>subscription, resource group,<br/>or individual resource"]
```

```bash
# Inspect a built-in role definition's actual permitted actions
az role definition list --name "Contributor" --output json

# A role assigned at a HIGHER scope is inherited by everything beneath it —
# a subscription-level assignment applies to every resource group inside it
az role assignment create --assignee "<group-id>" --role "Reader" \
  --scope "/subscriptions/<sub-id>"
```

**A subtlety worth stating precisely, since it's a frequent source of "why can this person do that" confusion during an access review: a principal's EFFECTIVE permission on a resource is the UNION of every role assignment that applies at that resource's scope or any scope above it — there's no single place a role assignment "overrides" a broader one; they simply all add up.** This is exactly why `az role assignment list` scoped to a specific resource (Part 1's cheat sheet) needs to check inherited assignments from parent scopes too, not just assignments made directly at that resource.

### Diagnosing an Unexpected Access Denial

A genuinely practical, daily-use troubleshooting sequence worth having memorized, since "why did this fail with a 403" is one of the most common real support requests a platform team fields.

```bash
# 1. Confirm which identity is actually signed in — a stale
#    az login session for the wrong account is a common false lead
az account show --query user

# 2. List every role assignment at the resource itself
az role assignment list --scope "<full-resource-id>" --output table

# 3. List assignments inherited from every parent scope —
#    resource group, subscription, and management group
az role assignment list --scope "<full-resource-id>" --include-inherited --output table

# 4. Check whether an Azure Policy Deny assignment, not RBAC at all,
#    is the actual blocker (Part 1's RBAC-vs-Policy distinction)
az policy assignment list --scope "<full-resource-id>" --output table
```

Running these four checks in order, before assuming the cause, resolves the large majority of "access denied" investigations without guessing — most turn out to be either a missing scope-level assignment, a stale signed-in identity, or a Policy `Deny` effect masquerading as an RBAC problem.

---

## Custom Roles and Deny Assignments

Azure's built-in roles (`Owner`, `Contributor`, `Reader`, and hundreds more service-specific roles) cover most needs, but a genuinely narrower permission set sometimes requires a **custom role**.

```json
{
  "Name": "Shipment API Deployer",
  "Description": "Can deploy and restart the shipment-api App Service, nothing else",
  "Actions": [
    "Microsoft.Web/sites/restart/action",
    "Microsoft.Web/sites/publish/action",
    "Microsoft.Web/sites/read"
  ],
  "NotActions": [],
  "AssignableScopes": [
    "/subscriptions/<sub-id>/resourceGroups/rg-shipment-api-prod"
  ]
}
```

```bash
az role definition create --role-definition custom-role.json
```

**Custom roles have a real, worth-knowing ceiling: a tenant supports a maximum of 5,000 custom role definitions, and a custom role that includes `DataActions` (permissions on data WITHIN a resource, like reading blob contents, as opposed to managing the resource itself) cannot be assigned at the management-group scope** — only at subscription, resource-group, or resource scope.

### Deny Assignments

A **deny assignment** is the rare, explicit exception to RBAC's additive model: it blocks a specific action for a specific principal at a scope, and — unlike a role's `NotActions`, which just excludes an action from that one role definition — a deny assignment overrides EVERY role assignment that would otherwise grant it, including `Owner`. Deny assignments aren't created directly through everyday RBAC commands; they're generated automatically by mechanisms like Deployment Stacks (Part 15) protecting resources they manage from being modified outside the declared deployment.

> **From the Trenches:** A brief, worth-noting deprecation: Azure Blueprints, an older mechanism that also generated deny assignments to protect blueprint-managed resources, reached end of life in mid-2026. Any team still relying on an existing Blueprint assignment for this protection needs to migrate to Template Specs plus Deployment Stacks (Part 15) — checking for lingering Blueprint dependencies is a real, worthwhile audit step for any organization that adopted Blueprints early.

---

## App Registrations vs. Enterprise Applications

A distinction that trips up nearly everyone the first time they encounter it in the portal: **App Registrations** and **Enterprise Applications** are two views of related but distinct objects.

```mermaid
graph TD
    AppReg["App Registration<br/>(the APPLICATION object) —<br/>defines the app GLOBALLY:<br/>redirect URIs, API permissions,<br/>credentials"] --> SP["Service Principal<br/>(the ENTERPRISE APPLICATION) —<br/>the app's LOCAL, usable identity<br/>in THIS specific tenant"]
```

**An App Registration defines what an application IS — its name, its requested API permissions, its redirect URIs — as a single, potentially multi-tenant definition.** A **Service Principal** (shown in the portal under "Enterprise Applications") is the concrete, tenant-local identity that actually gets granted permissions and shows up in RBAC assignments and sign-in logs — every App Registration automatically creates a corresponding Service Principal in its own home tenant, and a multi-tenant app gets a *separate* Service Principal created in each tenant that consents to use it.

```bash
# Create an app registration
az ad app create --display-name "meridian-deploy-pipeline"

# The corresponding service principal — THIS is what gets RBAC role assignments
az ad sp create --id "<app-id>"
```

---

## Service Principals — Identities for Automation

A **service principal** is the non-human identity a script, pipeline, or application authenticates as. It's the direct Azure analog of an AWS IAM role assumed by a service, or a GCP service account.

```bash
# Create a service principal with a client secret (the LEGACY, less-secure pattern)
az ad sp create-for-rbac --name "meridian-ci-pipeline" \
  --role "Contributor" --scopes "/subscriptions/<sub-id>/resourceGroups/rg-shipment-api-prod"
```

**Why this specific command's output (a client ID plus a client SECRET) is worth flagging as something to actively avoid for anything running inside Azure, worth stating with real force: a client secret is a long-lived credential that must be stored somewhere (a CI/CD secret store, a config file) and rotated manually — every stored secret is a genuine, ongoing liability, directly extending the "secrets sprawl" problem the DevSecOps series covered.** The next two sections cover Azure's two better answers to this, for workloads inside and outside Azure respectively.

---

## Managed Identities — System-Assigned vs. User-Assigned

**A managed identity is a special-purpose service principal that Azure itself creates, rotates, and manages — the workload never sees or stores a credential at all.** This is Microsoft's own current, explicit recommendation: **if a workload runs on Azure, there is almost never a good reason to use a service principal with a stored secret instead.**

```mermaid
graph TD
    SysAssigned["System-Assigned<br/>Managed Identity —<br/>tied 1:1 to ONE resource's<br/>lifecycle (deleted when it is)"] --> Example1["e.g. one VM's own identity"]
    UserAssigned["User-Assigned<br/>Managed Identity —<br/>a standalone identity object,<br/>attachable to MULTIPLE resources"] --> Example2["e.g. shared by an entire<br/>VM Scale Set (Part 3)"]
```

```bash
# Enable a system-assigned managed identity on an existing App Service
az webapp identity assign --name shipment-api --resource-group rg-shipment-api-prod

# Create a standalone, reusable user-assigned managed identity
az identity create --name mi-shipment-api-shared --resource-group rg-shipment-api-prod

# Grant that identity access to a specific resource — e.g. read secrets
# from a Key Vault (Part 12), with NO credential ever stored anywhere
az role assignment create --assignee "<managed-identity-principal-id>" \
  --role "Key Vault Secrets User" \
  --scope "/subscriptions/<sub-id>/resourceGroups/rg-shipment-api-prod/providers/Microsoft.KeyVault/vaults/kv-meridian"
```

| | System-assigned | User-assigned |
|---|---|---|
| Lifecycle | Tied to one resource — deleted automatically when it is | Independent — must be explicitly deleted, survives its attached resources |
| Reusable across resources? | No — one identity per resource | Yes — one identity can be attached to many resources (e.g. every instance in a VM Scale Set) |
| Best fit | A single resource with a unique, non-shared identity need | A shared identity used consistently across a fleet of resources |

For Meridian Freight's `shipment-api` App Service (Part 10), a system-assigned managed identity is the natural fit — it needs to read secrets from Key Vault and nothing else — while the `driver-portal`'s VM Scale Set (Part 3) uses a shared user-assigned identity across every instance, since instances scale in and out constantly and shouldn't each mint a brand-new identity.

### The Cross-Tenant Limitation Worth Knowing Before It Surprises Anyone

A managed identity's underlying service principal is created in, and scoped to, the SAME tenant as the resource using it — it has no meaning and cannot be granted access in a different tenant. This matters concretely for any organization structured with multiple tenants (a common pattern after a merger or acquisition, or for isolating a heavily regulated subsidiary): a managed identity created in Meridian Freight's tenant cannot directly authenticate to a resource living in a separate, acquired company's own Entra tenant, even if that resource is otherwise reachable over the network. A service principal with workload identity federation, cross-tenant B2B guest access, or Azure Lighthouse (a delegated cross-tenant resource-management mechanism, referenced again in Part 16's multi-subscription discussion) are the genuine options for that specific cross-tenant scenario — a managed identity alone is never the answer once a second tenant is involved.

---

## Workload Identity Federation — Secretless Authentication From Outside Azure

Managed identities only work for resources running **inside** Azure — a GitHub Actions runner, a workload in another cloud, or an on-premises CI server can't use one, since there's no Azure metadata endpoint for them to call. **Workload identity federation** solves the equivalent problem for exactly these cases: it lets an external identity provider's own short-lived token be exchanged for an Azure AD token, with no stored secret at all.

```mermaid
sequenceDiagram
    participant GH as GitHub Actions runner
    participant GHOIDC as GitHub's own OIDC provider
    participant Entra as Microsoft Entra ID
    participant Azure as Azure Resource Manager
    GH->>GHOIDC: Request a short-lived OIDC token
    GHOIDC-->>GH: Signed token (valid minutes, scoped to this specific workflow run)
    GH->>Entra: Exchange OIDC token for an Azure AD access token
    Entra->>Entra: Verify token against a pre-configured Federated Credential
    Entra-->>GH: Azure AD access token issued
    GH->>Azure: Deploy using the Azure AD token — NO stored secret anywhere
```

```bash
# Configure a federated credential on an existing app registration,
# trusting a SPECIFIC GitHub repo + branch's OIDC tokens
az ad app federated-credential create --id "<app-object-id>" --parameters '{
  "name": "meridian-shipment-api-main-branch",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:meridianfreight/shipment-api:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'
```

**Why this is worth treating as the correct default for CI/CD pipelines specifically, echoing the exact recommendation Part 15 will build on: a federated credential has NOTHING to leak — there is no long-lived secret sitting in GitHub's secret store for an attacker to exfiltrate, since the token GitHub presents is freshly minted, short-lived, and scoped to that one specific repository and branch by the `subject` claim.** This directly closes the exact secrets-sprawl gap the previous section flagged for service principals with stored client secrets, for the one case (external identity providers) where a managed identity itself isn't an option.

```bash
# Verify a federated credential is correctly scoped before trusting it —
# a subject claim that's too broad (e.g. matching any branch, not just
# main) grants far more than intended
az ad app federated-credential list --id "<app-object-id>" --output table
```

Getting the `subject` claim's scope wrong is a genuinely easy mistake with real consequences: a subject pattern matching `repo:meridianfreight/shipment-api:*` (any ref in the repo) rather than the specific `refs/heads/main` branch shown earlier would let a token minted from a pull-request branch — potentially opened by any contributor with write access, not just a trusted release process — authenticate with the same production-scoped Azure AD token a merge to `main` would get. Scoping the subject claim as narrowly as the actual deployment workflow requires is the whole security value of workload identity federation; a too-broad subject claim quietly gives that value back.

---

## Access Reviews and Identity Governance

Access, once granted, has a well-documented tendency to never get revoked — a contractor's guest access outlives their contract, a role assignment made for a one-time migration project stays active for years. **Access reviews** are Entra ID's structural answer: a recurring, assignable task ("does this access still make sense?") routed to a designated reviewer, with an option to auto-remove access from anyone who doesn't respond.

```bash
# Access reviews are configured via Microsoft Graph or the portal's
# Identity Governance blade — list existing review definitions
az rest --method get --uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions"
```

**Why this matters as a genuine governance control, not paperwork: without a recurring review, "who has access to what, and does it still make sense" is a question that can only be answered by manually auditing role assignments — a task that gets skipped under delivery pressure exactly the way the AWS series flagged deferred governance tooling getting skipped.** A quarterly access review on Meridian Freight's guest-user population and its PIM-eligible role assignments turns that question into a routine, scheduled task instead of a reactive scramble during a security audit.

```bash
# Create a recurring access review targeting a specific group's
# membership, with automatic removal for non-response
az rest --method post \
  --uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions" \
  --body '{
    "displayName": "Quarterly guest access review",
    "scope": {"query": "/groups/<guest-group-id>/transitiveMembers", "queryType": "MicrosoftGraph"},
    "reviewers": [{"query": "<reviewer-object-id>", "queryType": "MicrosoftGraph"}],
    "settings": {"defaultDecision": "Deny", "autoApplyDecisionsEnabled": true, "recurrence": {"pattern": {"type": "absoluteMonthly", "interval": 3}}}
  }'
```

Setting `defaultDecision` to `Deny` for non-response (rather than `Approve`) is worth calling out explicitly as the correct default posture: it means access is removed automatically unless a reviewer actively confirms it should continue, closing the exact gap where accumulated, forgotten access otherwise persists indefinitely simply because nobody got around to reviewing it.

---

## Administrative Units — Delegating Identity Administration Without Full Directory Access

A genuinely useful, easy-to-overlook feature for a growing organization: **administrative units** let a directory role (like "User Administrator," from the previous section's Entra-roles discussion) be scoped down to a specific subset of users or groups, rather than granting it tenant-wide.

```mermaid
graph TD
    Tenant["Full Microsoft Entra tenant<br/>(hundreds of users)"] --> AU1["Administrative Unit:<br/>Driver Portal Users"]
    Tenant --> AU2["Administrative Unit:<br/>Platform Engineering"]
    AU1 --> Scoped["A regional ops lead gets<br/>'User Administrator' scoped<br/>ONLY to this AU —<br/>can reset driver-portal<br/>users' passwords, nothing else"]
```

```bash
# Create an administrative unit and add members to it
az ad administrative-unit create --display-name "Driver Portal Users"
az ad administrative-unit member add \
  --administrative-unit-id "<au-id>" --member-id "<user-object-id>"
```

**Why this matters concretely for an organization like Meridian Freight with genuinely distinct user populations (internal employees vs. the much larger, higher-turnover population of independent driver-portal users): granting a regional operations lead a directory-wide "User Administrator" role just so they can reset a driver's forgotten password would hand them the ability to manage EVERY user in the tenant, including platform engineers and executives.** Scoping that same role to an administrative unit containing only driver-portal users grants exactly the access the job requires and nothing more — a direct, practical application of least privilege at the identity-administration layer itself, not just at the Azure-resource layer RBAC governs.

```bash
# Assign the "User Administrator" Entra role, scoped to ONE
# administrative unit rather than the whole directory
az rest --method post \
  --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
  --body '{
    "principalId": "<regional-ops-lead-object-id>",
    "roleDefinitionId": "<user-administrator-role-id>",
    "directoryScopeId": "/administrativeUnits/<au-id>"
  }'
```

---

## Hybrid Identity — Microsoft Entra Connect and Hybrid Join

Meridian Freight's legacy on-premises freight-routing servers (introduced in Part 1) still authenticate against an on-premises Active Directory domain — **Microsoft Entra Connect** is the tool that synchronizes those on-premises AD DS identities into Entra ID, so the same user has one consistent identity across both worlds rather than two separate, drifting accounts.

```mermaid
graph TD
    OnPremAD["On-premises AD DS<br/>(source of truth for<br/>domain-joined machines)"] --> Connect["Microsoft Entra Connect<br/>(sync agent)"]
    Connect --> Entra["Microsoft Entra ID<br/>(synced identity, usable<br/>for cloud app sign-in<br/>and Azure RBAC)"]
```

| Sync feature | What it enables |
|---|---|
| Password Hash Sync | On-prem password hash synced to Entra ID — same password works in both worlds, without a live dependency on the on-prem domain controller for cloud sign-in |
| Pass-Through Authentication | Cloud sign-in validates the password against the on-prem domain controller directly, in real time — no hash leaves the on-prem network |
| Federation (AD FS) | An older, more complex pattern delegating authentication entirely to an on-premises federation server |
| Hybrid Join | A device joined to on-prem AD DS is ALSO registered in Entra ID, enabling Conditional Access device-compliance checks on domain-joined machines |

**Why Password Hash Sync is worth naming as Microsoft's own current default recommendation for most organizations, rather than the historically more common Federation pattern: it has no dependency on on-premises infrastructure being reachable for cloud sign-in to succeed** — an on-premises domain controller outage doesn't take down cloud application access the way a Federation-based design's dependency on a reachable AD FS server would.

### Microsoft Entra Cloud Sync — A Lighter-Weight Alternative

Worth knowing as a genuinely current alternative rather than the only path: **Microsoft Entra Cloud Sync** runs a lightweight agent (rather than Entra Connect's single, more heavyweight sync server) and can pull from **multiple** on-premises AD DS forests into one tenant — a scenario Entra Connect's classic architecture handles far more awkwardly. For an organization with a single, simple AD DS forest, Entra Connect Sync remains a perfectly reasonable choice; for a company like Meridian Freight that grows by acquiring smaller regional carriers — each potentially arriving with its own pre-existing AD DS forest — Cloud Sync's native multi-forest support is the more forward-looking design, avoiding a costly later migration between the two sync mechanisms.

| | Entra Connect Sync | Entra Cloud Sync |
|---|---|---|
| Agent model | One heavier, more feature-complete sync server | Multiple lightweight agents, easier to scale horizontally |
| Multi-forest support | Possible, but more complex to configure | Native — designed for exactly this from the start |
| Feature completeness | Currently broader (some advanced sync rules only exist here) | Growing over time, sufficient for most common scenarios |

---

## A Full Worked Identity Bootstrap for Meridian Freight

```bash
# 1. Create the platform team's security group
az ad group create --display-name "Platform Engineers" --mail-nickname "platform-engineers"

# 2. Assign a P2 license to engineers who need PIM/Identity Protection
az rest --method post --uri "https://graph.microsoft.com/v1.0/users/priya.shah@meridianfreight.onmicrosoft.com/assignLicense" \
  --body '{"addLicenses": [{"skuId": "<p2-sku-id>"}], "removeLicenses": []}'

# 3. Convert standing Owner/Contributor assignments to PIM-eligible
#    (done via the portal's PIM blade or Microsoft Graph)

# 4. Require phishing-resistant MFA via Conditional Access authentication
#    context specifically at PIM role activation

# 5. Create a break-glass emergency account, excluded from every
#    Conditional Access policy, credential stored offline

# 6. Create a system-assigned managed identity for shipment-api,
#    granting it exactly the Key Vault Secrets User role it needs
az webapp identity assign --name shipment-api --resource-group rg-shipment-api-prod

# 7. Configure workload identity federation for the GitHub Actions
#    deployment pipeline — no stored secret in GitHub at all
az ad app federated-credential create --id "<app-object-id>" --parameters '{...}'

# 8. Schedule a quarterly access review over guest users and
#    PIM-eligible privileged role assignments
```

---

## Part 2 CLI Cheat Sheet

| Area | Command | Purpose |
|---|---|---|
| Users | `az ad user create` | Create a cloud-only user |
| Guests | `az ad user invite` | Invite an external partner as a guest |
| Groups | `az ad group create` / `az ad group member add` | Create a security group and add members |
| RBAC | `az role assignment create` | Grant a role to a principal at a scope |
| RBAC | `az role definition create` | Create a custom role definition |
| RBAC | `az role assignment list` | List effective (including inherited) role assignments |
| App identity | `az ad app create` / `az ad sp create` | Create an app registration and its service principal |
| Managed identity | `az webapp identity assign` | Enable a system-assigned managed identity on an App Service |
| Managed identity | `az identity create` | Create a standalone user-assigned managed identity |
| Federation | `az ad app federated-credential create` | Configure secretless workload identity federation |
| Conditional Access / PIM | `az rest ... graph.microsoft.com` | Manage CA policies, PIM, and access reviews via Microsoft Graph |
| Elevation | `az rest ... elevateAccess` | Global Administrator's one-time elevation to Azure RBAC User Access Administrator |
| Cross-tenant | `az rest ... crossTenantAccessPolicy` | View/manage inbound and outbound collaboration trust settings |

---

## Common Mistakes and Interview Traps

| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Assuming Microsoft Entra ID and on-premises AD DS are the same system | They use different protocols and solve different problems — one doesn't "replace" the other automatically | Understand hybrid identity (Entra Connect) as bridging two distinct systems, not a migration that eliminates one |
| Granting standing, permanent Owner/Global Administrator roles | Maximum exposure window if the credential is ever compromised | Use PIM to make privileged roles eligible and time-boxed instead of permanently active |
| Using a service principal with a client secret for a workload running inside Azure | A stored secret is a standing liability that must be rotated and can leak | Use a managed identity instead — it has no credential to store or leak |
| Using a service principal with a client secret for a CI/CD pipeline outside Azure | Same standing-secret liability, avoidable for federatable external identity providers | Use workload identity federation (OIDC) for GitHub Actions, other clouds, or Kubernetes workloads |
| Not configuring a break-glass account excluded from all Conditional Access policies | A misconfigured Conditional Access policy can lock every administrator out of the tenant with no recovery path | Always maintain at least one excluded emergency-access account with a strong, offline-stored credential |
| Relying on SMS-based MFA as the strongest available factor | SMS is vulnerable to SIM-swap attacks and doesn't resist phishing | Require phishing-resistant methods (FIDO2, Windows Hello) for privileged accounts and PIM activation |
| Granting guest users the same default access as full-time employees | External partners' access should be scoped narrowly, not inherited from a generic template | Apply Conditional Access policies and RBAC scoped specifically to the guest population |
| Treating access review as a one-time cleanup rather than a recurring process | Access naturally accumulates and goes stale without a repeated check | Schedule recurring access reviews over guest users and privileged role assignments |
| Assuming a Global Administrator can manage every Azure resource by default | Microsoft Entra roles and Azure RBAC roles are separate systems — Entra roles grant no Azure resource access automatically | Use the explicit, auditable elevation action, or a separate Azure RBAC assignment, when a role genuinely needs both |
| Leaving outbound cross-tenant collaboration unrestricted | Employees can accept guest invitations into arbitrary, unvetted external tenants with no organizational review | Scope outbound cross-tenant access settings deliberately, the same way inbound guest access is scoped |

---

## Worked Practice Problems

**Problem 1:** Meridian Freight's `shipment-api` App Service needs to read a database connection string stored in Key Vault (Part 12). An engineer proposes creating a service principal with a client secret, storing that secret as an App Service application setting. What's the better approach, and why?

*Answer:* Enable a system-assigned managed identity on the App Service instead, and grant that managed identity's principal the `Key Vault Secrets User` role scoped to the specific Key Vault. This eliminates the stored secret entirely — Azure creates and rotates the managed identity's underlying credential automatically, and the App Service's code authenticates using the Azure SDK's managed identity support with zero credential material anywhere in configuration. A client-secret-based service principal, by contrast, requires that secret to be stored somewhere (an App Service setting is itself a real leak surface) and manually rotated before it expires — Microsoft's own current guidance explicitly recommends managed identities for exactly this scenario, workloads running on Azure.

**Problem 2:** A security audit at Meridian Freight finds that four former contractors, whose engagements ended between six and eighteen months ago, still hold active guest-user accounts with standing Contributor access to the production resource group. No Conditional Access policy or access review currently applies to guest users specifically. What's the root cause, and what two changes would prevent recurrence?

*Answer:* The root cause is the absence of any recurring governance process specifically targeting guest accounts — access was granted once, at onboarding, and nothing ever re-evaluated whether it should still exist after the engagement ended. Two changes address this: first, a recurring (e.g. quarterly) access review scoped specifically to the guest-user population, routed to the resource owner, with automatic removal for non-response; second, applying a Conditional Access policy scoped to guest accounts specifically (e.g., requiring more frequent re-authentication or restricting access to approved locations) so guest access is deliberately governed as a distinct risk category rather than defaulting to the same treatment as full-time employee access.

**Problem 3:** An organization deploys Conditional Access requiring a compliant, hybrid-joined device for all administrative sign-ins, tested thoroughly in a staging tenant. On rollout to the production tenant, EVERY administrator — including the person who deployed the policy — is immediately locked out, because no device in the production tenant had yet completed hybrid join. What should have prevented this, and what's the immediate recovery path?

*Answer:* This is exactly the scenario a break-glass emergency account exists to prevent — an account deliberately excluded from every Conditional Access policy, with a strong, offline-stored credential, used ONLY for emergency recovery. If one had been correctly configured and excluded before rollout, the immediate recovery path is signing in with the break-glass account (which the faulty policy cannot block, since it's explicitly excluded) and either fixing or disabling the policy. Longer-term, the process gap is testing a new Conditional Access policy in "Report-only" mode against production first — which logs what the policy WOULD have done without actually enforcing it — before switching it to enforced, rather than assuming staging-tenant testing alone is sufficient given how differently device compliance state can look between the two.

**Problem 4:** A platform team wants Meridian Freight's GitHub Actions pipeline to deploy Bicep templates (Part 15) to Azure, and is deciding between a service principal with a client secret stored as a GitHub encrypted secret, versus workload identity federation. The team argues the client-secret approach is "simpler, and GitHub secrets are already encrypted at rest, so it's secure enough." What's the flaw in this reasoning?

*Answer:* GitHub's encryption at rest protects the secret from someone reading GitHub's own database directly, but it does nothing to prevent the secret from being exposed through the far more common leak paths: an overly permissive workflow accidentally echoing it into build logs, a compromised third-party GitHub Action with access to the runner's environment variables, or a former team member who copied it before offboarding. Workload identity federation removes this entire class of risk structurally rather than relying on careful handling — there is no persistent secret to leak in the first place, since each workflow run exchanges a short-lived, run-scoped OIDC token for an Azure AD token. The "simpler" argument for the client-secret approach also doesn't hold up in practice: federated credential setup is a one-time configuration step, and it removes the recurring operational burden of secret rotation the client-secret approach requires indefinitely.

**Problem 5:** Meridian Freight's on-premises freight-routing servers authenticate against an on-premises AD DS domain. The platform team wants users to sign into cloud applications with the same corporate password, without introducing a dependency where an on-premises domain controller outage would also break cloud application sign-in. Which Entra Connect synchronization method fits, and why?

*Answer:* Password Hash Sync is the right fit. It synchronizes a hash of each user's on-premises password into Entra ID, so cloud sign-in validation happens entirely within Entra ID itself, with no real-time dependency on an on-premises domain controller being reachable. Pass-Through Authentication, by contrast, validates each cloud sign-in against the on-premises domain controller in real time — functionally similar from the user's perspective, but it reintroduces exactly the dependency the team wants to avoid, since an on-premises domain controller or network outage would then also block cloud sign-in. Password Hash Sync is Microsoft's own current default recommendation for most organizations for precisely this resilience reason.

**Problem 6:** Meridian Freight promotes an engineer to a platform lead role, granting them the Entra ID "User Administrator" role so they can manage employee onboarding/offboarding. Three weeks later, the engineer asks why they still cannot view or manage the production resource group's virtual machines despite their new "administrator" title. What's the actual gap, and what's needed to close it?

*Answer:* The "User Administrator" role is a Microsoft Entra role, governing directory objects like users and groups — it grants no permissions over Azure resources like virtual machines, because Microsoft Entra roles and Azure RBAC roles are entirely separate permission systems with no automatic overlap. Closing the gap requires a deliberate, separate Azure RBAC role assignment (for example, `Reader` or a scoped custom role) explicitly granted at the relevant subscription or resource-group scope — the Entra role and the Azure RBAC role have to be assigned independently, because holding one was never going to imply the other regardless of how broadly either role's name sounds.

---

## Summary and What's Next

- **Microsoft Entra ID is not "Active Directory in the cloud"** — it's an architecturally distinct, protocol-different system, commonly run alongside on-premises AD DS via Entra Connect rather than replacing it outright.
- **Conditional Access** is the policy engine that evaluates sign-in context (user, device, location, risk) and enforces controls in real time; **Identity Protection** feeds continuous risk scoring into it, and a **break-glass account excluded from every policy** is a non-negotiable safety net.
- **PIM converts standing privileged access into eligible, time-boxed, re-authenticated activation** — the single highest-leverage change most organizations can make to reduce their privileged-access attack surface.
- **RBAC's effective permission on a resource is the union of every applicable role assignment at that scope and every scope above it** — there's no overriding, only accumulation, which is why access reviews matter.
- **Managed identities eliminate stored credentials for workloads running inside Azure; workload identity federation does the same for workloads outside it** (GitHub Actions, other clouds, Kubernetes) — a client-secret-based service principal should be treated as a last resort for either case, not a default.
- **App Registrations define an application; Service Principals are that application's tenant-local, RBAC-assignable identity** — every registration creates one automatically in its home tenant.
- **Microsoft Entra roles and Azure RBAC roles are two separate, non-overlapping permission systems** — a Global Administrator gains no Azure resource access without an explicit, auditable elevation step or a separate RBAC assignment.
- **Cross-tenant access settings govern collaboration in both directions** — inbound trust for a specific partner's guests, and outbound restrictions on which external tenants an organization's own users can be invited into.

**Continue to Part 3** (`03-compute-vms-and-scale-sets.md`) for Azure's compute layer — where this chapter's managed identities, RBAC roles, and Conditional Access policies get attached to the actual virtual machines and scale sets running Meridian Freight's workloads.
