← Recipes

阅读中文版本

Deploy from GitHub Actions Without Long-Lived Keys

Replace stored cloud credentials with OIDC federation, scope the trust policy to your repo and environment, and verify no secrets remain.

~30 mindeploycisecurity

Task

Configure GitHub Actions to authenticate to the cloud via OIDC so deployments need no stored secrets at all.

When to use this

A leaked personal access token with deploy rights forced a credential rotation across every service. The deploy pipeline itself was the last holder of static cloud keys.

Prerequisites

  • GitHub repository with Actions enabled and an environment defined
  • A cloud provider supporting OIDC federation, such as AWS IAM roles or Azure workload identity
  • Admin rights to create the federation trust

Steps

1. Configure the workflow to request an OIDC token.

permissions:
  id-token: write   # required for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
          aws-region: eu-central-1

Expected output: The job exchanges a short-lived OIDC token for temporary cloud credentials with no stored secret involved.

2. Scope the cloud trust policy to exactly this pipeline.

{
  "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"
      },
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:acme/app:environment:production"
      }
    }
  }]
}

Expected output: Only the production environment of that one repository can assume the role; forks and other repos are rejected at the trust boundary.

3. Verify the credentials are temporary.

aws sts get-caller-identity && aws configure list | grep -E "access_key|expiration" || echo "session credentials in use"

Expected output: The caller identity shows the assumed role, and the session expires within hours by construction.

4. Remove the now-unused static secrets.

gh secret list --repo acme/app && gh secret delete AWS_SECRET_ACCESS_KEY --repo acme/app && gh secret delete AWS_ACCESS_KEY_ID --repo acme/app

Expected output: The secret list shrinks to zero cloud credentials, eliminating the leakable artifact entirely.

5. Test the boundary with a fork-style pull request.

gh pr create --repo acme/app --title "verify oidc boundary" && gh run watch

Expected output: The deploy step fails with access denied on a pull request, confirming untrusted contexts cannot assume the role.

Troubleshooting

Problem

Not authorized to perform sts:AssumeRoleWithWebIdentity

Solution

The sub condition does not match the actual workflow path; print the token claims in a debug step and align the condition string exactly.

Problem

Jobs fail with a 403 on OIDC endpoint

Solution

The id-token write permission is missing from the workflow or the environment protection rules; add permissions and verify the job references the right environment.

Problem

Need per-environment separation

Solution

Create one role per environment and encode environment name in the sub condition, so production trust cannot be exercised by staging workflows.

Limitations

  • OIDC removes static secrets but not authorization mistakes; review role permissions as strictly as you reviewed the static keys.
  • Trust policy string matching is exact and brittle to workflow renames; alert on failed assumptions to catch drift.

The deploy key you cannot leak

Every static credential in CI is a liability with an expiration problem: it works until leaked, and rotation is manual and disruptive. OIDC federation removes the stored secret entirely. GitHub issues a short-lived signed token per job, the cloud trusts your repository identity via a scoped federation policy, and the credentials that exist are born expiring. Nothing to rotate, nothing to leak.

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
      aws-region: eu-central-1

The sub condition is the whole security model

The federation trust policy decides which GitHub contexts may assume the role. repo owner and name plus environment, as the sub claim, means a pull request from a fork, a workflow in a different repo, or even a different environment of the same repo all fail at the cloud boundary. Write the condition narrowly, then verify the narrowness with a deliberately failing run.

Delete the old secrets visibly

Migration is only complete when the static keys are gone. List the repository secrets after cutover and delete the cloud credentials in the same change that introduces OIDC. Half-migrated pipelines with both mechanisms are worse than either alone, because the leakable artifact persists while everyone believes it was removed.