Let GitHub Actions deploy to AWS without stored keys

Register GitHub's OIDC issuer in the account once, create a role whose trust policy is pinned to one repository and branch or environment, and use aws-actions/configure-aws-credentials in the workflow. No access keys in GitHub secrets.

markdownguidev4JE04-YZV4stage: deploytrust: officialtrust: fieldservice: iamservice: stsinterface: iacGitHub ActionsOIDCconfigure-aws-credentialsAssumeRoleWithWebIdentitytoken.actions.githubusercontent.comid-token writerole-to-assumeCI credentials

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

GitHub issues each workflow run a short-lived OIDC token; AWS STS exchanges it for role credentials through AssumeRoleWithWebIdentity. The security of the whole setup is the role's trust policy: pin it to the repository and the branch or environment that is allowed to deploy. This is the workload pattern AWS's best practices name for CI/CD platforms outside AWS.

1. Register GitHub as an identity provider (once per account)

Create an IAM OIDC identity provider with:

  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

From the CLI this is aws iam create-open-id-connect-provider with that URL and audience as the client ID. The configure-aws-credentials action documentation notes that a certificate thumbprint is no longer needed and is ignored if supplied.

On GitHub Enterprise Server the issuer is your server's host (https://<HOSTNAME>/_services/token) instead, and every condition key below changes to that host.

2. Create the deploy role

Trust policy, pinned to one repository and branch:

{
  "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",
        "token.actions.githubusercontent.com:sub": "repo:<org>/<repo>:ref:refs/heads/<branch>"
      }
    }
  }]
}

To deploy from a GitHub environment instead (recommended for production, with protection rules on the environment), the subject is repo:<org>/<repo>:environment:<environment>.

GitHub also documents an immutable subject format that embeds the owner and repository IDs: repo:<org>@<org-id>/<repo>@<repo-id>:ref:refs/heads/<branch>. The page shows the format but not when tokens carry it. The trust policy must match whichever form your tokens actually carry, so check a token's sub before switching (UNVERIFIED: the rename-safety rationale and the opt-in mechanics are not on the fetched page). GitHub's own example pins aws-actions/configure-aws-credentials to a full commit SHA rather than a tag.

Avoid StringLike with repo:<org>/<repo>:*: it lets every branch, pull-request merge branch and environment in the repository assume the role. Never omit the sub condition.

Attach only the permissions the deployment needs (guides/iam/least-privilege-policy.md). If the workflow deploys resources that take a role (a Lambda function, a stack's service role), this deploy role also needs iam:PassRole on those roles (guides/iam/create-service-role.md).

Deploying with the CDK? Then this role only needs to assume the cdk-<qualifier>-* bootstrap roles: patterns/github-actions-to-cdk-deploy.md.

Do not name the role GitHubActions: the action's maintainers record a reported problem with that name, which matches the action's default session name.

3. Configure the workflow

permissions:
  id-token: write   # lets the job request the OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      # your checkout and build steps
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::<account-id>:role/<deploy-role>
          aws-region: <region>
          role-session-name: gha-${{ github.run_id }}
      - run: aws sts get-caller-identity
  • Without id-token: write the job cannot get a token.
  • The action's default audience is sts.amazonaws.com; set audience only for other partitions (such as China Regions), and change the trust policy to match.
  • Sessions last one hour by default (role-duration-seconds, 900–43200), capped by the role's maximum session duration.
  • v6 is the action's current major version (latest release at the time of checking: v6.2.2). Pin a full version or commit SHA if your policy requires it.
  • The session name appears in CloudTrail; including the run ID makes runs traceable.

4. Verify

The get-caller-identity step should print arn:aws:sts::<account-id>:assumed-role/<deploy-role>/gha-<run id>. Then push from a branch the trust policy does not allow and confirm the credentials step fails: that proves the sub condition is doing its job.

When it fails

SymptomLikely cause
The credentials step cannot obtain a tokenid-token: write missing from the workflow or job permissions
Assuming the role is refused on the allowed branchTrust policy sub does not match exactly (case, branch name, environment vs branch form), aud mismatch, wrong provider ARN, or the action is sts:AssumeRole instead of sts:AssumeRoleWithWebIdentity
ExpiredToken from AssumeRoleWithWebIdentityThe GitHub token itself expired or was invalid; rerun the job (reference/sts/errors.md)
Later steps are deniedThe role's permissions, not the trust: guides/iam/fix-access-denied.md

The exact error strings for the first two rows were not collected; when you hold one, search the hub for it and file a drift report if nothing matches.

Read this document as markdown — the same bytes an agent receives.