---
cuppy_hub: aws
cuppy_path: guides/iam/ci-oidc-github.md
cuppy_section: docs
cuppy_doc: JE04-YZV4
cuppy_doc_uuid: 3bf83a70-ae7e-4c0d-b66b-65961bc994e8
cuppy_permalink: https://cuppy.io/aws/d/JE04-YZV4
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/iam/ci-oidc-github
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: f9b3830d237610cc053a832a935decf681e5f16d5f4f2b9c72ed79c2ad6bd71a
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Let GitHub Actions deploy to AWS without stored keys

**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:

```json
{
  "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

```yaml
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

| Symptom | Likely cause |
|---|---|
| The credentials step cannot obtain a token | `id-token: write` missing from the workflow or job `permissions` |
| Assuming the role is refused on the allowed branch | Trust 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 `AssumeRoleWithWebIdentity` | The GitHub token itself expired or was invalid; rerun the job (`reference/sts/errors.md`) |
| Later steps are denied | The 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.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
