Purpose of this file
Create a passwordless identity GitHub Actions can use to authenticate to Azure, and grant it just enough permission to deploy the project's web apps and build its images.
Every automated deployment needs some way to prove to Azure that it's allowed to make changes. This file sets up that proof — using a modern approach (OIDC) instead of a traditional stored password or key.
The full file code sample
terraform/github_oidc.tfresource "azurerm_user_assigned_identity" "github" {
name = "${local.prefix}-github"
resource_group_name = azurerm_resource_group.metals.name
location = azurerm_resource_group.metals.location
}
resource "azurerm_federated_identity_credential" "github" {
name = "github-development"
user_assigned_identity_id = azurerm_user_assigned_identity.github.id
issuer = "https://token.actions.githubusercontent.com"
audience = ["api://AzureADTokenExchange"]
subject = local.oidc_subject
}
resource "azurerm_role_assignment" "github_deployment_api" {
scope = azurerm_linux_web_app.api.id
role_definition_name = "Website Contributor"
principal_id = azurerm_user_assigned_identity.github.principal_id
principal_type = "ServicePrincipal"
}
resource "azurerm_role_assignment" "github_deployment_ui" {
scope = azurerm_linux_web_app.ui.id
role_definition_name = "Website Contributor"
principal_id = azurerm_user_assigned_identity.github.principal_id
principal_type = "ServicePrincipal"
}
resource "azurerm_role_assignment" "github_deployment_tutorials" {
scope = azurerm_linux_web_app.tutorials.id
role_definition_name = "Website Contributor"
principal_id = azurerm_user_assigned_identity.github.principal_id
principal_type = "ServicePrincipal"
}
# AcrPush only grants data-plane pull/push actions; az acr build/show also
# need the control-plane "registries/read" action to look up the registry
# itself, which only a role like Contributor provides.
resource "azurerm_role_assignment" "github_acr_contributor" {
scope = azurerm_container_registry.metals.id
role_definition_name = "Contributor"
principal_id = azurerm_user_assigned_identity.github.principal_id
principal_type = "ServicePrincipal"
}
Why not just use a password? the problem this solves
A simple approach would be to generate an Azure password or key once, and paste it into a GitHub secret. It works, but that secret is long-lived: if it ever leaks, it keeps working until someone notices and manually rotates it.
With OIDC (OpenID Connect), GitHub issues a short-lived, cryptographically signed token for each individual workflow run, describing exactly which repository and environment it came from. Azure trusts GitHub's signature and checks that description against rules configured here — if it matches, Azure hands back temporary access. Nothing durable is ever stored as a GitHub secret; there's nothing long-lived to leak.
The identity itself azurerm_user_assigned_identity
This creates an empty Azure identity — on its own, an azurerm_user_assigned_identity can't do anything. It's just a named “account” that the next two resources give a way to log in to, and permission to use.
The federated identity credential azurerm_federated_identity_credential
This is the actual trust relationship: the rule that decides which GitHub tokens Azure will accept as proof of identity for the resource above.
"https://token.actions.githubusercontent.com" — the fixed address GitHub Actions uses to issue its identity tokens. This tells Azure “only trust tokens signed by GitHub Actions,” not tokens from anywhere else.
["api://AzureADTokenExchange"] — a fixed value that marks the token as intended for exchanging with Microsoft Entra ID specifically, rather than some other service GitHub could also issue tokens for.
local.oidc_subject — the computed string from main.tf, something like repo:owner/repo:environment:Development. This is the most important line: it means Azure will only accept tokens that say “I am the GitHub Actions workflow running in this exact repository, in this exact environment.” A workflow from a different repository, or the same repository but a different environment, presents a different subject and is rejected.
The role assignments azurerm_role_assignment
Being able to log in as this identity still wouldn't let it do anything — Azure permissions are granted separately, by role assignment. Deploying containers needs four: one per web app, plus one on the registry.
| Resource | Role | Scope | Why it's needed |
|---|---|---|---|
github_deployment_api | Website Contributor | the API web app | Set its container image and restart it. |
github_deployment_ui | Website Contributor | the UI web app | The same, for the UI — a second assignment, because scope is per-resource. |
github_deployment_tutorials | Website Contributor | the tutorials web app | The same, for the tutorial site. |
github_acr_contributor | Contributor | the container registry | Run az acr build, which uploads source and schedules a build. |
The scope is what the permission applies to. Each of these is scoped to one specific resource — not the resource group, and not the subscription. If the GitHub Actions identity were ever compromised, the damage is limited to those three resources; the database, in particular, is untouched.
AcrPush sounds like the right role and isn't. az acr build doesn't push a locally-built image — it uploads source and asks the registry to build it, which are management-plane actions (listBuildSourceUploadUrl, scheduleRun) that the data-plane push roles don't include. Contributor, scoped to the single registry, is the smallest built-in role that covers them.
The full progression of failures this produces — and how to read them — is on the Identity & permissions page.
principal_id identifies the identity created above, and principal_type = "ServicePrincipal" tells Azure this principal is an application/service identity, not a human user — which avoids an extra Azure lookup and a small delay Terraform would otherwise need to resolve the principal's type on its own.
Key terms for beginners
- OIDC (OpenID Connect)
- A standard way for one system (like GitHub Actions) to prove its identity to another (like Azure) using short-lived, verifiable tokens, instead of a shared long-lived secret.
- Managed identity
- An Azure identity that applications or automated processes can authenticate as, without a human ever typing a password for it.
- Federated identity credential
- The specific trust rule connecting an external token issuer (GitHub) to an Azure identity, defining exactly which tokens are acceptable.
- Role assignment
- The Azure record that grants a specific identity a specific role (a bundle of permissions) at a specific scope — the combination of who, what they can do, and where.
- Least privilege
- The security practice of granting only the minimum permissions needed for a task, so that if an identity is ever misused, the possible damage is limited.