Last spring we removed the last long lived AWS keys from GitHub. Each deploy workflow now asks GitHub for an OIDC token, with permissions set to id-token: write, and the configure-aws-credentials action exchanges it for an hour of credentials on an IAM role. The roles come from a shared Terraform module, and their trust policies accept tokens from GitHub's provider, token.actions.githubusercontent.com, with an audience of sts.amazonaws.com.

On a Thursday afternoon in October our public tracking site was replaced by a page of hackathon slides for twenty five minutes. A team in the company hackathon had copied our site's deploy workflow into their own repository as a starting point, role ARN and all. Their first push to main ran aws s3 sync with --delete against our production bucket, and AWS let it.

The trust policy said why. Besides the audience, it had one condition on the subject claim, StringLike on token.actions.githubusercontent.com:sub with the value repo:ourorg/*. GitHub writes the subject as the repository followed by what the job ran for, such as repo:ourorg/tracking-site:ref:refs/heads/main, or repo:ourorg/tracking-site:environment:production when the job uses an environment. In an IAM condition a star matches any characters, colons included. So the role trusted every branch, every pull request and every environment of every repository in our organisation, a hundred and forty of them, including the hackathon ones that anybody in the company could create.

The value was the example in the module's README, written when there was one repository and copied into eleven roles since. Swapping keys for OIDC had felt like the end of the security work. We had stopped storing a secret, and the condition that replaced it decided who could deploy, and nobody had reviewed it as if it did.

We redeployed the site from our own workflow within the half hour. CloudTrail had the subject of every token each role had accepted, and the hackathon repository was the only stranger in it. Every role now uses StringEquals on one exact subject, and the deploy roles accept only repo:ourorg/:environment:production. The production environment allows only main and needs an approval from the owning team. The module has no default for the subject and fails if it contains a star, and a weekly job lists every role that trusts GitHub's provider and posts any whose condition does not name a single repository.

OIDC took the deploy key out of our repositories. The trust policy became the key, and ours had been cut for the whole company.

– Sergey Shinder