What does the id_tokens: keyword do in a GitLab CI job, and what does the job actually receive?
answer
- job-level keyword, one entry per recipient
- each name becomes an environment variable
- aud says who may accept it
- claims describe project, ref and protection
- replaced the removed CI_JOB_JWT
basics
~20 sid_tokens: asks GitLab to mint one or more short-lived JWTs for the job, each with an audience you specify. Each token appears as an environment variable of the name you chose, so the job can exchange it for cloud or Vault credentials without any stored key.
solid answer
~50 sYou declare `id_tokens:` on a job with one or more names, each carrying an `aud:` value. GitLab signs a JSON Web Token per name, valid only for that job, and the runner exports it as an environment variable with that name — so `VAULT_ID_TOKEN:` gives you `$VAULT_ID_TOKEN` holding the JWT. The token's claims describe the job: the issuer is your GitLab instance, and the payload carries the project path, namespace, the ref and its type, whether that ref is protected, the environment when the job has one, and a `sub` claim that combines them. The relying party — Vault, AWS, GCP, Azure — verifies the signature against GitLab's public keys, checks that `aud` matches what it expects, and grants a role only if the claims match its trust policy. This replaced the removed `CI_JOB_JWT` and `CI_JOB_JWT_V2` variables, whose single fixed audience made them dangerous to send to more than one service.
code
yaml · 10 linesdeploy-production:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
AWS_ID_TOKEN:
aud: https://gitlab.example.com
environment: production
script:
- echo "token length: ${#VAULT_ID_TOKEN}"
- ./scripts/exchange-and-deploy.shgo deeper
Know that id_tokens: makes GitLab issue a short-lived JWT into a variable you name, so the pipeline needs no stored cloud key.
Explain the aud value, the fact that each entry becomes its own environment variable, the job-lifetime validity, and that the token is exchanged for a real credential rather than being one.
Show how the receiving trust policy must bind on ref and protection claims, and diagnose a working exchange that grants far more than intended because the policy matched only the project.
Own the migration away from stored cloud keys entirely, and set the standard for how trust policies are written and reviewed so a push permission never becomes a production role.
## The keyword `id_tokens:` is a job-level keyword. Each entry names a variable and gives it an audience: ```yaml deploy: id_tokens: VAULT_ID_TOKEN: aud: https://vault.example.com AWS_ID_TOKEN: aud: https://gitlab.example.com script: - ./exchange-token.sh ``` GitLab mints one signed JWT per entry and the runner exports it under that exact name. The tokens are created for the job and expire with it; nothing is stored in project settings, nothing is rotated by a human, and nothing survives the pipeline. ## What is in the token The payload describes *which job asked*, which is the whole point — the receiving system authorises based on identity rather than on possession of a shared string. The claims include the issuer (`iss`, your GitLab instance URL), the audience you set, the project path and project id, the namespace path, the ref and its type (branch or tag), whether that ref is protected, the environment name when the job declares one, and a composite `sub` claim built from those parts, of the general shape `project_path:group/project:ref_type:branch:ref:main`. GitLab publishes the matching public keys through its OpenID Connect discovery document, so a relying party can fetch the JWKS and verify the signature without any pre-shared secret. ## Why one audience per token A JWT is a bearer credential. Whoever holds it can present it. The `aud` claim is the instruction to the recipient: "this token was minted for you; reject it if you are not the named audience". If a single token carried a wildcard audience and you sent it to three services, any one of them could replay it against the other two. That is exactly the flaw in the old approach: `CI_JOB_JWT` was injected into every job with one fixed audience, so every job carried a credential it did not need, aimed at whatever the instance was configured for. `id_tokens:` fixes both problems. Tokens exist only in the jobs that declare them, and each has an audience the recipient can check. `CI_JOB_JWT` and `CI_JOB_JWT_V2` were deprecated in GitLab 15.9 and removed in 17.0; pipelines still referencing them silently get nothing. ## How the exchange works in practice The job holds a token that proves who it is; it does not yet hold a credential for the target system. The script trades one for the other — an STS-style call for a cloud provider, or a login call for a secret manager — and receives a short-lived credential scoped by the target's own policy. Cloud SDKs increasingly do this for you when pointed at a file containing the token, which is one reason the token is exposed as an ordinary variable you can write out or pass along. The critical configuration is on the **other** side: the trust policy. It must bind on the issuer, the audience, and enough of the subject claims to name exactly the pipelines you meant. A policy that matches only the project path lets any branch in that project assume the role, including a branch a contributor just pushed. Binding on `ref` and on `ref_protected`, or on the environment, is what makes the grant meaningful. ## What this does not do It does not remove the need for protected variables and protected refs — the trust policy is now the thing that must reference them. It does not authenticate anything by itself: without a configured relying party, the JWT is an unusable string. And it does not apply to jobs that do not declare it, which is a feature: a job with no `id_tokens:` block has no identity token to leak. ## Where it shows up next The token is also the input to GitLab's built-in external secret-manager integration, where a `token:` key on a `secrets:` entry names one of these variables so the runner can authenticate on the job's behalf and place the fetched value into the environment.
- Why declare two id_tokens entries in one job instead of reusing a single token?Each token names one audience, and the recipient is expected to reject a token minted for someone else. Two recipients therefore need two tokens; reusing one means whichever service receives it could replay it against the other. Declaring one entry per recipient also keeps the blast radius of a leaked token to that single relying party.
- Which claims should a cloud trust policy match on, and why is the project path alone not enough?Match the issuer and audience first, then bind the subject tightly: the project, the ref and its type, and whether the ref is protected — or the environment for a deploy job. Matching only the project path means any branch anyone can push to that project mints a token that assumes the role, which turns a push permission into a production credential.
- What replaced CI_JOB_JWT and why was it removed?The `id_tokens:` keyword. `CI_JOB_JWT` was present in every job with a single instance-wide audience, so every job carried a bearer credential it had not asked for, and a recipient could not distinguish a token meant for it from one meant for another service. It was deprecated in GitLab 15.9 and removed in 17.0.
saying these in an interview costs you the question
- Calling the ID token a credential for the cloud provider itself
- Thinking one token can safely serve several audiences
- Configuring a trust policy that matches only the project
- Assuming id_tokens works without configuration on the receiving side
- Still referencing CI_JOB_JWT in current pipelines