Appearance
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:
- 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. - 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:
| Resource | Value |
|---|---|
| Managed identity name | id-heritageva-github-actions |
| Resource group | rg-heritageva-prod-eus |
| Client ID | 4c52e001-3b2f-4871-ad90-783d230f6585 |
| Principal ID | 97a4f620-309c-4f73-aec0-7a856dc970a6 |
| Role | Contributor on rg-heritageva-prod-eus |
Federated credential:
| Field | Value |
|---|---|
| Credential name | github-actions-main |
| Issuer | https://token.actions.githubusercontent.com |
| Subject | repo:Heritage-Virginia/heritage-community-hub:ref:refs/heads/main |
| Audience | api://AzureADTokenExchange |
GitHub secrets set (non-sensitive identifiers):
| Secret | Value |
|---|---|
AZURE_CLIENT_ID | 4c52e001-3b2f-4871-ad90-783d230f6585 |
AZURE_TENANT_ID | d6fc73cf-2a7a-4876-b67f-ca48961a6e83 |
AZURE_SUBSCRIPTION_ID | be069ae1-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
Contributorrole 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