4 min read

Deploy to AWS from GitHub Actions with OIDC and no static keys

Stop storing AWS access keys in GitHub. Use OpenID Connect so your workflow assumes a role and gets short-lived credentials scoped to your repo.

CI/CD and rollback designAWSGitHub ActionsTerraformDevOpsSecurity

Long-lived AWS access keys in your CI secrets are a liability. They do not rotate on their own, they leak through logs and forks, and they keep working until someone notices. OpenID Connect removes them. Your workflow proves who it is, assumes an IAM role, and gets short-lived credentials that expire on their own. This is how I deploy this site, and here is the setup end to end.

Why static keys are the problem

A stored access key is a standing grant. Anyone who reads it can use it from anywhere until you rotate it. In a public repo a leaked key can be abused within minutes. Even in a private repo, the key sits in secrets, gets injected into every run, and often carries more permission than the job needs.

OIDC replaces that standing grant with a request. GitHub issues a signed token for the specific workflow run, AWS trusts that token, and it hands back credentials that last for the job and no longer.

How OIDC works here

The flow is short.

  1. Your workflow asks GitHub for an OIDC token that describes the run, including the repository and branch.
  2. The aws-actions/configure-aws-credentials action sends that token to AWS Security Token Service and asks to assume a role.
  3. AWS checks the token against the role trust policy, then returns temporary credentials.

GitHub documents the full flow in Configuring OpenID Connect in AWS.

Add the identity provider to AWS

You register GitHub as an OIDC identity provider in IAM once per account. The provider URL is https://token.actions.githubusercontent.com and the audience is sts.amazonaws.com. The AWS guide walks through creating an OIDC identity provider.

Create a role your repo can assume

Now create a role and let only your repository assume it. The trust policy is where the security lives.

Lock the trust policy to your repo

The sub condition is the part that matters. It says which repository and branch may assume the role. Without it, any repository on GitHub could request credentials for your account.

{
  "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:OWNER/REPO:ref:refs/heads/main"
    }
  }
}

Keep the sub tight. Pin it to a branch, an environment, or a tag pattern. A loose condition like repo:OWNER/* is the most common mistake.

Use it in the workflow

Two things wire it up. You grant the job permission to request the token, and you call the credentials action with your role ARN.

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-deploy-role
          aws-region: us-east-1
      - run: aws sts get-caller-identity

id-token: write is required, or GitHub will not mint the token. The configure-aws-credentials action handles the token exchange for you.

Scope the role to the job

Short-lived credentials still carry whatever the role allows. Grant only what the deploy needs. If Terraform manages your infrastructure, the role needs the services in your plan and access to the Terraform S3 state backend. Scope actions to your resource names where the service supports it, and add a condition on iam:PassRole so the role can only pass roles to the services you expect.

Frequently asked questions

What does OIDC actually replace?

It replaces stored AWS access keys in your CI secrets. Instead of a static key, each run gets temporary credentials that expire when the job ends.

Do I still need any GitHub secrets?

Not for AWS authentication. You still store application secrets if your build needs them, but the AWS role ARN is not sensitive and can live in a repository variable.

How do I stop other repositories from assuming my role?

Use the sub condition in the trust policy to pin the role to your repository and branch. Never leave it open with a wildcard owner.

Can one role deploy to multiple environments?

You can, but separate roles per environment are safer. Give each a trust policy scoped to its branch or GitHub environment, so a change to staging cannot reach production.

Why did my workflow fail with a token error?

The most common cause is a missing id-token: write permission on the job. Without it, GitHub does not issue the OIDC token and the credentials step fails.

This pipeline deploys this site and a multi-environment Terraform platform in production.

Comments

    Leave a comment