Skip to content

0029 — CI/CD Azure authentication: user-assigned managed identity with OIDC workload identity federation ​

Status: Accepted

Date: 2026-06-21

ADO work item: AB#3082

Deciders: Kristopher Turner (platform owner)


Context ​

GitHub Actions workflows must authenticate to Azure to push container images to GHCR and update the Azure Container App on every push to main. Two mechanisms were available:

  1. Service principal with client secret — create an az ad sp, store the JSON credentials as a GitHub secret, reference in the workflow. Requires secret rotation; secret is a long-lived credential that can be leaked.
  2. User-assigned managed identity with workload identity federation (OIDC) — create an Azure managed identity, add a federated credential that trusts GitHub Actions OIDC tokens, grant the identity Azure RBAC roles. No stored secret anywhere — GitHub exchanges a short-lived OIDC token for an Azure access token at workflow runtime.

The azure/login@v2 action supports both patterns. The workflow already has permissions: id-token: write, which enables the OIDC token exchange.

Decision ​

Use a user-assigned managed identity with a workload identity federation credential scoped to the main branch of this repository. No client secret is stored in GitHub or anywhere else.

Container Apps have their own system-assigned managed identity for Key Vault access at runtime. That is a separate identity from the one used by GitHub Actions.

Rationale ​

  • No secret to rotate or leak — the OIDC exchange produces a short-lived token scoped to the workflow run
  • Azure RBAC grants are on the managed identity principal, not tied to a user account that could be offboarded
  • Consistent with the principle in CLAUDE.md and platform standards: no credentials committed or stored in CI

Implementation ​

Azure resources:

ResourceValue
Managed identity nameid-heritageva-github-actions
Resource grouprg-heritageva-prod-eus
Client ID4c52e001-3b2f-4871-ad90-783d230f6585
Principal ID97a4f620-309c-4f73-aec0-7a856dc970a6
RoleContributor on rg-heritageva-prod-eus

Federated credential:

FieldValue
Credential namegithub-actions-main
Issuerhttps://token.actions.githubusercontent.com
Subjectrepo:Heritage-Virginia/heritage-community-hub:ref:refs/heads/main
Audienceapi://AzureADTokenExchange

GitHub secrets set (non-sensitive identifiers):

SecretValue
AZURE_CLIENT_ID4c52e001-3b2f-4871-ad90-783d230f6585
AZURE_TENANT_IDd6fc73cf-2a7a-4876-b67f-ca48961a6e83
AZURE_SUBSCRIPTION_IDbe069ae1-fc96-4a07-9f8e-5994d83a137d

Workflow azure/login step:

yaml
- uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

No client-secret is passed. The action negotiates the OIDC exchange automatically.

Consequences ​

  • Adding new branches that need deploy rights requires adding an additional federated credential subject claim (e.g., repo:...:ref:refs/heads/release/...)
  • The Contributor role on the resource group is broad; if scope needs tightening in future, narrow to specific resource-level roles (Container Apps Contributor, etc.)
  • Container App system-assigned managed identity and this GitHub Actions managed identity are independent — do not confuse them

Heritage Community Hub — Internal. Access restricted via Cloudflare Access + Entra ID.