TL;DR: A CI pipeline that deploys to AWS traditionally needs an AWS access key, stored as a CI secret. A long-lived key sitting in a CI system is the single credential you would most like to be rid of. OIDC federation removes it: the CI platform mints a short-lived signed token, AWS exchanges that token for short-lived credentials against a role whose trust policy names the pipeline. No key is ever stored. There is one quiet way to get it wrong.
The access key you don’t want
A CI pipeline that runs tofu apply against AWS needs AWS credentials. The traditional way to give it some is an IAM user with an access key pair, pasted into the CI system as a masked variable.
Look at what that key is. It is long-lived: it works until someone remembers to rotate it, and rotating it is a chore, so mostly nobody does. It is powerful: it can apply infrastructure, so it can do nearly anything. And it is sitting in a CI system, which is one of the most attractive targets in your whole supply chain. You have taken your highest-value credential and stored a permanent copy of it in a place built for running automated jobs.
For infrastructure that is going to hold a release-signing key, that is precisely the wrong starting point. So the phpboyscout infrastructure has no AWS access key in CI at all. Not a well-guarded one. None.
Federation instead of a stored secret
The replacement is OIDC federation, and the shape of it is worth walking through, because it is genuinely different from “a secret, but better.”
A modern CI platform can mint an OIDC token. GitLab does this with an id_tokens: block: at job time, GitLab issues a short-lived JSON Web Token, signed by GitLab, that asserts a set of facts. This is project X. This is pipeline Y. This is running on ref Z, of this type.
AWS can consume that. The sts:AssumeRoleWithWebIdentity call takes such a token and, if it satisfies an IAM role’s trust policy, returns short-lived AWS credentials for that role. The trust policy is where the control lives: it names GitLab as a trusted token issuer, and it constrains the token’s sub claim so that only the specific project, and the specific refs, you intend can assume the role.
Put together: the pipeline asks GitLab for a token, hands it to AWS, and gets back credentials that last about an hour and are scoped to one role. Nothing long-lived is stored anywhere. The credential exists only for the job that needs it, and it cannot be stolen from a CI variable store, because it was never in one.
Two halves of one handshake
That handshake is built by two of the repos in this series, each owning one side.
terraform-aws-bootstrap builds the AWS half, in its automation-iam module: it registers GitLab as an OIDC identity provider in the account, and it creates the automation role with the trust policy that decides which pipelines may assume it.
The CI components build the consuming half: the id_tokens: block that asks GitLab for the JWT, and then simply letting the AWS provider’s own credential chain perform the exchange. The pipeline does not call sts by hand. It presents the token; the SDK does the rest.
The gotcha: don’t set a profile
There is one quiet way to break this, and a stack can look completely correct while doing it.
The AWS SDK finds credentials by walking a chain of sources in order. The web-identity path, the one that uses the OIDC token, is one link in that chain. It triggers off environment variables the CI sets up automatically.
But if the aws provider block has a hardcoded profile = "...", the SDK takes the profile link of the chain instead, and never reaches the web-identity link. A profile line is the sort of thing that ends up in a provider block from someone’s local development setup, where it is exactly right. Committed and run in CI, it silently short-circuits the federation. The pipeline either fails to find credentials, or finds the wrong ones.
The rule is simple once you know it: the provider block that runs in CI must not name a profile. Leave the chain free to find the web identity. It is the kind of bug that teaches you to be precise about which link of the credential chain you are relying on.
The bottom line
Giving CI an AWS access key means storing your most powerful, longest-lived credential in one of your most exposed systems. OIDC federation removes it entirely. The CI platform mints a short-lived signed token, AWS exchanges it via AssumeRoleWithWebIdentity for hour-long credentials against a role whose trust policy names the exact pipeline, and nothing permanent is stored.
terraform-aws-bootstrap builds the AWS side, the identity provider and the trust policy; the CI components build the consuming side, the token request. The one trap is a hardcoded profile in the provider block, which short-circuits the SDK’s credential chain before it reaches the web-identity path. Get that right, and a pipeline deploys to AWS as a verifiable, short-lived identity, with no key to steal.