How to Use OIDC Instead of Long Lived Cloud Keys in CI
Use OIDC in CI to get short lived cloud credentials instead of stored keys: GitHub id-token permission, AWS trust policy conditions and least privilege roles.

A cloud access key in a CI secret has three problems: it never expires, it can be copied by anyone who can read the secret, and it is rarely rotated. OIDC replaces it with a token that exists for one job. The pattern is the same in AWS, Google Cloud and Azure; this post uses AWS and GitHub Actions for the details. It is the key piece of safer deploys from CI and CI security. Syntax follows the current aws-actions/configure-aws-credentials v4.
The flow
When a job asks for it, GitHub's OIDC provider issues a JSON Web Token that says who is running: the repository, the branch or tag, the environment and the workflow. The job sends the token to the cloud's security token service. The cloud checks the signature against GitHub's public keys, then checks the claims against a trust policy you wrote, and if they match, returns credentials that expire in an hour or less. Nothing long-lived is stored anywhere.
The workflow
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-24.04
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: eu-west-1
- run: aws sts get-caller-identity
- run: ./scripts/deploy.shid-token: write allows the job to request the token; it grants no repository write access. Without the line, the login step fails with "Credentials could not be loaded" or a message about the missing token URL. Set the permission at job level, not workflow level, when only one job needs it.
The trust policy
{
"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:example-org/app:environment:production"
}
}
}]
}This is the part that must be right. The sub condition pins the role to one repository and one environment. If you leave it out, or use a wildcard such as repo:*, any repository on GitHub can assume the role. The identity provider (token.actions.githubusercontent.com) is registered once per AWS account. The sub format depends on the trigger: repo:ORG/REPO:ref:refs/heads/main for a branch, repo:ORG/REPO:ref:refs/tags/v1.0.0 for a tag, and repo:ORG/REPO:environment:production when the job names an environment, which is the safest choice since you can require approvals on the environment.
Least privilege
Attach a permissions policy that allows only what the job does: push to one registry repository, update one service, write to one bucket path. A role with administrator access defeats the purpose of narrowing the trust. Use separate roles for build and deploy, and for each environment. Session duration defaults to one hour; reduce it with role-duration-seconds for short jobs.
Other clouds
Google Cloud uses Workload Identity Federation with google-github-actions/auth and an attribute condition on the repository. Azure uses federated credentials on an app registration with the azure/login action. The ideas match: permission to request a token, a trust rule tied to the repository claims and a role with narrow rights. GitLab offers the same through id_tokens in the job definition.
Where this does not apply
Targets with no OIDC support, such as a server reached over SSH or a third-party API with only static keys, still need secrets; keep those narrow and rotated, as in rotating credentials and keys. Self-hosted runners can use OIDC but must be trusted not to leak the token, and any step in the job, including third-party actions, can request it, so pin actions to a full commit SHA.
What problem identity federation solves
A cloud access key stored in a CI secret is a long-lived credential that anyone with access to the repository settings, the CI logs or a compromised action can copy, and it keeps working until somebody notices and rotates it. Federation removes the stored secret. The CI provider issues a signed identity token for each job, stating which repository, branch, workflow and environment the job belongs to. The cloud provider is configured to trust the CI provider as an identity source and to exchange a valid token for temporary credentials, if the claims in the token match a rule you wrote. The credentials last minutes to an hour, are scoped to a role, and expire on their own. Nothing durable sits in the repository secrets, so there is nothing to leak, and every access carries the identity of the job that asked for it in the cloud's audit log.
The setup takes effort once and saves repeated rotation afterwards. It also changes how you think about trust: the question becomes which jobs may assume which roles, and that question has a precise answer in the claims.
The claims that decide who may assume a role
The token has fields such as the issuer, the audience and the subject. The subject is the important one: for GitHub Actions it looks like repo:owner/name:ref:refs/heads/main for a branch, repo:owner/name:environment:production when the job uses an environment, or repo:owner/name:pull_request for pull requests. The trust policy on the cloud side should match the subject exactly, or with a narrow pattern, and should check the audience too. The most common mistake is a wildcard that is too broad, such as every repository in an organization, which lets any repository, including a throwaway one created by a compromised account, assume the role. Another is allowing pull request subjects to reach a role with write access, so a pull request from a fork or a branch with a modified workflow can deploy. Restrict powerful roles to a protected environment or branch, give pull requests a separate read-only role, and review the policy as carefully as a firewall rule.
Least privilege and operations
The role that the job assumes should have only the permissions the job needs, on the resources it needs: push to one repository of images, write to one bucket path, update one service. Create a role per purpose and not one for all pipelines, so a flaw in one workflow cannot reach everything. Keep the session duration short, and the job requests the token at the moment it needs it, in a step that has the id-token: write permission set at the job level and not for the whole workflow. Log which role was assumed and for what, and watch the cloud audit trail for assumptions from unexpected subjects, which is how you notice a misconfigured or abused trust relationship. Other clouds work the same way with different names: Google calls it workload identity federation, and Azure calls it federated credentials. Where federation is not available, for a service that only accepts a static key, keep the key in a secret manager, scope it as narrowly as possible, rotate it on a schedule and treat its exposure as an incident.
See GitHub's OIDC hardening guide and configuring OIDC in AWS.


