TutorialsTerraform › github_oidc.tf

Terraform Tutorial · File 7 of 9

github_oidc.tf

Builds a secure digital identity that lets the GitHub Actions deployment workflows prove who they are to Azure, and grants that identity exactly the permissions container deployment needs — configuring and restarting all three web apps, and building images in the container registry — without ever storing a password or long-lived secret.

Codey the Sr Developer, standing with a pointer, ready to walk through the guide
Resources created → 6 Auth method → OIDC (OpenID Connect) Role assignments → 4, each narrowly scoped

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.

Codey inspecting something with a magnifying glass
proving identity, without a password

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

The old way: a stored secretwhat this avoids

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.

The OIDC way: prove it, per runwhat this does instead

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.

issuerwho's allowed to vouch

"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.

audiencewho the token is for

["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.

subjectexactly which workflow run

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.

ResourceRoleScopeWhy it's needed
github_deployment_apiWebsite Contributorthe API web appSet its container image and restart it.
github_deployment_uiWebsite Contributorthe UI web appThe same, for the UI — a second assignment, because scope is per-resource.
github_deployment_tutorialsWebsite Contributorthe tutorials web appThe same, for the tutorial site.
github_acr_contributorContributorthe container registryRun az acr build, which uploads source and schedules a build.
scope is the most important lineleast privilege

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.

Why Contributor on the registryand not AcrPush

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 / principal_typewho's receiving the permission

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.

Curious what these same three resources look like as individual az commands instead? See az_setup_github_oidc in the Manual Deployment tutorial.

Codey giving a thumbs up

GitHub can now prove who it is — last stop, the receipt Terraform hands back when it's done.