skip to content

Project maintainers control their own .gitlab-ci.yml, but security requires a scan job in every pipeline. How do you actually enforce that in GitLab, and what does that cost the teams?

level: principalimportance: should knowfreq 28%

answer

  1. a line the project can delete is not a control
  2. invert the include direction
  3. the mandate owns the root configuration
  4. group-level label the project cannot detach
  5. who gets paged when it breaks fifty teams

basics

~20 s

An include the project can delete is a convention, not a control. GitLab enforces mandated jobs above the project: a compliance framework applied to the project supplies the pipeline's root configuration, which then includes the project's own file. Newer GitLab moves this to pipeline execution policies.

solid answer

~50 s

Asking every project to `include:` the security template is advisory — a maintainer can remove the line, and you find out during an audit. Enforcement has to sit somewhere the project cannot edit. GitLab's mechanism is to make the **mandated configuration the root of the pipeline** and let it pull the project's own config in: a compliance framework, applied to projects from the group, points at a configuration file in a separate project; that file becomes the pipeline configuration, and it typically ends with `include: project: '$CI_PROJECT_PATH', file: '$CI_CONFIG_PATH', ref: '$CI_COMMIT_REF_NAME'`. Newer GitLab versions supersede this with **pipeline execution policies** in security policies, which inject jobs the same way with a policy-based UI. Either way the cost is real: teams lose the ability to fully predict their pipeline from their own repository, the mandated jobs consume their minutes and can block their merges, and every failure of the injected job becomes a platform-team support ticket.

code

yaml · 9 lines
yaml
include:
  - project: '$CI_PROJECT_PATH'
    file: '$CI_CONFIG_PATH'
    ref: '$CI_COMMIT_REF_NAME'

secret-detection:
  stage: test
  script:
    - run-secret-scan

go deeper

for a junior

Understand that adding include: to your own .gitlab-ci.yml is voluntary — you could remove it — so an organisation-wide requirement has to be applied from outside the project.

for a middle

Explain the inversion: the mandated configuration becomes the pipeline's root and includes the project's own file back in, so removing your include changes nothing. Name compliance frameworks and pipeline execution policies as the GitLab features involved.

for a senior

Reason about failure ownership and rollout: version the mandated config, run new jobs non-blocking first, and have a plan for the day the injected job breaks every team's merges at once.

for a principal

Decide which controls deserve injection at all, layer convention, review gates and detection around the small mandatory core, and be explicit about the cost you are imposing — predictability, CI minutes, pipeline latency and a central support burden.

## Why an `include:` is not a control The first answer everyone gives is "publish a template and have everyone include it": ```yaml include: - project: 'platform/ci-templates' ref: 'v2.3.0' file: '/security-scans.yml' ``` This is fine as a *convention* and useless as a *control*, because the thing that would remove it — deleting a line from `.gitlab-ci.yml` — is precisely what the project's maintainers are allowed to do. Worse, it can be defeated without deleting anything: redefine the included job locally with an empty script, or override its `rules:` so it never matches. Array keys are replaced, not merged, so `rules: [when: never]` in the consuming project silently disables the mandated job. The design principle is the same as elsewhere in delivery: **the enforcement point must be outside the artefact being enforced upon.** Client-side git hooks cannot enforce policy for the same reason. ## GitLab's mechanism: own the root of the pipeline GitLab's answer inverts the include direction. Instead of the project including the mandated jobs, the mandated configuration becomes the pipeline's root and includes the project: ```yaml # .compliance-gitlab-ci.yml, in a project only the platform team can write to include: - project: '$CI_PROJECT_PATH' file: '$CI_CONFIG_PATH' ref: '$CI_COMMIT_REF_NAME' secret-detection: stage: test script: - run-secret-scan ``` This file is attached to a **compliance framework** defined at the group level and applied to projects. When a pipeline runs in a labelled project, GitLab uses this file as the configuration rather than the project's own, and the project's `.gitlab-ci.yml` participates only because the compliance file includes it — via the predefined variables for the project path, its configured CI file path and the branch being built. Remove your include of the security template and nothing changes; the scan still runs, because it was never yours to remove. The framework label itself is managed from the group, so a project cannot detach. Newer GitLab versions move this capability into **pipeline execution policies** under security policies: the same injection, expressed as a policy scoped to groups or projects, with the policy project separately protected. The compliance-pipeline configuration route has been deprecated in favour of it, so a current answer should name both and say which your version supports. ## What it costs A principal-level answer has to price this, because the mechanism is powerful enough to be resented. **Predictability.** A team can no longer read its repository and know what its pipeline does. The merged-configuration view in the pipeline editor becomes essential, and "why is there a job I never wrote?" becomes a recurring question. **Failure ownership.** The injected job fails on someone else's merge request. If the platform team ships a scanner change on Friday, fifty teams are blocked and none of them can fix it. That argues hard for versioning the mandated configuration, rolling it out gradually, and running new mandated jobs as non-blocking before they gate merges. **Resource and time budget.** Mandated jobs consume the project's CI minutes and runner capacity and add to every pipeline's wall-clock time. A scan that adds four minutes to a twelve-minute pipeline is a 33% tax on iteration speed, paid by every team, forever. **Escape hatches.** Real programmes need exceptions — a documentation repository does not need a container scan. Model that inside the mandated configuration with `rules:` on the injected jobs, or with separate frameworks per risk tier. Do *not* model it by letting projects opt out, or you are back to a convention. **Bypass surface.** Even with injection, ask what a determined project can still do: shrink the scope the scanner sees, or arrange for the job to pass trivially. Enforcement of *presence* is not enforcement of *effectiveness*, and the report needs to be collected centrally rather than trusted from the job's exit code. ## The layered answer In practice you use more than one control, matched to how much you need to trust the project: - **Convention** — a published, versioned template teams include voluntarily. Cheap, high adoption, zero enforcement. Right for conventions like a standard build. - **Review gate** — CODEOWNERS on `.gitlab-ci.yml` plus a protected branch and required approvals, so changes to the pipeline need the platform team's sign-off. Enforces *scrutiny*, not presence, and depends on humans. - **Injection** — compliance framework or pipeline execution policy. The only one that survives a hostile or careless maintainer. - **Detection** — independent verification that the scan actually ran and produced a report, since injection guarantees a job ran, not that it meant anything. The answer an interviewer is listening for is that you reach for injection *only* for the small set of genuinely mandatory controls, keep everything else at the convention layer, and are explicit about who gets paged when the mandated job breaks the whole organisation's merges.

  • If the mandated configuration becomes the pipeline's root, how does the project's own pipeline still run?
    Because the mandated file includes it, using the predefined variables for the project path, its configured CI file path and the current ref. Omit that include and you have replaced every team's pipeline with only the mandated jobs — which is occasionally deliberate for a lockdown, and otherwise an outage.
  • Why isn't CODEOWNERS on .gitlab-ci.yml enough on its own?
    It enforces review, not content: it guarantees someone from the platform team approves a change to the file, but approvals get granted under delivery pressure, and it does nothing about a project whose pipeline never included the scan in the first place. It is a good second layer, not the enforcement boundary.
  • A project redefines the injected scan job with an empty script. Does that defeat the mandate?
    It can, if the project's configuration is merged after the mandated jobs — same-named jobs merge with the later definition winning, and arrays like script and rules are replaced outright. That is why you validate outcomes centrally: collect the scan report and alert on projects whose mandated job produced nothing, rather than trusting a green job.
  • How do you roll out a new mandatory job without blocking fifty teams at once?
    Version the mandated configuration and stage it: run the new job non-blocking first — allowed to fail, reporting only — measure the failure rate across projects, fix the noisy cases, then flip it to gating. Pair that with a named owner and a documented break-glass path, because from the teams' side this job is unfixable by them.

saying these in an interview costs you the question

  • Proposing a template everyone must include as enforcement
  • Assuming a maintainer cannot override an injected job
  • Ignoring that mandated jobs consume the team's CI time
  • Treating a green mandated job as proof the scan was meaningful
  • Allowing projects to opt out of the framework themselves

context