GitHub Actions OIDC: Keyless Cloud Auth for CI/CD

Photo by Yu. Samoilov on flickr
GitHub Actions OIDC lets a workflow request a short-lived, signed identity token from GitHub at run time and exchange it for temporary credentials from AWS or Google Cloud, instead of relying on a long-lived access key stored as a repository secret. It removes the need to store, rotate, or worry about leaking static cloud credentials, and every credential it issues expires automatically within an hour or less.
You register GitHub's OIDC issuer as an identity provider in AWS IAM, then create an IAM role with a trust policy that checks the audience claim against sts.amazonaws.com and the subject claim against your specific repository, branch, or environment. The aws-actions/configure-aws-credentials action then requests the OIDC token and exchanges it for temporary credentials scoped to that role.
This happens when the attribute condition on your Workload Identity Provider is missing or too broad. GitHub's OIDC issuer is shared across every organization on the platform, so Google Cloud requires you to add an explicit attribute condition, typically checking that the repository owner attribute matches your organization name, or any GitHub repository could theoretically attempt to federate into your pool.
By default, no. GitHub deliberately restricts token permissions, including the ability to request an OIDC token, for workflow runs triggered by pull requests from forked repositories, since the workflow code itself is not trusted in that context. This is intentional and should not be worked around without a very deliberate reason.
The most common mistake is scoping the subject claim too broadly, for example matching an entire organization or every branch in a repository instead of a specific repository, branch, tag, or protected environment. A second frequent mistake is omitting the audience condition entirely, which weakens the verification that the token was actually issued for your specific cloud provider.

Photo by Yu. Samoilov on flickr
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.
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.
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.
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.
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.
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: [email protected]
- 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.
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 |
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.
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.
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.
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.