Rules / GL081
GL081 OIDC id_tokens issued on an unrestricted pipeline source
Severity: warn
OWASP: CICD-SEC-5 – Insufficient PBAC
CWE: CWE-284 – Improper Access Control
Minimum GitLab version: 15.7 (id_tokens:)
Risk
A job that declares id_tokens: mints an OIDC JWT that federates into a cloud provider or secrets manager (AWS STS, GCP, Vault, …). If nothing restricts which pipeline sources run that job, any Developer can trigger it:
- Same-project MR and branch pipelines. Anyone with the Developer role can push a branch, open a merge request, and cause the job to mint a token. Cloud-side trust policies overwhelmingly condition on
project_path/project_idonly — in which case a token minted from an unreviewed feature branch is indistinguishable from one minted on the default branch. Effectively every Developer holds whatever role the job can assume, without ever touching a protected branch. ci_allow_fork_pipelines_to_run_in_parent_project. A parent-project member can run a fork MR's pipeline in the parent context, where fork-controlled code runs before merge.
(The naive "a fork MR steals your token" framing is not the main path: fork MR pipelines run in the fork project by default, so the JWT identifies the fork, not the parent, and the parent's trust policy will not accept it.)
The robust fix is a cloud-side condition on the ref_protected / ref / pipeline_source claims — which a YAML linter cannot see. The pipeline-side guard is the part glsec can check, and its absence is a reliable signal that nobody scoped which refs may assume the role.
What glsec checks
GL081 flags a job that declares id_tokens: when either:
- Its guard explicitly admits
merge_request_event— arules:ifmatchingmerge_request_event(not awhen: neverexclusion), or a legacyonly: merge_requests. - It has no effective guard — no
rules:/only:/except:, or arules:block whose items carry noif:/changes:/exists:condition — and no top-levelworkflow:rulesrestricts the pipeline to protected refs.
GL081 does not fire when:
rules:restrict to a protected ref or tag —$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH,$CI_COMMIT_TAG,$CI_COMMIT_REF_PROTECTED == "true", or an explicit branch equality ($CI_COMMIT_BRANCH == "main").- a top-level
workflow:rulesconfines the whole pipeline to protected refs (every admitting rule restricts to a protected ref and none admitsmerge_request_event). A negated comparison ($CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH,$CI_COMMIT_REF_PROTECTED != "true",$CI_COMMIT_TAG == null) is not a restriction, and every||alternative of a condition must restrict on its own. - the job inherits config via
extends:— the guard may live in an unresolved base. YAML merge keys (<<:) are resolved. - the job is a hidden template (name starting with
.). id_tokens:is declared in the top-leveldefault:block rather than on a job — pipeline-wide inheritance is out of scope, on the same footing asextends:(glsec flags the per-job declaration, not the inherited one).
Trigger example
aws-integration-test:
stage: test
id_tokens:
AWS_ID_TOKEN:
aud: https://sts.amazonaws.com # correct, scoped audience — but…
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"' # …any MR can mint it
script:
- aws sts assume-role-with-web-identity --web-identity-token "$AWS_ID_TOKEN"
Safe alternative
Restrict the job to protected refs, and condition the cloud trust policy on the ref_protected claim so a feature-branch token cannot assume the role:
aws-deploy:
stage: deploy
id_tokens:
AWS_ID_TOKEN:
aud: https://sts.amazonaws.com
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
script:
- aws sts assume-role-with-web-identity --web-identity-token "$AWS_ID_TOKEN"
// AWS IAM trust policy condition (cloud-side, not visible to glsec)
"Condition": {
"StringEquals": { "gitlab.example.com:sub": "project_path:my/proj:ref_type:branch:ref:main" }
}
Relationship to other rules
- GL009 flags an overly broad OIDC audience (
aud:). GL081 is independent — a token can have a perfectly narrow audience and still be mintable from any branch. The two can co-fire on the same job; both defects are real. - GL080 flags sensitive deploy/publish jobs (by
environment:or a deploy keyword) with no source guard. GL081 keys onid_tokens:specifically, catching jobs GL080 misses — e.g. anaws-integration-testorterraform-planjob that federates into a cloud account but is named like a test. - GL053 flags a missing or unrestricted top-level
workflow:rulesat the pipeline level.
False-positive note
Small blast radius by construction — the rule only looks at jobs that already declare id_tokens:, which is opt-in and uncommon. The main false-positive class is a guard expressed somewhere glsec does not resolve (extends:, an included template), which the extends: bail-out already handles. Review each finding against your cloud trust policy: a job restricted only by an unprotected branch condition is still exploitable even when glsec accepts it.
Documentation for glsec 1.23.0. Source of this page · Rule implementation