If you've been deploying to AWS from GitHub Actions using access keys stored in GitHub Secrets, you're doing it the old way. It works - but it carries risks that are easy to avoid. OIDC (OpenID Connect) is the modern replacement, and once you understand how it works, you won't go back.
The traditional approach looks like this:
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=abc123...
You generate these in IAM, paste them into GitHub Secrets, and your workflow uses them to authenticate. Simple enough. But here's what you're actually dealing with:
Keys never expire by default. Unless you manually rotate them, those credentials are valid forever. One leak and someone has permanent access to your AWS account.
They live in too many places. GitHub Secrets, your local machine, your CI logs if you're not careful, maybe a Slack message from a coworker. Keys have a way of spreading.
Rotation is painful. Rotating means generating new keys, updating every secret in every repo that uses them, and hoping nothing breaks. Teams put it off. That's how 2-year-old keys end up in production.
Blast radius is hard to limit. A leaked key gives an attacker persistent, patient access. They can enumerate your resources, exfiltrate data, or wait for the right moment.
OIDC replaces the key exchange with a trust relationship. Instead of "here are my credentials", the workflow says "here's a signed token proving I am who I say I am."
Step by step:
GitHub generates a short-lived JWT token when the workflow runs. This token is cryptographically signed by GitHub's identity provider and contains claims like:
The workflow sends this token to AWS STS (Security Token Service) and says: "I want to assume role X."
AWS checks:
The workflow uses those temporary credentials to deploy, then they expire automatically. Nothing to clean up.
When you create the IAM role, you define who can assume it:
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID: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:*"
}
}
}
The sub condition is the key part. It means only workflows running from your-org/your-repo can assume this role. You can tighten it further:
"repo:your-org/your-repo:ref:refs/heads/main"
Now only the main branch can deploy. A PR branch cannot assume the role even if someone tries to trigger it.
Before (static keys):
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
After (OIDC):
permissions:
id-token: write
contents: read
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/your-role-name
aws-region: us-east-1
No secrets to store. No keys to rotate. No keys to leak.
The permissions block is required - it tells GitHub to generate an OIDC token for this workflow. Without it, the token is not issued.
Two things:
1. OIDC Identity Provider Register GitHub as a trusted identity provider in IAM:
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list sts.amazonaws.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
This is a one-time setup per AWS account. It tells AWS: "I trust JWT tokens signed by GitHub's identity server."
2. IAM Role with a trust policy Create a role whose trust policy references the OIDC provider and scopes access to your specific repo/branch (as shown above). Attach whatever permissions the deployment needs - S3, CloudFront, Secrets Manager, etc.
| Static Keys | OIDC |
|---|---|
| Credentials never expire | Credentials expire in ~1 hour |
| Stored in GitHub Secrets | Nothing stored anywhere |
| Manual rotation required | No rotation needed |
| Leaked key = permanent access | Leaked token = useless after expiry |
| Hard to audit which key did what | Every assume-role is logged in CloudTrail |
| Scoped to nothing | Scoped to specific repo + branch |
| Works from anywhere | Only works from GitHub Actions |
Every time the workflow assumes the role, AWS CloudTrail logs it with the full OIDC claims - including the repo, branch, workflow file, and commit SHA. You get a complete audit trail of every deployment without any extra tooling.
With static keys, you know the key was used. With OIDC, you know exactly which workflow run used it and from which branch.
OIDC is not just more secure - it's simpler to maintain. No secrets to manage, no rotation schedule, no risk of a leaked key sitting in an old Slack thread. The setup takes 10 minutes and the trust relationship is self-documenting in your IAM role policy.
If you're still using static keys for AWS deployments from GitHub Actions, there's no good reason not to switch.