🧭 How toCloudIntermediate✨ AI-assisted

How to Deploy to AWS From GitHub Actions Without Stored Keys

WittyTech··2 min read
#github-actions#aws#oidc

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 sub condition such as repo: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.

← More in Cloud