Contents

Terraform Github AWS/Azure OIDC Authentication

What is OIDC Authentication and why do I want it.

Github supports OpenID Connect which allows GitHub to act as an identity provider.

By setting up a trust relationship in Azure/AWS to Github, you can get github to assume temporary credentials within your cloud provider to do your deployments for you. This trust relationship gives you fine-grained control of which repositories and branches can assume the role/access token.

GitHub Actions workflow
  └─ requests OIDC token from GitHub
       └─ presents token to AWS STS / Azure AD
            └─ cloud validates token against GitHub's JWKS endpoint
                 └─ returns short-lived credential (IAM role / access token)

Repository link

Prerequisites

  • Terraform cloud configured (terraform login)
    • It’s expected that the user has Org level permissions to be able to create a new project + workspaces in that project.
  • AWS CLI configured (aws login)
  • Azure CLI configured (az login)

Remote state (Terraform Enterprise / Terraform Cloud)

The workspace is auto-created on first terraform init if it does not already exist.

Edit the cloud block in aws/providers.tf / azure/providers.tf to match your organization and project:

cloud {
  hostname     = "app.terraform.io"
  organization = "my-org"
  workspaces {
    project = "oidc_base"            # auto-created on init (need org level token)
    name    = "github-oidc-aws"      # auto-created on init
  }
}

AWS module

How it works

What it creates

ResourcePurpose
aws_iam_openid_connect_providerTrusts GitHub’s OIDC issuer
aws_iam_roleRole GitHub Actions assumes
aws_iam_role_policy_attachmentAttaches managed policies to the role
aws_iam_role_policyAttaches each policies/*.json file as a separate inline policy

Inline policies (aws/policies/)

Every *.json in aws/policies/ is loaded in and attached to the role. Policy files are terraform template files that inject account_id for more secure control of resources.

At time of writing there is one policy: docker-deploy which is used to deploy resources for a post that I’m writing.

Usage

cd aws
terraform init
terraform apply

Variables

NameTypeDefaultDescription
github_orgstringrequiredGitHub organisation name (prepended to each repo in the OIDC sub condition)
github_reposlist(string)["hugoblog"]Repository names within github_org allowed to assume the role
role_namestring"github-deploy-role"Name of the IAM role
managed_policy_arnslist(string)[]Managed policy ARNs to attach
aws_regionlist(string)["us-east-1"]AWS region(s); the first element is used for the provider
tagsmap(string){}Tags applied to all resources

Outputs

NameDescription
role_arnSet as AWS_ROLE_ARN in your workflow
role_nameIAM role name
oidc_provider_arnARN of the GitHub OIDC provider
oidc_sub_conditionsList of OIDC sub conditions used in the assume-role policy
assume_role_policy_jsonFull JSON of the assume-role policy document

Tests

I haven’t included tests, the thing that I’d want to check is that SrzStephen is always in the repo but it’s a bit hard to do when you’re loading in template files the way I wanted to.

Azure module

How it works

What it creates

ResourcePurpose
azuread_applicationApp Registration that represents the identity
azuread_service_principalService principal backed by the app
azuread_application_federated_identity_credentialOne credential per repo, scoped to the main branch
azurerm_role_assignmentGrants the service principal Azure RBAC roles at subscription scope

Usage

[!Warning] If you’re using terraform enterprise a remote executor, you may run into some problems here, set your workspace executor to local by default.

cd azure
terraform init
terraform apply

Variables

NameTypeDefaultDescription
subscription_idstringrequiredAzure subscription ID
app_namestring"github-deploy"App Registration display name
github_orgstring"SrzStephen"GitHub organisation name used as the OIDC subject prefix
github_reposlist(string)["hugoblog"]Repository names within github_org to grant federated access
additional_ownerslist(string)[]Extra Azure AD object IDs to add as owners
role_assignmentslist(string)[]Azure RBAC role definition names to assign at subscription scope, e.g. ["Contributor"]

Outputs

NameDescription
client_idSet as AZURE_CLIENT_ID in your workflow
tenant_idSet as AZURE_TENANT_ID in your workflow
subscription_idSet as AZURE_SUBSCRIPTION_ID in your workflow
service_principal_object_idObject ID of the service principal
oidc_subjectsMap of repo name → federated credential subject (useful for debugging)

Tests

cd azure
terraform test

The test suite (azure/tests/oidc_trust.tftest.hcl) verifies that:

  • Every federated credential subject is scoped to the correct GitHub organisation.
  • All subjects are restricted to the main branch (ref:refs/heads/main).
  • The number of credentials matches the number of configured repositories.

GitHub Actions workflows

See .github/workflows/ for the CI workflows:

  • terraform.yml — runs on every push and PR: format check (terraform fmt -check -recursive), then terraform validate for both modules (no backend, no credentials required).
  • verify-oidc.yml — triggered on push to main, PRs, and workflow_dispatch: assumes the AWS role and logs in to Azure to confirm that OIDC authentication works end-to-end.

Required secrets

SecretUsed byDescription
AWS_ROLE_ARNverify-oidc.ymlOutput of terraform output role_arn from the AWS module
AZURE_CLIENT_IDverify-oidc.ymlOutput of terraform output client_id from the Azure module
AZURE_TENANT_IDverify-oidc.ymlOutput of terraform output tenant_id from the Azure module
AZURE_SUBSCRIPTION_IDverify-oidc.ymlOutput of terraform output subscription_id from the Azure module