Two directions of trust#

Connecting a Git provider to the cloud involves two separate trust relationships. In one, an external system (your CI, a deployment platform) needs to read your repositories. In the other, that system needs to act in your AWS account: push images, update services or sync a bucket.

For years the standard recipe was a personal SSH key for Git plus an IAM user's access key pair pasted into the platform or the CI secrets. It works, but it leaves long-term credentials scattered across several systems.

This guide covers the modern alternative for each direction: OpenID Connect (OIDC) so GitHub Actions gets temporary AWS credentials without storing any key, and read-only keys or GitHub Apps for minimal repository access.

Why avoid access keys#

Access keys are long-term credentials tied to an IAM user: an Access key ID and a Secret access key that never expire on their own. AWS's best practice is to use temporary credentials, such as IAM roles, instead of creating access keys, and to review the alternatives before generating them.

  • If they leak (in a log, a fork or a screenshot), they keep working until someone deactivates them.
  • They must be rotated by hand, and AWS only shows the secret once, at creation.
  • Every system that stores them is one more place to protect.

With OIDC, each workflow run receives a token signed by GitHub, exchanges it for AWS credentials that are valid for a limited time, and leaves nothing to rotate.

Documentation: AWS IAM · Access keys ↗ · AWS IAM · Managing access keys ↗ · AWS IAM · Best practices ↗

How OIDC works between GitHub and AWS#

  • The GitHub Actions job asks GitHub for a JWT; it needs the id-token: write permission to do so.
  • The token carries claims about who is asking, such as the repository, branch or environment in the sub claim, and the audience in aud.
  • The configure-aws-credentials action presents the token to AWS STS to assume a role (AssumeRoleWithWebIdentity).
  • IAM compares the claims with the conditions in the role's trust policy. If they match, STS returns temporary credentials; if not, the job fails.

Security rests entirely on those conditions. GitHub requires at least one so untrusted repositories cannot obtain tokens for your cloud, and IAM recommends always evaluating the sub claim.

Documentation: GitHub · About OIDC ↗ · GitHub · OIDC with AWS ↗

Configure AWS: provider and role#

First add GitHub as an OIDC identity provider in IAM, with the URL https://token.actions.githubusercontent.com and the audience sts.amazonaws.com (the one the official action uses). You do this once per account.

Then create a role whose trust policy pins the organization, the repository and, ideally, the GitHub environment:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:prod"
        }
      }
    }
  ]
}
Trust policy based on the AWS and GitHub examples. With an environment, sub looks like repo:ORG/REPO:environment:NAME; without one, repo:ORG/REPO:ref:refs/heads/BRANCH.

Avoid broad wildcards such as repo:my-org/* unless you truly want every repository in the organization to assume the role. Attach only the deployment's permissions to the role, and use a separate role per environment.

Watch for a recent change: in repositories created after July 15, 2026, or that have opted in, the sub claim includes immutable owner and repository IDs (for example repo:my-org@123456/my-repo@456789:...). Check which format your repository uses before writing the condition.

Documentation: AWS IAM · Create an OIDC provider ↗ · AWS IAM · Role for OIDC and GitHub ↗ · GitHub · Immutable OIDC subject claims ↗

The GitHub Actions workflow#

yaml
# .github/workflows/deploy.yml
name: deploy
on:
  push:
    branches: [main]

permissions:
  id-token: write   # lets the job request the OIDC token (grants no write access)
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: prod   # must match the sub in the trust policy
    steps:
      - uses: actions/checkout@v7
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::111122223333:role/github-deploy-prod
          aws-region: us-east-1
      - run: aws sts get-caller-identity   # confirms the assumed role
Minimal workflow. Replace the role ARN and region, then add your build and deploy steps.

If you configure the prod environment in GitHub with protection rules, such as required reviewers or allowed branches, that approval also becomes a prerequisite for getting production credentials, because the role only accepts tokens from that environment.

Documentation: configure-aws-credentials ↗ · GitHub · Environments ↗ · AWS CLI · sts get-caller-identity ↗

Common errors#

  • Not authorized to perform sts:AssumeRoleWithWebIdentity: nearly always a sub mismatch. A job with an environment emits environment:NAME, not ref:refs/heads/BRANCH.
  • The job cannot request the token: permissions id-token: write is missing at workflow or job level.
  • Trust policy with only aud: any GitHub repository could try to assume the role. Always add sub.
  • Workflows using pull_request_target or non-ephemeral runners: the action's README warns to be especially careful, since they can run untrusted code with access to credentials.

Documentation: GitHub · OIDC with AWS ↗ · configure-aws-credentials ↗

The other direction: read access to your repositories#

If an external platform needs to clone your repositories, give it minimal access. The main options, from broadest to narrowest:

OptionScopeNotes
SSH key on your personal accountEverything your user can read and writeTesting only; breaks when that person leaves the organization.
Machine user (GitHub)Repositories assigned in the organizationOnly organizations can restrict it to read-only; GitHub allows one such account for automation.
Deploy key (GitHub, GitLab) or access key (Bitbucket)One repository (Bitbucket: several)Can be read-only; Bitbucket access keys are always read-only.
GitHub AppRepositories where it is installed, with fine-grained permissionsInstallation tokens expire after 1 hour; it does not consume a user seat.

When you register a public key handed to you by a platform, confirm it is the right one by comparing its fingerprint with the one your provider displays: save it to a file and run ssh-keygen -lf file.pub.

Documentation: GitHub · Deploy keys and machine users ↗ · GitHub · When to build a GitHub App ↗ · GitHub · Installation access tokens ↗ · Bitbucket · Access keys ↗ · GitLab · Deploy keys ↗ · OpenSSH · ssh-keygen ↗ · GitHub · Reviewing SSH keys ↗

Checklist#

  • GitHub OIDC provider created in every AWS account that receives deployments.
  • One role per environment, with aud and sub pinned to repository and environment.
  • Role permissions limited to what the deployment does.
  • No AWS access keys in repository secrets.
  • Read access to repositories through deploy keys, access keys or a GitHub App, not personal keys.

Sources and scope

Documentation checked on September 25, 2026. Examples and decision criteria are editorial proposals; adapt them to your application's contract and validate them in an authorized test environment.

From design to decision

Compare cloud options

Review pricing, limits, conditions and sources for each option (in Spanish).

Open comparison