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.
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/appExpected 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 watchExpected 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.