Blog / GitHub Actions OIDC
DevOps

Keyless AWS deployments with GitHub Actions OIDC

Vishwaraja Pathi · February 25, 2026 · 7 min read

Somewhere in your GitHub repository settings, there's a pair of secrets named AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. They belong to an IAM user that was created months or years ago. Nobody remembers what permissions it has. The credentials have never been rotated. And they have full access to your production AWS account.

This is the state of CI/CD credentials at most organizations we work with. It's not that teams don't understand the risk -- it's that long-lived keys are easy, they work, and replacing them has historically been more effort than anyone wants to budget for. But that calculus changed when GitHub added native OIDC support to Actions. The setup is straightforward, the Terraform is minimal, and the result is zero static credentials anywhere in your pipeline.

How OIDC federation works

The core idea is simple. Instead of giving GitHub a set of AWS keys, you tell AWS to trust GitHub as an identity provider. When a GitHub Actions workflow runs, GitHub issues a short-lived JSON Web Token (JWT) that contains claims about the workflow -- which repository triggered it, which branch, which environment, the workflow name, and more.

Your workflow then presents this token to AWS STS via the AssumeRoleWithWebIdentity API. AWS validates the token against GitHub's OIDC provider, checks that the claims match the conditions on your IAM role, and issues temporary credentials that expire in an hour (or whatever duration you configure). No static keys involved at any step.

The security improvement is significant. There are no credentials to leak, no keys to rotate, and the blast radius of a compromised workflow is limited to whatever the IAM role allows -- scoped to a specific repo and branch.

Terraform setup

You need two resources: an OIDC provider (created once per AWS account) and an IAM role for each repository or workflow that needs access.

The OIDC provider

This tells AWS to trust tokens issued by GitHub:

resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

You create this once. Every repository in your GitHub organization can use the same provider. The thumbprint is GitHub's OIDC server certificate fingerprint -- AWS uses it to verify the TLS connection to GitHub's token endpoint.

The IAM role

This is where the scoping happens. The trust policy on the role determines which repositories, branches, and workflow contexts are allowed to assume it:

resource "aws_iam_role" "github_actions_deploy" {
  name = "github-actions-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow"
      Principal = {
        Federated = aws_iam_openid_connect_provider.github.arn
      }
      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"
        }
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "deploy_permissions" {
  role       = aws_iam_role.github_actions_deploy.name
  policy_arn = aws_iam_policy.deploy.arn
}

The Condition block is the critical part. The sub claim in the JWT contains the repository and ref that triggered the workflow. By matching on repo:your-org/your-repo:ref:refs/heads/main, you ensure that only workflows triggered by pushes to the main branch of that specific repository can assume this role.

Attach whatever IAM policies the deployment actually needs -- ECR push, ECS deploy, S3 sync -- and nothing more. This role should be purpose-built for the pipeline, not a reused "admin" role.

The workflow

On the GitHub side, the workflow change is minimal. You replace your static credential configuration with the aws-actions/configure-aws-credentials action and tell it to assume a role:

name: Deploy
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1

      - name: Deploy
        run: |
          aws ecs update-service --cluster prod --service api --force-new-deployment

Two things to note. First, the permissions block at the top is required -- id-token: write grants the workflow the ability to request a JWT from GitHub's OIDC provider. Without it, the token request silently fails and the credential action gives you a confusing error. Second, there's no aws-access-key-id or aws-secret-access-key input. The action handles the entire OIDC exchange internally.

Scoping it down properly

The most common mistake we see with OIDC federation is making the IAM role's trust policy too broad. A few patterns to avoid:

  • Don't use StringLike with repo:your-org/* -- this lets every repository in the organization assume the role. Fine for a read-only role, dangerous for anything with write access.
  • Don't omit the branch condition -- without ref:refs/heads/main, any branch (including feature branches from forks in public repos) can trigger the assumption.
  • Consider using GitHub Environments -- the sub claim changes to repo:your-org/your-repo:environment:production when the job references a GitHub Environment, which gives you an additional layer of gating with required reviewers and wait timers.

For organizations with multiple repositories deploying to the same AWS account, we create one IAM role per repository (or per repository-environment pair). The naming convention is straightforward: github-actions-{repo}-{environment}. Each role gets only the permissions that specific pipeline needs.

One piece of a larger puzzle

OIDC federation eliminates static credentials from your CI/CD pipeline, which removes one of the most common and most easily exploited attack vectors. But it's one piece of a secure deployment pipeline, not the whole thing. You still need to think about artifact signing, deployment approval gates, audit logging, and least-privilege IAM policies that actually match what the pipeline does rather than what was convenient when someone first set it up.

The setup we've walked through here takes about thirty minutes if you already have Terraform in place. For a net-new pipeline design -- including environment promotion, rollback strategy, and proper IAM boundaries -- reach out to us. We design deployment pipelines that teams actually maintain, not ones that rot the moment the original author leaves.

V

Vishwaraja Pathi

Cloud & DevOps specialist with 13+ years of experience. Founder of Adiyogi Technologies. Previously at Roku, Rocket Lawyer, and BetterPlace.

More from the blog