Azure OIDC
With OpenID Connect (OIDC), the GitHub Actions job that runs your plan authenticates to Azure with a short-lived token from GitHub, through an Entra ID federated credential. You store no client secret in your repository. It is the recommended method for production.
Setup has two steps:
- Create an App Registration, its service principal, and a federated credential with the Azure CLI. Do this one time, from your workstation, not through Orchestration.
- Add an
oidchook to.stategraph/config.yml.
Prerequisites
- The Azure CLI
- An Azure subscription with permission to create App Registrations and assign RBAC roles
Setup
- Log in to the Azure CLI:
az login
- Get your subscription ID and tenant ID:
az account show --query '{subscriptionId:id, tenantId:tenantId}' -o json
Output:
{
"subscriptionId": "00000000-0000-0000-0000-000000000000",
"tenantId": "00000000-0000-0000-0000-000000000000"
}
- Export the values for the later commands.
GITHUB_ORGandREPO_NAMEare case-sensitive.
export SUBSCRIPTION_ID="<subscription-id>"
export TENANT_ID="<tenant-id>"
export GITHUB_ORG="<github-org-or-username>"
export REPO_NAME="<repository-name>"
- Create an App Registration, and note the
appIdin the output:
az ad app create --display-name "stategraph"
export CLIENT_ID="<appId>"
- Create a service principal for the App Registration:
az ad sp create --id "$CLIENT_ID"
- Add a federated credential. First, get the object ID of the App Registration. It is not the same as the client ID.
APP_OBJECT_ID=$(az ad app show --id "$CLIENT_ID" --query "id" -o tsv)
Then create a flexible federated credential through the Microsoft Graph beta API. Its claimsMatchingExpression accepts tokens from all branches, tags, and pull requests of your repository.
az rest --method POST \
--uri "https://graph.microsoft.com/beta/applications/${APP_OBJECT_ID}/federatedIdentityCredentials" \
--headers "Content-Type=application/json" \
--body "{'name': 'stategraph-github', 'issuer': 'https://token.actions.githubusercontent.com', 'audiences': ['api://AzureADTokenExchange'], 'description': 'Stategraph GitHub Actions OIDC', 'claimsMatchingExpression': {'value': 'claims[\'sub\'] matches \'repo:${GITHUB_ORG}/${REPO_NAME}:*\'', 'languageVersion': 1}}"
Make sure that the credential exists:
az rest --method GET \
--uri "https://graph.microsoft.com/beta/applications/${APP_OBJECT_ID}/federatedIdentityCredentials" \
--query "value[].{name:name, expression:claimsMatchingExpression.value}" -o table
- Assign an RBAC role to the service principal on your subscription:
az role assignment create \
--assignee "$CLIENT_ID" \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID"
Contributor is an Azure built-in role. It grants full access to manage resources, but it cannot assign roles in Azure RBAC. Use the role that fits your organization.
Why claimsMatchingExpression
Orchestration runs on branches and pull requests, so the federated credential must match any ref in your repository. The standard Azure subject field needs an exact match, with no wildcards. The claimsMatchingExpression property with the matches operator accepts the * (multi-character) and ? (single-character) wildcards in the sub claim. It is a preview feature, Flexible Federated Identity Credentials.
Other expressions for the sub claim:
claims['sub'] matches 'repo:org/repo:ref:refs/heads/*': any branch onlyclaims['sub'] matches 'repo:org/repo:ref:refs/tags/*': any tag onlyclaims['sub'] eq 'repo:org/repo:ref:refs/heads/main': one specific branch
Configure Orchestration for OIDC
Add an oidc hook to .stategraph/config.yml at the root of your repository. Replace CLIENT_ID, TENANT_ID, and SUBSCRIPTION_ID with your values.
hooks:
all:
pre:
- type: oidc
provider: azure
client_id: "CLIENT_ID"
tenant_id: "TENANT_ID"
subscription_id: "SUBSCRIPTION_ID"
Before each operation, the hook gets a GitHub OIDC token with the audience api://AzureADTokenExchange. It sets these environment variables, and the Terraform AzureRM and AzAPI providers exchange the token with Entra ID for Azure credentials:
| Variable | Value |
|---|---|
ARM_USE_OIDC |
true |
ARM_CLIENT_ID |
Your App Registration client ID |
ARM_TENANT_ID |
Your Entra ID tenant ID |
ARM_OIDC_TOKEN |
The GitHub OIDC token |
ARM_SUBSCRIPTION_ID |
Your Azure subscription ID, when subscription_id is set |
For the other keys of the hook, see the hooks reference.
Test the setup
- Add a small Terraform configuration to your repository.
- Open a pull request with the change.
- Comment
stategraph planon the pull request.
Orchestration authenticates through the federated credential and posts the plan on the pull request.
Multiple environments
To use one App Registration or subscription per environment, put the oidc step in workflows, not in hooks. Each workflow selects directories with a tag_query and runs its own steps.
workflows:
- tag_query: "dir:terraform/production/**"
plan:
- type: oidc
provider: azure
client_id: "PROD_CLIENT_ID"
tenant_id: "TENANT_ID"
subscription_id: "PROD_SUBSCRIPTION_ID"
- type: init
- type: plan
apply:
- type: oidc
provider: azure
client_id: "PROD_CLIENT_ID"
tenant_id: "TENANT_ID"
subscription_id: "PROD_SUBSCRIPTION_ID"
- type: init
- type: apply
- tag_query: "dir:terraform/staging/**"
plan:
- type: oidc
provider: azure
client_id: "STAGING_CLIENT_ID"
tenant_id: "TENANT_ID"
subscription_id: "STAGING_SUBSCRIPTION_ID"
- type: init
- type: plan
apply:
- type: oidc
provider: azure
client_id: "STAGING_CLIENT_ID"
tenant_id: "TENANT_ID"
subscription_id: "STAGING_SUBSCRIPTION_ID"
- type: init
- type: apply
For more patterns, see Cloud credentials.
Next Steps
- Multi-environment: structure directories and workflows per environment
- Apply requirements: require approvals and passing checks before an apply
- Azure static credentials: the client secret alternative