Use this page when a pipeline still uses long-lived cloud keys as variables, or when setting up deploys for a migrated repo. OIDC means no long-lived cloud keys live in CI at all. The pattern is the same across CI systems. Only the issuer URL and claim names change. Look those up in your provider's OIDC docs.
How it works
sequenceDiagram participant Job as CI job participant CI as CI provider (issuer) participant STS as Cloud STS Job->>CI: request id_token (aud = configured audience) CI-->>Job: signed JWT with project, ref, environment claims Job->>STS: AssumeRoleWithWebIdentity(role, JWT) STS->>STS: check issuer, audience, sub conditions STS-->>Job: temporary credentials (1 h)
Prerequisites
- Permission to create an OIDC identity provider and roles, through Terraform in an MR.
- The issuer URL for your CI provider.
- A decision on the audience value, often the same issuer URL.
Step 1: Create the identity provider (Terraform)
resource "aws_iam_openid_connect_provider" "ci" {
url = "https://oidc.example.com"
client_id_list = ["https://oidc.example.com"]
# thumbprint_list is optional on recent AWS provider versions; AWS verifies well-known CAs itself
}
Step 2: One role per project and environment
Scope the trust policy tightly with the sub claim. The exact sub format depends on your CI provider (project or repo, branch, environment). Copy it from that provider's docs. Do not use a bare *.
data "aws_iam_policy_document" "deploy_trust" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.ci.arn]
}
condition {
test = "StringEquals"
variable = "oidc.example.com:aud"
values = ["https://oidc.example.com"]
}
condition {
test = "StringLike"
variable = "oidc.example.com:sub"
values = ["<sub-for-this-project-and-branch>"]
}
}
}
resource "aws_iam_role" "deploy_prod" {
name = "<project>-deploy-prod"
assume_role_policy = data.aws_iam_policy_document.deploy_trust.json
max_session_duration = 3600
}
Attach a permission policy that allows only what the deploy needs (for a container service: register the task definition, update the named services, pass the named task roles, read and write the one deploy parameter). See least privilege.
Step 3: Use it in the job
Wire this into your CI provider's OIDC or id-token feature. The YAML differs by vendor. The job itself does this:
echo "$OIDC_TOKEN" > /tmp/web-identity-token
export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/web-identity-token
export AWS_ROLE_ARN="$DEPLOY_ROLE_ARN" # non-secret CI variable per environment
export AWS_ROLE_SESSION_NAME="ci-$JOB_ID"
aws sts get-caller-identity
./ci/deploy.sh prod
Restrict the production job to the default branch, and make it manual.
Verify
-
aws sts get-caller-identityin the job prints the expected role. - A job on a feature branch is denied for the prod role (test this on purpose).
- Old access key variables are removed, and the IAM user's keys are deactivated, then deleted after a quiet period.
Pitfalls
subcondition with a bare*. Any project on the instance could assume the role.- Using
StringLikewith a wildcard ref on a prod role. Restrict prod to the default branch or protected tags. - Forgetting that merge-request or pull-request pipelines have a different
ref. Give them a read-only or dev role, not prod. - A self-managed CI server behind a private network: the cloud must reach the issuer's
/.well-known/openid-configurationand JWKS over the internet, or OIDC fails.