Skip to main content

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):

  1. In AWS, create an IAM OIDC Identity Provider with URL https://token.actions.githubusercontent.com and audience sts.amazonaws.com.
  2. 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"
}
}
}]
}
  1. 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: write at 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)

  1. Create the identity provider in your cloud exactly once per cloud account.
  2. Create one role per environment (capstone-deploy-prod, capstone-deploy-staging) so blast radius is scoped.
  3. Constrain trust conditions to specific repo: and ref: or environment: pairs -- never wildcards.
  4. Grant only the IAM permissions Terraform and the deploy step need. Start too restrictive and loosen when something legitimately fails.
  5. Put id-token: write at the job level, only on jobs that call cloud.
  6. Call aws sts get-caller-identity / gcloud auth print-identity-token as a sanity-check step so a broken trust condition fails loudly.
  7. Remove every long-lived static cloud key from GitHub secrets after OIDC is live; leaving them behind is a trap.

See also (integrative)

Check Yourself

  1. What sub claim does your deploy role trust, and does it include both repo and ref/environment?
  2. What happens if a malicious PR adds a new workflow file that tries to assume the role?
  3. How long do the credentials handed to your workflow live, and have you confirmed that via the cloud's logs?
  4. Which IAM permissions does the deploy role not have, and how did you decide?
  5. Where is id-token: write granted -- workflow level, job level, or step level?
  6. What long-lived cloud keys still exist in GitHub secrets, and what is the plan to delete them?

Mini Drill or Application (Capstone-scoped)

  1. Stand up OIDC (40 min). Create the identity provider, a deploy role per env, and a workflow step that calls aws sts get-caller-identity or gcloud auth list. Make that step green before doing anything else.
  2. 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.
  3. 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.