Every long-lived cloud credential stored as a repository secret is a ticking time bomb. It does not expire on its own, it grants exactly the same permissions whether a workflow run legitimately needs them or a compromised dependency is exfiltrating them, and once it leaks, someone has to notice, rotate it, and hope nothing happened in between. OpenID Connect, or OIDC, removes that entire category of risk by letting a GitHub Actions workflow request short-lived, scoped credentials directly from AWS or Google Cloud at run time, with no stored secret at all.
This post walks through how the federation actually works under the hood, how to wire up AWS IAM and GCP Workload Identity Federation from scratch, how to read and scope the subject claim that GitHub embeds in every token, and the specific misconfigurations that quietly turn a keyless setup into a bigger blast radius than the static keys it replaced.
The Problem With Long-Lived Cloud Keys
Most teams that have not adopted OIDC store an AWS access key pair or a GCP service account JSON key as an encrypted GitHub secret, then reference it in every workflow that needs to touch the cloud. It works, but it accumulates risk in ways that are easy to underestimate until an incident forces the issue.
- The key has no natural expiry, so a leak from a build log, a misconfigured artifact, or a compromised third-party action stays exploitable indefinitely until someone manually rotates it.
- Rotation is a manual, disruptive process that teams postpone, which means keys that should live for ninety days often live for years.
- A single key typically grants the same permissions to every workflow and every branch, so a low-risk documentation build can carry the same blast radius as a production deployment.
- Secret scanning catches some leaks after the fact, but it cannot prevent the exposure window between a key ending up somewhere it should not and someone noticing.
How GitHub Actions OIDC Federation Works
When a workflow requests it, GitHub's OIDC provider mints a signed JSON Web Token that describes exactly who is asking: which repository, which branch or tag, which environment, which event triggered the run, and a short validity window measured in minutes. This token is never a long-lived secret. It is generated fresh for that specific job and expires almost immediately after.
The cloud provider never has to trust GitHub blindly. Instead, you configure AWS or GCP ahead of time to trust GitHub's OIDC issuer as an identity provider, then write a trust policy or attribute condition that only accepts tokens matching specific claims, most importantly the subject claim. When the workflow runs, the cloud SDK or action presents the token, the provider validates its signature against GitHub's published public keys, checks the claims against your trust policy, and if everything matches, hands back temporary, scoped credentials that expire in an hour or less.
- The workflow declares the id-token write permission, which allows the job to request an OIDC token from GitHub's provider at token.actions.githubusercontent.com.
- An action such as configure-aws-credentials or google-github-actions/auth requests the token and sends it to the cloud provider along with the role or service account you want to assume.
- The cloud provider verifies the token's signature against GitHub's public JSON Web Key Set and checks the audience, issuer, and subject claims against the trust policy you configured in advance.
- If the claims satisfy the policy, the provider issues short-lived temporary credentials scoped to whatever role or service account you designated, and those credentials are used for the rest of the job.
Treat the subject claim the same way you would treat a firewall rule. Narrow it to the exact repository, branch, or deployment environment that should be allowed to assume a given role, rather than trusting an entire organization or every branch in a repository.
Federating GitHub Actions With AWS
Setting up AWS starts with registering GitHub's OIDC issuer as an identity provider in IAM, using the issuer URL and a thumbprint or the built-in GitHub provider that AWS now recognizes automatically. Once that provider exists, you create an IAM role that trusts it, and the trust policy is where the actual security boundary lives.
The example below restricts the role to a single repository and a single deployment environment named production, which means a pull request build from a fork, or even a push to a feature branch in the same repository, cannot assume this role. The audience condition also confirms the token was actually issued for AWS and not replayed from a token minted for a different provider.
{
"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:my-org/my-repo:environment:production"
}
}
}
]
}Attach this trust policy to an IAM role, then reference that role's ARN as role-to-assume in the aws-actions/configure-aws-credentials step of your workflow, alongside the id-token write permission at the job or workflow level. No access key, no secret key, and no long-lived credential ever touches your repository secrets.
Federating GitHub Actions With Google Cloud
Google Cloud uses Workload Identity Federation, which is conceptually similar but split into three pieces: a Workload Identity Pool that acts as a container for external identities, a Workload Identity Provider inside that pool configured with GitHub's issuer and an attribute mapping, and a service account that the federated identity is allowed to impersonate.
The attribute mapping step is where most of the security work happens. GitHub's OIDC issuer is shared across every GitHub organization in existence, so Google Cloud requires an attribute condition that filters incoming tokens to your specific organization or repository, otherwise any GitHub repository anywhere could theoretically attempt to federate against your pool. A typical condition checks that the repository owner attribute matches your organization name exactly.
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/gh-pool/providers/gh-provider
service_account: deployer@my-project.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud run deploy my-service --region us-central1Grant the service account the roles/iam.workloadIdentityUser role scoped to the specific provider, then reference the full provider resource path and the service account email in the google-github-actions/auth step shown above. Just as with AWS, this removes the need to ever generate, store, or rotate a service account key file.
Subject Claims Cheat Sheet
GitHub formats the subject claim differently depending on what triggered the workflow, and getting this format wrong is the single most common reason a trust policy silently fails to match. The table below covers the formats you will use most often when scoping a role or provider to a specific trigger type.
| Trigger scope | Subject claim format |
|---|---|
| Push to a specific branch | repo:org/repo:ref:refs/heads/branch-name |
| Push of a version tag | repo:org/repo:ref:refs/tags/tag-name |
| Deployment to a named environment | repo:org/repo:environment:environment-name |
| Pull request events | repo:org/repo:pull_request |
Misconfigurations to Avoid
OIDC federation removes the risk of a leaked static key, but a sloppy trust policy can quietly recreate the same blast radius, or a worse one, in a different form. These are the mistakes that show up most often in real audits.
- Using a wildcard subject claim such as matching every branch or every repository in an organization, which effectively lets any workflow in any repository assume a production-grade role.
- Forgetting the audience condition entirely, which means a token minted for a completely different relying party could theoretically be replayed against your role or provider.
- Scoping the trust policy to a branch name instead of a protected GitHub environment, since branch names can be created by anyone with write access while environments can enforce required reviewers and deployment branch restrictions.
- Granting the federated role or service account broad, long-standing permissions instead of the minimum needed for that specific pipeline, which turns a narrow identity boundary into a wide one the moment the trust policy has any gap.
- Assuming pull requests from forked repositories automatically get the same OIDC token access as pushes to the base repository, when GitHub deliberately withholds token permissions and secrets from most fork-originated pull request runs for exactly this reason.
Never scope a trust policy using only the repository name without also constraining the ref, environment, or actor. GitHub's issuer is shared across millions of repositories, and a subject claim that only checks the repository name without the org prefix or ref suffix can match far more workflows than intended.
Migration Checklist
Moving an existing pipeline from static keys to OIDC is a mechanical process once the identity provider and trust policy are in place. Follow this order to avoid a broken deploy in the middle of the migration.
- Create the OIDC identity provider in AWS IAM or the Workload Identity Pool and Provider in Google Cloud, pointing at GitHub's issuer URL.
- Write a narrowly scoped trust policy or attribute condition that matches only the repository, branch, tag, or environment that pipeline actually needs.
- Add the id-token write permission to the workflow file and swap the credential-setting step for configure-aws-credentials or google-github-actions/auth configured for OIDC.
- Run the updated workflow on a non-production branch first and confirm the cloud provider issues temporary credentials before touching anything that matters.
- Once the OIDC path is confirmed working, delete the static access key or service account key from the cloud provider and remove the corresponding secret from the repository.
Keyless authentication is not just a security upgrade, it is also less operational overhead in the long run: no more quarterly key rotation reminders, no more wondering which pipeline a given access key belongs to, and a trust policy that documents, in plain text, exactly which workflows are allowed to touch which cloud resources.

