Build-Test-Deploy with OIDC to Cloud
What This Concept Is
OpenID Connect (OIDC) between GitHub Actions (or GitLab, Buildkite, etc.) and your cloud provider replaces long-lived access keys with short-lived, scoped, per-workflow credentials. The pipeline asks the cloud "here is a token proving I am this repo, this workflow, this branch," and the cloud hands back temporary credentials bound to a specific IAM role for roughly one hour.
Three moving pieces, identical in shape across AWS, GCP, and Azure:
- an Identity Provider in the cloud that trusts GitHub's token issuer (
token.actions.githubusercontent.com) - a role (AWS IAM role, GCP service account, Azure managed identity or federated credential) the provider can hand short-lived credentials for
- a trust condition limiting which repos, branches, environments, or pull-request contexts can assume that role
The standards are not GitHub-specific. GitLab CI, CircleCI, and Buildkite all expose OIDC tokens; the cloud side of the setup is identical. The GitHub Actions version happens to be the most documented.
Why It Matters Here (In the Capstone)
A static access key in a CI secret is a time bomb. If the repo leaks, the org's cloud is exposed until someone finds the key and rotates it. OIDC credentials expire in an hour by default and cannot be exfiltrated to another repo, because the cloud checks the token's sub claim on every assumption. For a capstone, OIDC is also real, auditable evidence that the reviewer will recognize on sight -- the presence of a workload_identity_provider input, or an aws-actions/configure-aws-credentials@v4 block with role-to-assume and no aws-access-key-id, tells a reviewer "this author understands modern CI security."
The cost is ~30 minutes of one-time setup per cloud account. The benefit is elimination of a whole class of "rotate the CI key" chores and "key leaked on GitHub" incidents for the life of the project.
Concrete Example(s)
The AWS version (identical ideas for GCP and Azure):
- In AWS, create an IAM OIDC Identity Provider with URL
https://token.actions.githubusercontent.comand audiencests.amazonaws.com. - Create an IAM role (
capstone-deploy-role) with a trust policy constrained to your repo and ref:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/capstone:environment:prod"
}
}
}]
}
- In the workflow:
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: prod
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/capstone-deploy-role
aws-region: us-east-1
- run: aws sts get-caller-identity # sanity check: prints the assumed role
- run: terraform apply -auto-approve
The GCP variant using Workload Identity Federation:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/NNN/locations/global/workloadIdentityPools/gh-pool/providers/gh-provider
service_account: capstone-deploy@my-proj.iam.gserviceaccount.com
With a provider attribute condition limiting which repos/refs can federate:
attribute.repository == "my-org/capstone" && attribute.ref == "refs/heads/main"
Least-privilege matters: the deploy role should grant only the IAM permissions Terraform and your deploy actually need, not AdministratorAccess. Start with an explicit allowlist of actions (run.services.*, secretmanager.versions.access, iam.serviceAccounts.actAs) and loosen when a legitimate apply fails.
Common Confusion / Misconceptions
- "OIDC setup is too complex for a capstone." It is ~30 minutes of one-time work. The alternative -- committing an AWS access key to the repo secrets, rotating it every 90 days, forgetting once, and being paged at 2 a.m. because a bot cloned the repo -- is the actual complex path.
- "I'll use OIDC with
sub: repo:my-org/capstone:*." That wildcard trusts every workflow and every ref, including ones merged via a malicious PR that adds a new workflow. Always constrain to specific branches or environments in the trust condition. - "The OIDC role is the app's runtime role." No. The deploy role is assumed by CI during
apply; the app's runtime role is attached to the compute resource (Cloud Run service, ECS task, Lambda). They are different principals with different permission sets. - "
id-token: writeat workflow level is fine." Give it at the job level, not the workflow level, and only on the job that calls the cloud. Narrower scope is safer; a printf of the token in an unrelated step is a credential leak.
How To Use It (In Your Capstone)
- Create the identity provider in your cloud exactly once per cloud account.
- Create one role per environment (
capstone-deploy-prod,capstone-deploy-staging) so blast radius is scoped. - Constrain trust conditions to specific
repo:andref:orenvironment:pairs -- never wildcards. - Grant only the IAM permissions Terraform and the deploy step need. Start too restrictive and loosen when something legitimately fails.
- Put
id-token: writeat the job level, only on jobs that call cloud. - Call
aws sts get-caller-identity/gcloud auth print-identity-tokenas a sanity-check step so a broken trust condition fails loudly. - Remove every long-lived static cloud key from GitHub secrets after OIDC is live; leaving them behind is a trap.
See also (integrative)
- S9 M05 Cluster 1: Identity-centric security -- the new perimeter -- why OIDC and least-privilege are the shape of modern security
- S9 M04 Cluster 5: Pipeline security -- secrets, OIDC, least privilege -- canonical treatment; capstone is the minimal instance
- S9 M04 Cluster 3: Progressive delivery and rollback discipline -- the role scoping constrains blast radius of a bad deploy
- S9 M01 Cluster 5: IAM principals, policies, roles vs users -- understand "role" before trusting one
- S8 M04 Cluster 3: Failure modes -- cascading, correlated, gray -- a stolen long-lived key is a correlated failure across every repo with the same secret
- GitHub Docs: About security hardening with OIDC -- model and threat assumptions
- GitHub Docs: Configuring OpenID Connect in AWS -- authoritative AWS setup
- Google Cloud: Workload Identity Federation with GitHub -- GCP equivalent
- Azure: Workload identity federation for GitHub Actions -- Azure equivalent
Check Yourself
- What
subclaim does your deploy role trust, and does it include both repo and ref/environment? - What happens if a malicious PR adds a new workflow file that tries to assume the role?
- How long do the credentials handed to your workflow live, and have you confirmed that via the cloud's logs?
- Which IAM permissions does the deploy role not have, and how did you decide?
- Where is
id-token: writegranted -- workflow level, job level, or step level? - What long-lived cloud keys still exist in GitHub secrets, and what is the plan to delete them?
Mini Drill or Application (Capstone-scoped)
- Stand up OIDC (40 min). Create the identity provider, a deploy role per env, and a workflow step that calls
aws sts get-caller-identityorgcloud auth list. Make that step green before doing anything else. - Kill the old keys. Inventory every long-lived cloud key in GitHub secrets. Delete them. If something breaks, fix with OIDC, not by recreating the key.
- Trust-condition drill. Create a branch named
attacker-pr, add a workflow that tries to assume the deploy role, and confirm the cloud rejects it. This is the most valuable 10-minute exercise in this module -- it proves your trust condition is not*.
Source Backbone
Capstone deployment applies cloud, delivery, and operations material. These books are the source backbone for the delivery decisions.
- Building Secure and Reliable Systems - secure/reliable deployment posture.
- GitHub Actions in Action - workflow automation support.
- Pro Git - release history, tags, and branch discipline.
- The Linux Command Line - shell and deployment automation support.