The standard way most teams configure GitHub Actions to deploy to AWS is to create an IAM user, generate an access key, and paste it into GitHub Secrets as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. It works. It is also a security liability that has caused real incidents at real companies.
OIDC eliminates those credentials entirely. Here is how it works and how to set it up.
What is wrong with long-lived access keys
Long-lived AWS access keys have several properties that make them risky in CI/CD pipelines. They do not expire — once created, they are valid until explicitly rotated or deleted. They are stored as static secrets, which means they exist as plaintext values in GitHub's secrets store, in your shell history if you ever paste them, and potentially in debug logs if a workflow step accidentally prints environment variables.
They also have broad scope by default. The IAM user that owns the key may have more permissions than any single workflow needs — because it was created once and shared across multiple use cases.
How OIDC works
OpenID Connect is a standard for identity federation. Instead of giving GitHub a credential to use, you configure AWS to trust GitHub's identity provider. When a workflow runs, GitHub generates a short-lived OIDC token — a signed JWT that says "this is a workflow in repository X, on branch Y, triggered by event Z." GitHub exchanges that token with AWS for temporary credentials scoped to a specific IAM role.
Those temporary credentials expire in 15 minutes to 1 hour. There is no static secret. Nothing to rotate. Nothing to leak. If an attacker somehow intercepts a token, it is already expired by the time they could use it.
Setting it up — step by step
The trust policy — get this right
The trust policy is where most mistakes happen. Here is what a correct one looks like:
The highlighted condition is critical. It locks the role to a specific repository and branch. Without it, any public GitHub repository could assume your role. The sub claim in the OIDC token contains the repo name and ref — always validate both.
The workflow configuration
What I learned the hard way
The trust policy condition must exactly match the sub claim that GitHub sends. If your condition says refs/heads/main but the workflow runs on a pull request, the sub claim will be different and the assume-role call will fail. Test on the exact branch and trigger type you intend to use.
Also: you still need to store the CloudFront distribution ID in GitHub Secrets — but that is not a credential. It is a resource identifier. Even if it leaks, an attacker cannot do anything with a distribution ID alone. The IAM role controls what actions are permitted.
The Terraform that provisions the IAM OIDC provider and role is in the repository. The full pipeline implementation is on the CI/CD Pipeline project page.