skip to content

What is the risk of using include: remote: in a GitLab CI configuration, and what would you use instead?

level: seniorimportance: should knowfreq 42%

answer

  1. fetched at pipeline creation, no credentials
  2. a location, not a version
  3. included YAML is executable configuration
  4. masking hides logs, not exfiltration
  5. pin the ref, or vendor the file

basics

~20 s

GitLab fetches the URL with an unauthenticated GET at pipeline creation and runs whatever it returns as part of your configuration, on your runners, with your CI/CD variables. Nothing is pinned or verified, so whoever controls that URL can inject jobs. Prefer include:project at a pinned ref, or a versioned component.

solid answer

~50 s

`include:remote` takes a full URL and fetches it over HTTP(S) with a plain GET at **pipeline creation**. GitLab sends no credentials, so the file must be publicly readable — and, more importantly, whatever is served at that moment becomes part of your pipeline configuration. The included file can define any job, with any `script`, on any runner your project can reach, and those jobs see your masked variables, your deploy tokens and your OIDC identity. There is no version pin, no checksum, no signature: the third party can change the file, or an attacker who compromises that host or the transport can. The mitigations are ordering, not tuning. Host the template on your own instance and use `include:project` with `ref:` pinned to a tag or commit; or consume a published component pinned to an exact version. If a remote include is unavoidable, mirror the file into your own repository and update it deliberately.

go deeper

for a junior

Know that include:remote fetches a public URL with no authentication and that the file it returns becomes part of your pipeline, so it must come from somewhere you trust.

for a middle

Explain that resolution happens at pipeline creation, that nothing is pinned or verified, and that the included configuration can define jobs which run on your runners with your variables. Name include:project with a pinned ref as the alternative.

for a senior

Walk the attack end to end — host compromise, injected job, secrets read from the environment despite masking, OIDC role assumed — and give a ranked remediation: pinned include:project, versioned component, vendored file. Add the availability argument.

for a principal

Set the policy: which include forms are permitted at all, how template projects are protected and tagged, and how you would detect that a pipeline's effective configuration changed without any commit in the consuming repository.

## What `include:remote` actually does ```yaml include: - remote: 'https://raw.example.com/ci/build.yml' ``` When a pipeline is created, GitLab performs an HTTP(S) GET on that URL, parses the response as CI configuration, and merges it into your pipeline. Three properties make this a security question rather than a convenience question: 1. **It is unauthenticated.** GitLab does not attach your credentials, which is why the file must be publicly readable. It also means you cannot use this form to reach a private file, and people who try often work around it by embedding a token in the URL — which then sits in `.gitlab-ci.yml` in plain text, in every clone and every diff. 2. **It is unpinned.** The URL identifies a location, not a version. You get whatever is served the moment your pipeline starts. There is no digest, no signature and no lockfile. 3. **The content is code.** CI configuration is not data. An included file can define new jobs, override existing ones, change `image:`, add `before_script:` to a job that deploys to production, and set `variables:`. Those jobs run on your runners with your project's CI/CD variables in the environment. ## The concrete attack The pipeline is production. If the party serving that URL is compromised — or simply decides to — the next pipeline in every consuming project can be made to run an extra step that reads the environment and posts it somewhere. Masked variables are masked in *logs*; masking is a display feature and does not stop a job from exfiltrating the value over the network. If the project uses OIDC to assume a cloud role, the injected job can request that token and use the role directly. And because you did not change your repository, nothing in your history explains what happened; the diff is on someone else's server. Domain hijack, an expired domain, a compromised CDN bucket, a maintainer account takeover, and a plain man-in-the-middle on a misconfigured endpoint are all the same failure from your side: the configuration you ran is not the configuration you reviewed. ## What to do instead, in order of preference **Host it on your instance and use `include:project` with a pinned `ref`.** ```yaml include: - project: 'platform/ci-templates' ref: 'v2.3.0' file: '/build.yml' ``` This is the default answer. Access is authorised against the user running the pipeline, so private templates work; the template lives under your instance's access controls, audit log and protected-branch rules; and `ref:` names an immutable-by-convention tag (or a commit) so the configuration you reviewed is the configuration that runs. Moving a tag is possible, which is why the template project's tags should be protected. **Consume a published component pinned to a version.** `include:component` with `@1.2.0` gives the same pinning with a declared input contract, and the component's project is subject to the same instance controls. **Vendor it.** If you genuinely need a third party's template, copy the file into your repository and include it with `include:local`. Now the content is reviewed in a merge request, lives in your history, and updates when *you* decide. The cost is manual updates — the same trade you make when you vendor a dependency. **If a remote include is truly unavoidable**, treat it as a trusted dependency and reduce blast radius: restrict which projects may use it, keep production credentials out of any pipeline that includes it, and prefer a URL whose path contains an immutable identifier such as a commit SHA so at least the content is fixed. None of that is as good as vendoring. ## The wider principle This is the supply-chain rule applied to pipeline configuration: **pin what you execute, and execute only what you can review.** It is the same reasoning as pinning a third-party action to a commit rather than a tag, or pinning a container image by digest. `include:remote` is the GitLab surface where that rule is easiest to forget, because the syntax is a single line and it feels like fetching a document rather than importing code. A good answer also names the operational failure mode, not just the security one: the URL's host being down or slow means your pipeline does not get created at all. A remote include makes a third party's availability a hard dependency of your ability to ship, which on its own is usually enough to reject it.

  • Masked variables exist — doesn't that protect the secrets from an injected job?
    No. Masking only redacts matching strings in job logs; the value is still present in the job's environment. An injected job can read it and send it anywhere over the network without ever printing it. Masking is a backstop against accidental disclosure in output, never a control against untrusted code running in the job.
  • What is the availability argument against include:remote, separate from security?
    Include resolution happens while GitLab creates the pipeline, so if the remote host is down, slow or rate-limiting, the pipeline is not created at all — you cannot ship. A remote include makes a third party's uptime a hard dependency of your delivery, which is usually enough to reject it even where you trust the content.
  • Is include:project with ref: a tag actually immutable?
    By convention, not by force — a tag can be moved or deleted in the template project. Protect the template project's tags, and pin to a commit SHA where reproducibility must be absolute. The bigger win over a remote URL is that the file lives inside your instance's access control and audit log, so a change is attributable.

saying these in an interview costs you the question

  • Treating included YAML as data rather than executable configuration
  • Believing GitLab authenticates or verifies the remote fetch
  • Assuming masked variables cannot be exfiltrated
  • Putting an access token in the include URL
  • Thinking HTTPS alone makes the include trustworthy

context