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
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: writethe job cannot get a token. - The action's default audience is
sts.amazonaws.com; setaudienceonly 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. v6is 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.