Azure Landing Zone Identity: Entra ID, RBAC, and PIM

min read

The most common Azure security finding in enterprise environments is not a misconfigured firewall or an exposed storage account. It is a service principal with Owner rights that was created two years ago, whose secret has never rotated, and whose owner left the company six months ago. Nobody knows what it does. Nobody is willing to delete it.

Identity is the control plane of Azure. Every action — whether taken by a human, a CI/CD pipeline, or a workload — is authorized through either Entra ID roles or Azure RBAC. Organizations that treat identity as an afterthought end up with over-privileged accounts, stale service principals, standing access to production, and no auditability. A landing zone designed without a deliberate identity model accumulates these problems as quickly as new workloads arrive.

By the end of this guide, you will:

  • Distinguish between Entra ID roles and Azure RBAC to close dangerous governance gaps.
  • Apply RBAC at the management group level to ensure every workload subscription inherits a consistent access baseline.
  • Configure PIM to eliminate standing privileged access for human operators.
  • Replace service principal secrets in CI/CD pipelines with OIDC federated credentials.
  • Deploy role assignments and federated credentials as IaC using Terraform and Bicep.

This is Post 3 in the Azure Platform Engineering series. It builds on the management group hierarchy from Post 1 and provides the OIDC identities used by the CI/CD pipeline in Post 8.


Two Identity Systems, One Azure Environment

Entra ID Roles — Tenant-Wide Administrative Access

Entra ID roles (formerly Azure AD roles) control administrative privileges to tenant-level services: user and group management, application registration, and PIM administration. These roles operate entirely within the Entra ID plane — they govern who can manage identities, not who can access Azure resources.

RoleWhat It ControlsNeeded By
Global AdministratorFull tenant adminBreak-glass accounts only
Privileged Role AdministratorManages Entra ID roles and PIM settingsPlatform team (via PIM)
Security AdministratorManages Defender XDR, Sentinel, CA policiesSecurity team
Application AdministratorCreates and manages app registrationsPlatform CI/CD pipeline
Directory ReadersReads directory objects (users, groups)Most automation identities

The Elevation Boundary: A Global Administrator cannot read a VM’s disk or view a subscription’s resources unless they also have an Azure RBAC role on that subscription. An Owner on a subscription cannot escalate themselves to Global Administrator. These are separate authorization systems.

Azure RBAC — Resource-Level Authorization

Azure RBAC controls who can perform specific actions on Azure resources. The scope hierarchy mirrors the management group hierarchy: Tenant Root → Management Group → Subscription → Resource Group → Resource.

Three built-in roles form the foundation of landing zone RBAC designs:

  • Owner: Full resource control plus the ability to assign roles to others.
  • Contributor: Full resource control, but cannot manage role assignments.
  • Reader: Read-only access.

Role assignments are additive. A principal’s effective permissions are the union of all role assignments across all applicable scopes.

Scale Tip: Azure allows 500 role assignments per management group. Assigning at the management group level uses 1 of that 500 quota but covers all subscriptions beneath it without consuming per-subscription quota. For 50 subscriptions, this is a 50:1 efficiency gain over subscription-level assignments.


RBAC Design at Management Group Scope

RBAC Conditions — Constraining Owner Permissions

The platform CI/CD pipeline requires Owner at the management group level to create role assignments for policy remediation identities. However, an unrestricted Owner assignment is a security risk.

RBAC conditions (GA since 2023) constrain Owner assignments with attribute-based logic. An 8 KB size limit applies to the condition expression; if you approach this limit, consider using Custom Security Attributes for more complex logic.

The following condition allows the pipeline to assign Contributor but blocks it from assigning Owner or User Access Administrator:

# Terraform: azurerm_role_assignment with RBAC condition
resource "azurerm_role_assignment" "platform_cicd_owner" {
  scope                = "/providers/Microsoft.Management/managementGroups/${var.platform_mg_id}"
  role_definition_name = "Owner"
  principal_id         = azurerm_user_assigned_identity.platform_cicd.principal_id

  condition_version = "2.0"
  # Use ForAllOfAnyValues:GuidNotEquals to ensure ALL requested roles 
  # are checked against the blocked list.
  condition = <<-EOT
    (
      (!(ActionMatches{'Microsoft.Authorization/roleAssignments/write'}))
      OR
      (
        @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]
          ForAllOfAnyValues:GuidNotEquals {
            8e3af657-a8ff-443c-a75c-2fe8c4bcb635, # Owner
            18d7d88d-d4f5-4b35-97b4-c3f4b4b9b9b6  # User Access Administrator
          }
      )
    )
  EOT

  skip_service_principal_aad_check = true
}

Privileged Identity Management

PIM for Groups — Scaling JIT Access

PIM for Groups (GA since 2023) ensures that users hold an eligible assignment rather than a standing one. Users are made eligible members of an Entra security group, and the group holds the role assignment.

This separates access management (group membership) from role management (group assignments). Adding a new engineer means adding them to the sg-platform-engineers group as an eligible member.

Automating PIM Role Settings

PIM role settings (activation duration, MFA, approval) must be configured per-role per-scope. Since these are not fully supported in the azurerm provider yet, use the Microsoft Graph API via a two-step az rest process:

# 1. Fetch the specific policy ID for the 'Owner' role at this scope
POLICY_ID=$(az rest --method GET --url "https://management.azure.com/providers/Microsoft.Management/managementGroups/${MG_ID}/providers/Microsoft.Authorization/roleManagementPolicies?api-version=2020-10-01" --query "value[?roleDefinitionId=='/providers/Microsoft.Authorization/roleDefinitions/${OWNER_ROLE_ID}'].id" -o tsv)

# 2. PATCH the policy to require MFA and 2-hour max duration
az rest --method PATCH --url "https://management.azure.com/${POLICY_ID}?api-version=2020-10-01" --body '{
    "properties": {
      "rules": [
        {
          "id": "Expiration_Admin_Assignment",
          "ruleType": "RoleManagementPolicyExpirationRule",
          "isExpirationRequired": true,
          "maximumDuration": "PT2H"
        },
        {
          "id": "Enablement_Admin_Assignment",
          "ruleType": "RoleManagementPolicyEnablementRule",
          "enabledRules": ["MultiFactorAuthentication", "Justification"]
        }
      ]
    }
  }'

Pipelines Without Secrets: Workload Identity with OIDC

Workload Identity Federation (OIDC) replaces static client secrets with short-lived tokens.

  sequenceDiagram
    autonumber
    participant GH as GitHub Actions
    participant Entra as Entra ID
    participant Azure as Azure Resource Manager

    GH->>GH: Generate OIDC ID Token (JWT)
    GH->>Entra: Request Access Token (JWT + Aud/Sub)
    Note over Entra: Validate Federated Identity<br/>(Repo/Env match Subject)
    Entra-->>GH: Issue Short-lived Access Token
    GH->>Azure: Deploy IaC (Terraform/Bicep)
    Note right of Azure: Authorization via RBAC Roles
    Azure-->>GH: Deployment Success

Notes:

  • No Client Secrets are stored in GitHub; the trust is based on the repository identity.
  • The Subject (sub) claim must match the Federated Credential configuration in Entra ID.
  • Tokens are short-lived (usually 1 hour), significantly reducing the risk of credential leakage.

Terraform — OIDC Federated Credential

# Federated credential for GitHub Actions (environment: production)
resource "azuread_application_federated_identity_credential" "github_prod" {
  application_id = azuread_application.landing_zone_cicd.id
  display_name   = "github-production"
  issuer         = "https://token.actions.githubusercontent.com"
  subject        = "repo:contoso/landing-zone:environment:production"
  audiences      = ["api://AzureADTokenExchange"]
}

Best Practices

  • Eliminate Standing Access: Make every Entra ID privileged role PIM-eligible. The 60-second activation overhead is a small price for a full audit trail.
  • Use User-Assigned Managed Identities: These survive resource recreation and can be pre-assigned roles in IaC.
  • Audit via Access Reviews: Schedule quarterly PIM reviews. If an eligible assignment isn’t activated for six months, revoke it.
  • Never Use Secrets: OIDC has been the standard for GitHub Actions since 2022. Delete existing secrets and migrate to federated credentials.

Troubleshooting

“Role assignment fails — ‘PrincipalNotFound’ error” The service principal was recently created and hasn’t replicated. In Bicep, set principalType: 'ServicePrincipal' explicitly to skip the AAD lookup. In Terraform, use skip_service_principal_aad_check = true.

“OIDC login fails — AADSTS70021” The sub claim in the JWT does not match the federated credential. Debug by printing the token claims in your workflow:

TOKEN=$(curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" | jq -r '.value')
echo $TOKEN | cut -d'.' -f2 | base64 -d | jq .sub

Sources

Move to Post 4: Governance at Scale with Azure Policy. The patterns established here are prerequisites for enforcing compliance and automating remediation across your subscriptions.