Long-lived AWS access keys stored as CI/CD secrets are one of the most common — and most avoidable — sources of cloud credential leaks. Every static key sitting in a GitHub repository's secrets store is a standing liability: it can be exfiltrated from a compromised runner, accidentally logged, or forgotten and left active for years. AWS OIDC GitHub Actions federation solves this by letting GitHub Actions authenticate directly to AWS using short-lived, cryptographically verified tokens instead of stored credentials. In this guide, you'll configure an OpenID Connect identity provider in AWS, scope a trust policy to a specific repository and branch, wire up a GitHub Actions workflow to assume an IAM role, and verify the whole chain works — so you can eliminate long-lived AWS credentials in CI/CD entirely.
Step 1: Create the AWS OIDC GitHub Actions Identity Provider
The foundation of this setup is an IAM OIDC identity provider that tells AWS to trust tokens issued by GitHub's OIDC token service. GitHub Actions already exposes this endpoint at token.actions.githubusercontent.com — you just need to register it in your AWS account.
Using the AWS CLI:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
The thumbprint above is GitHub's current root CA fingerprint. AWS has stopped strictly validating this field for GitHub's provider (it trusts the full CA bundle automatically), but the API still requires a value, so keep it in your infrastructure-as-code for reproducibility. If you manage infrastructure with Terraform, the equivalent resource is:
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
Only one OIDC provider per URL is allowed per AWS account, so if a previous integration already created this provider, reuse it rather than erroring out on a duplicate.
Step 2: Write a Trust Policy Scoped to Your Repository
This is the step most teams get wrong, and it's the one that matters most for security. The trust policy determines which GitHub workflows are allowed to assume your role. If you scope it too broadly — for example, trusting the entire GitHub OIDC issuer without a sub condition — any repository in any GitHub organization could potentially assume your role.
A properly scoped trust policy looks like this:
{
"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:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
The sub claim is the key control here. GitHub encodes the repository, branch, environment, or pull-request context into it, so you can restrict federation to a single branch (ref:refs/heads/main), a tagged release (ref:refs/tags/v*), or a specific deployment environment (environment:production). Always pin the aud claim to sts.amazonaws.com as well — this is what prevents tokens minted for other audiences from being replayed against your AWS account.
Step 3: Create the GitHub Actions AWS IAM Role and Attach Permissions
With the trust relationship defined, create the actual GitHub Actions AWS IAM role that workflows will assume, and attach only the permissions the pipeline genuinely needs:
aws iam create-role \
--role-name github-actions-deploy-role \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name github-actions-deploy-role \
--policy-arn arn:aws:iam::123456789012:policy/deploy-scoped-permissions
Resist the temptation to attach AdministratorAccess "just to get it working." Build a scoped policy that covers exactly the actions your pipeline performs — pushing to a specific ECR repository, updating a specific ECS service, writing to a specific S3 bucket — and nothing else. Because this role is reachable from CI, it should be treated as an internet-adjacent identity and permissioned with the same rigor as a public-facing service account.
Step 4: Configure the GitHub Actions Workflow
On the GitHub side, your workflow needs permissions: id-token: write so the runner is allowed to request an OIDC token, plus the official aws-actions/configure-aws-credentials action to exchange that token for temporary AWS credentials via AssumeRoleWithWebIdentity.
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write # required to request the OIDC JWT
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy-role
aws-region: us-east-1
- name: Deploy
run: aws s3 sync ./dist s3://my-app-bucket --delete
Notice there are no AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY secrets anywhere in this file. The action requests a JWT from GitHub's OIDC provider, presents it to AWS STS, and receives back credentials that are valid for roughly one hour by default — automatically scoped to the workflow run that requested them.
Step 5: Harden the Trust Policy with Additional Conditions
Once the basic flow works, tighten it further. If your workflow only ever runs from a specific GitHub Environment with required reviewers, scope the sub condition to that environment instead of a branch:
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:environment:production"
}
You can also add an explicit deny for pull-request-triggered events if your deploy role should never be assumable from a fork's pull_request context, since forked-repo workflows have a different (and less trustworthy) token subject. Combining environment protection rules on the GitHub side with tight sub matching on the AWS side gives you two independent layers of defense against a malicious or compromised workflow file.
Step 6: Test the Federation End-to-End
Push a commit that triggers the workflow and watch the run logs. A successful configure-aws-credentials step will print the assumed role ARN without ever exposing a secret value. Confirm the identity actually in use with a quick sanity step:
- name: Verify identity
run: aws sts get-caller-identity
The output should show the assumed role's ARN, not a long-term IAM user. From there, run a deploy or read action that matches the permissions you scoped in Step 3 and confirm it succeeds — and, just as importantly, confirm an out-of-scope action (like an S3 bucket you didn't grant access to) correctly fails with AccessDenied.
Troubleshooting and Verification
"Not authorized to perform sts:AssumeRoleWithWebIdentity" — This almost always means the sub claim in your trust policy doesn't match the actual workflow context. Double-check the branch name, whether you're using StringEquals vs StringLike correctly, and whether the workflow is running from a fork (forks get a different sub and typically shouldn't be trusted).
"No OpenIDConnect provider found" — The provider ARN in your trust policy doesn't match what exists in your account, or the provider was deleted and recreated with a different ARN. Run aws iam list-open-id-connect-providers to confirm the exact ARN.
Token audience mismatch — If you're using a custom aud value instead of the default sts.amazonaws.com, make sure both the workflow's configure-aws-credentials inputs and the trust policy's aud condition agree.
Credentials work locally but not in CI — This is a strong signal that someone is still exporting static keys as a fallback. Audit your workflow and any reusable/composite actions for lingering AWS_ACCESS_KEY_ID references and remove them once OIDC is confirmed working.
Confirming no static keys remain — Search your GitHub organization's Actions secrets for anything resembling AWS key material, and cross-reference with IAM access-key last-used timestamps in AWS. Any long-lived key with recent CI activity that hasn't been touched since your migration is worth rotating or deleting outright.
How Safeguard Helps
Migrating to OIDC federation is a strong step, but it only closes one gap in the software supply chain — the workflow file, the role's permission boundary, and every action referenced in the pipeline are all still part of your attack surface. Safeguard continuously scans GitHub Actions workflows for exactly the misconfigurations that undermine OIDC federation in practice: overly permissive sub conditions, missing aud restrictions, roles with excess IAM permissions attached, and third-party actions pinned to mutable tags instead of commit SHAs.
Safeguard also gives security teams visibility across every repository and pipeline in the organization at once, flagging any workflow that still relies on long-lived AWS credentials so you can prioritize migration where the risk is highest. Once you've adopted OIDC, Safeguard continues to monitor trust policies and workflow permissions for drift — catching the moment someone loosens a sub condition to "make CI work" or grants a role broader access than the pipeline actually needs — so the security guarantees of your AWS OIDC GitHub Actions setup don't quietly erode over time.