← Back to list

Migrating AWS access keys & secrets in GitHub Actions to OIDC

If you’ve got AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY sitting in your GitHub repository secrets right now, this one's for you.

Shikha Singh · 2026-04-22 12:49 · 0 claps · 4.2 min read paywalled
#devops #aws #ci-cd-pipeline #security #github-actions
Open on Medium ↗
Wiki topics: ☁️ · DevOps & Cloud 🔓 · Open Source

Migrating AWS access keys & secrets in GitHub Actions to OIDC

If you’ve got AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY sitting in your GitHub repository secrets right now, this one's for you.

A few weeks ago, I audited our CI/CD setup and noticed something uncomfortable — 28 of our GitHub Actions workflows were authenticating to AWS using long-lived access keys stored as repo secrets.

The quiet risk of long-lived keys

Long-lived AWS access keys/secrets works quite fine but are a pain during rotation. Frequent passwords were exchanged during rotation, causing a security risk.

That was exactly the problem.

The better answer has existed for a while: OpenID Connect (OIDC). GitHub Actions exchanges a short-lived identity token directly with AWS, & AWS hands back temporary credentials scoped to a specific IAM role. Tokens expire in 1 hr by default. Your repo no longer holds the keys.

Here’s what the migration actually looks like:-

The shape of the migration

Three things to set up:

  1. An OIDC provider in AWS that trusts GitHub’s token issuer
  2. An IAM role with a trust policy scoping which GitHub repos / branches / environments can assume it
  3. A small change to each workflow to request and use the short-lived credentials

The first two are one-time AWS setup. The third is where most of the work lives if you have a lot of workflows.

Step 1: trust GitHub in AWS

In IAM, register GitHub’s OIDC provider once per AWS account:

Two minutes in the console, or a few lines of Terraform.

Step 2: create the IAM role

The trust policy is where the security lives. It says “GitHub Actions can assume this role, but only from these specific places”:

{
  "Version": "2012-10-17",
  "Statement": [{
    "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 claim is the important bit. You can narrow it to specific branches, tags, or environments:

  • repo:your-org/your-repo:ref:refs/heads/main — only the main branch
  • repo:your-org/your-repo:environment:production — only a specific environment
  • repo:your-org/your-repo:* — any workflow in the repo (convenient, less scoped)

Pair the trust policy with a permissions policy that grants only the AWS actions the workflows actually need. This is your chance to aggressively apply least privilege. In our case, most workflows only needed s3:GetObject for ML models; a subset needed write access to a specific cache prefix; one needed Device Farm permissions for mobile tests. Three scopes, not "admin."

Step 3: update the workflows

Before:

env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Download models
        run: aws s3 cp s3://my-bucket/models/ ./models/

After:

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@<pinned-sha>
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/github-actions-role
          aws-region: eu-central-1
      - name: Download models
        run: aws s3 cp s3://my-bucket/models/ ./models/

Three real changes:

  1. id-token: write is mandatory — without it, GitHub won't mint the OIDC token.
  2. The env block with static credentials is gone.
  3. An auth step runs before any AWS call and populates the standard AWS environment variables with temporary credentials.

Known issues-

Problem1 : When 1 hour isn’t enough

This one didn’t show up during the migration. It showed up weeks later, after a mobile test suite started failing at oddly consistent times.

The default session lifetime from aws-actions/configure-aws-credentials is 1 hour. For most CI jobs that's plenty. For anything that kicks off long-running work and then polls for results — AWS Device Farm test runs, SageMaker training jobs, CodeBuild triggers, big ML pipelines — it's not.

The failure mode is always the same: the job runs past the 60-minute mark, the next AWS API call throws ExpiredTokenException, and the pipeline dies holding the bag.

Fixes-

The fix is two-part, and both parts are required.

Part 1— on the AWS side, the IAM role itself has a MaxSessionDuration attribute, and it defaults to 1 hour (3600s). If your workflow asks for more than the role allows, STS refuses with —

The requested DurationSeconds exceeds the MaxSessionDuration set for this role.

Solution-

Bump the role’s max session duration. Console path: IAM → Roles → (your role) → Edit → Maximum session duration. Or via CLI:

aws iam update-role \
  --role-name github-actions-my-app \
  --max-session-duration 7200

Part 2— on the workflow side, tell configure-aws-credentials you want a longer session:

- name: Configure AWS credentials via OIDC
  uses: aws-actions/configure-aws-credentials@<pinned-sha>
  with:
    role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/github-actions-my-app
    aws-region: us-west-2
    role-duration-seconds: 7200  # 2 hours for device farm test monitoring

The trap is that these are two separate settings in two separate places, and the error message only tells you about one of them at a time. Change only the workflow and you get the DurationSeconds exceeds MaxSessionDuration error. Change only the role and STS just gives you the default 1-hour token anyway, and your job still dies at the 60-minute mark.

Match both sides. Then move on.

Problem2: Forgetting the permissions block

The default permissions for GITHUB_TOKEN don't include id-token: write. Skip it and you get a confusing "not authorized" error that looks like an AWS problem but is actually a GitHub one.

Scoping the IAM Role too tightly/loosely- For IAM roles, its advisable to tighten the permissions but the role should have enough permissions to perform required actions on AWS as per workflow requirements.

Was it worth it?

Migrating 28 workflows wasn’t glamorous work. The result is a CI system where:

  • No AWS credentials are stored anywhere in GitHub
  • Every AWS API call from CI has a clear AssumeRoleWithWebIdentity audit trail
  • Permissions are scoped to exactly what each workflow needs
  • Rotation is automatic — there’s nothing to rotate

The piloting pattern worked well: start with one simple, non-critical workflow, prove the end-to-end flow, then roll out in batches. If you’re staring at a similar pile of workflows, don’t try to do them all at once

Have you done this migration? Hit a gotcha I didn’t cover? Drop it in the comments — I’d like to keep collecting them.


메타데이터
post_id
874b08f59fc0
slug
migrating-aws-access-keys-secrets-in-github-actions-to-oidc-874b08f59fc0
url
https://medium.com/@shiqs90/migrating-aws-access-keys-secrets-in-github-actions-to-oidc-874b08f59fc0
canonical_url
https://medium.com/@shiqs90/migrating-aws-access-keys-secrets-in-github-actions-to-oidc-874b08f59fc0
author_url
https://medium.com/@shiqs90
status
ok
fetched_at
2026-06-15 22:55:51