How to Deploy to AWS From GitHub Actions Without Stored Keys
Many pipelines still deploy to AWS with an access key pair saved as a GitHub secret. Those keys rarely get rotated, and anyone who can edit a workflow can use them. GitHub's OpenID Connect (OIDC) support replaces them with short-lived credentials issued for each run.
How it works
GitHub issues a signed token for each workflow run. AWS checks the token against an identity provider you configure. If the token matches the conditions in a role's trust policy, AWS returns temporary credentials that expire within the hour.
Step 1: Add the identity provider
In IAM, add an OpenID Connect provider with the URL https://token.actions.githubusercontent.com and the audience sts.amazonaws.com. You only need one per AWS account.
Step 2: Create a role with a narrow trust policy
The trust policy decides which repositories and branches may use the role. The key statement looks like this:
{
"Effect": "Allow",
"Principal": {"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"},
"StringLike": {"token.actions.githubusercontent.com:sub": "repo:acme/ai-service:ref:refs/heads/main"}
}
}
Attach only the permissions the deployment needs.
Step 3: Use the role in the workflow
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/ai-service-deploy
aws-region: us-east-1
Later steps can use the AWS CLI and SDKs as usual.
Step 4: Remove the old keys
Delete the access key secrets from GitHub and deactivate the keys in IAM. Watch CloudTrail for a few days to confirm nothing else was still using them.
Step 5: Check that it worked
Add a step that runs aws sts get-caller-identity. The output should show the assumed role with a session name, not an IAM user. If the step fails with an access denied error, compare the sub value in the error with the condition in the trust policy. A typo in the repository or branch name is the usual cause.
Things to watch
- A loose
subcondition such asrepo:acme/*lets every repository in the organization assume the role. Be specific. - Pull requests from forks shouldn't be able to deploy. Limit the condition to protected branches or GitHub environments.
- Separate roles for staging and production let you require an approval before production deployments.
Start with one repository and one role, then apply the same pattern to the rest.