GitLab CI to AWS with OIDC: Scope Trust, Assume Roles, and Verify Identity
Federate GitLab CI jobs to AWS with short-lived OIDC credentials, narrowly scoped IAM trust, protected deployment rules, and auditable role sessions.
CI jobs often need to upload an artifact, read a deployment parameter, or update a cloud service. A long-lived AWS access key stored as a project variable can make that integration appear simple, but it creates a credential that must be rotated, protected, and removed from every place it may have been copied. GitLab CI can instead issue an OpenID Connect (OIDC) ID token to a job, and AWS Security Token Service (STS) can exchange that token for temporary role credentials.
Federation does not automatically make a pipeline safe. AWS must trust the correct issuer and audience, and the IAM role trust policy must restrict which GitLab project and ref can assume the role. The role’s permission policy must separately limit what the resulting session can do. A correct integration therefore has two boundaries: who may obtain the role, and what that role may change.
Understand the federation boundary
The CI job requests an ID token through id_tokens. GitLab signs the token with claims describing its issuer, audience, project, ref, and job context. AWS validates the token using the configured OIDC provider and checks the role’s trust policy before STS issues temporary credentials. The runner does not need a stored AWS secret key to perform this exchange.
Use GitLab’s id_tokens configuration. The older CI_JOB_JWT_V2 token was removed in GitLab 17.0; examples based on it are obsolete. Give each token a deliberate aud claim and configure the IAM OIDC provider to accept that audience. The audience identifies the service expected to validate the token, not simply an arbitrary label that happens to make the exchange work. A commonly used AWS STS audience is sts.amazonaws.com; use the exact value consistently in the token configuration and AWS identity provider.
The issuer must also match the GitLab instance that issues the token. For GitLab.com, use the GitLab.com issuer. For Self-Managed GitLab, the issuer is the instance URL and AWS must be able to retrieve the issuer’s OpenID configuration and signing keys. A private instance is not automatically reachable by AWS. Exposing identity metadata or signing keys through a public mirror is a sensitive architecture change that needs a separate threat review; a misconfigured public bucket or stale key set can compromise every cloud role trusting that issuer.
Restrict the AWS trust policy
An OIDC provider establishes that AWS can validate tokens from an issuer. It does not mean every token from that issuer should assume a deployment role. The role trust policy should match the intended audience and subject, and should add stable project identifiers where GitLab.com and the AWS integration support them. The following example is for GitLab.com. Replace the account, group, project, and numeric IDs with values verified from the actual project. A rename or transfer can change path-related claims; stable numeric identifiers help make the intended project boundary explicit.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"gitlab.com:aud": "sts.amazonaws.com",
"gitlab.com:sub": "project_path:platform/payments:ref_type:branch:ref:main",
"gitlab.com:namespace_id": "12345",
"gitlab.com:project_id": "67890"
}
}
}
]
}
The exact match on sub restricts this example to a single project path and the main branch. The numeric project and namespace conditions further tie the permission to the intended GitLab project and group. These additional GitLab claim condition keys are available for the GitLab.com OIDC provider; GitLab’s current AWS guidance documents sub and aud as the supported keys for Self-Managed and Dedicated instances. For those instances, use the supported claims and do not copy a GitLab.com-only policy without confirming AWS and GitLab support for the particular issuer.
Do not replace exact matching with a wildcard merely to make a failed test pass. If a role genuinely supports a set of refs, StringLike can express a pattern, but every wildcard broadens the set of tokens that can satisfy the trust policy. A pattern such as ref:* can permit tags, branches, or other refs that were not intended for production. Define the allowable ref types and names explicitly, and test positive and negative examples against the actual token claims. GitLab protected branches and protected environments are useful controls on the GitLab side; they do not remove the need for restrictive AWS trust.
The trust policy is not the role’s permission policy. After STS accepts a token, AWS evaluates the role’s attached identity policies and applicable resource policies. Grant only the service actions and resources required by that job. A role that can assume successfully but has broad s3:* or iam:* permissions is still dangerously privileged. Use separate roles for materially different duties such as artifact upload, infrastructure planning, and production deployment.
Request short-lived credentials in a deploy job
The job below requests an ID token, exchanges it for temporary credentials, and verifies the resulting principal before running a deployment command. AWS_ROLE_ARN should be a protected, non-secret configuration value available only to the intended protected deployment job. The sample suppresses shell tracing around the exchange and does not print the ID token or temporary credentials.
deploy_production:
stage: deploy
environment:
name: production
rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
when: manual
- when: never
id_tokens:
GITLAB_OIDC_TOKEN:
aud: sts.amazonaws.com
script:
- |
set -eu
set +x
credentials="$(aws sts assume-role-with-web-identity \
--role-arn "$AWS_ROLE_ARN" \
--role-session-name "gl-${CI_PROJECT_ID}-${CI_JOB_ID}" \
--web-identity-token "$GITLAB_OIDC_TOKEN" \
--duration-seconds 3600 \
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' \
--output text)"
IFS="$(printf '\t')" read -r \
AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN <<EOF
${credentials}
EOF
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN
unset credentials
aws sts get-caller-identity
./ci/deploy-production.sh
This illustrates the exchange, not a drop-in workflow. Use a runner image that contains the AWS CLI version your team tests, scope AWS_ROLE_ARN to the intended deployment, and configure the GitLab production environment as protected with the required approvers. The exact ref conditions in AWS remain the final cloud-side check; the GitLab rules stanza only controls whether this job is added to a pipeline. Confirm the STS session duration is permitted by the role and appropriate for the task. Shorter sessions reduce exposure time, but the deploy process must still be able to refresh credentials or finish within the chosen duration.
The sample reads the three tab-separated fields emitted by the AWS CLI’s text query and exports them only for the current job. In production, validate this small credential-handling block with the exact CLI version and shell used by the runner. Do not enable debug output around token exchange, print environment variables, or upload a workspace archive containing token files. The GitLab ID token and STS credentials are bearer credentials while valid; treat them as secrets even though they are short-lived.
Scope token issuance and job permissions
id_tokens makes an ID token available to that job. Define it only in jobs that require federation, rather than at a broad default level. A deploy role should not be available to ordinary test jobs, forked merge requests, or every branch just because a shared CI template makes the token easy to request.
Use independent controls:
- GitLab
rulesdecide when the deployment job is included. - Protected branches and environments constrain which GitLab users and refs can execute production work.
- The ID token audience identifies the intended verifier.
- The AWS role trust policy restricts the issuer, project, and ref claims that can assume the role.
- The role’s permission policy limits the AWS actions and resources in the session.
- STS session duration and job lifetime bound how long the temporary credentials can be used.
These controls protect different layers. A manual job is not by itself an approval policy; a protected environment is not a replacement for AWS trust conditions; and a role trust policy does not limit what the role can do after assumption. Review all layers as one workflow, including the job token and any other credentials the runner receives.
Diagnose failures without widening access
If AssumeRoleWithWebIdentity returns AccessDenied, compare the actual token claims with the IAM conditions. Confirm iss matches the configured identity provider, aud exactly matches an allowed audience, and sub uses the expected project path, ref type, and ref name. A branch-versus-tag mismatch or a merge request pipeline’s source project identity can make the subject different from a push pipeline. Decode a token only in a controlled diagnostic job and never paste a live token into logs, tickets, or a public JWT inspection service.
If AWS reports that it cannot connect to the identity provider, verify the issuer’s TLS certificate chain and that AWS can retrieve the OpenID configuration and JSON Web Key Set. A private GitLab instance may need an explicitly designed public metadata path. Do not solve availability by publishing private data broadly or by trusting an unrelated issuer. Validate the identity configuration and signing-key rotation path before moving deployment traffic to the integration.
For an authorization failure after role assumption, inspect the role permission policy and the target resource policy, not the OIDC trust policy alone. sts get-caller-identity confirms which AWS identity the job obtained; it does not prove that the identity has safe permissions or that the intended project was the only possible caller. Keep an audit record linking GitLab project, pipeline, job, commit, role session, and AWS deployment activity.
Production validation plan
Test the trust boundary in a non-production AWS account before granting production access:
- Confirm an allowed push to the protected deployment branch can assume the expected role.
- Confirm an unrelated project with a matching branch name cannot assume it.
- Confirm a feature branch, tag, fork merge request, and unprotected environment are denied unless intentionally allowed.
- Confirm the role cannot read or mutate AWS resources outside its task.
- Let the STS session expire and verify the deployment fails safely rather than falling back to a stored long-lived key.
- Review job logs and retained artifacts for the absence of token and credential values.
- Exercise a project rename or namespace move in a test configuration if the trust policy depends on path claims.
- Record the expected issuer, audience, project identifiers, subject pattern, role ARN, and permission policy so reviewers can reproduce the authorization decision.
The check should fail closed. If no role is available, the deployment should stop and ask an operator to repair the trust configuration. Do not create a static-key fallback that silently bypasses the OIDC boundary.
Operational checklist
- Is the issuer URL correct for GitLab.com or the exact Self-Managed instance?
- Does the AWS OIDC provider allow the same audience the job requests?
- Does the role trust policy match the intended project and production ref without unnecessary wildcards?
- Are GitLab.com-only claims distinguished from claims supported by Self-Managed or Dedicated installations?
- Is the role’s permission policy least-privilege and separate from the trust policy?
- Can only protected deployment jobs request the token and assume the production role?
- Are token values, STS credentials, and AWS CLI debug output excluded from logs and artifacts?
- Is the exact assumed role and pipeline identity observable in deployment records?
- Have both allowed and forbidden projects, refs, and pipeline types been tested?
OIDC federation removes the need to store a durable AWS key in the repository, but the trust relationship becomes the critical configuration. Pair a narrowly scoped GitLab identity with a narrowly scoped AWS role, verify both positive and negative cases, and retain enough deployment evidence to explain exactly which pipeline obtained which authority.
Related:
- GitLab CI Production Pipelines: DAG Dependencies, Artifact Contracts, and Deployment Locks
- GitHub Actions OIDC: Short-Lived Cloud Credentials Without Repository Secrets
Sources: