skip to content

Variables & Secrets

How values reach a job, which of them are safe, and how GitLab gets short-lived cloud or Vault credentials without storing a static key. The precedence order and the masked/protected distinction are standard screening material.

part ofGitLab CI/CDoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In GitLab CI/CD, the same variable name is set both in the project's CI/CD settings and in .gitlab-ci.yml. Which value does the job get, and what is the overall precedence order?

level: middleimportance: must knowfreq 70%

answer

  1. settings beat the file
  2. outside the repository outranks inside it
  3. closest group wins over parents
  4. manual and trigger values sit on top
  5. predefined variables are the floor

basics

~20 s

The project setting wins. GitLab ranks variables defined outside the repository above ones defined inside it: manual, trigger and schedule values first, then project, then group, then instance, then the values in .gitlab-ci.yml, with predefined variables last.

solid answer

~40 s

GitLab resolves a duplicate name by precedence, and the rule that surprises people is that UI- and API-defined variables beat the ones written in `.gitlab-ci.yml`. From highest to lowest, as of GitLab 17: variables typed into a manual pipeline run, passed with a trigger, or attached to a scheduled pipeline; then project variables; then group variables, where the group closest to the project wins over its parents; then instance variables; then values inherited from an earlier job's `dotenv` report; then a job's own `variables:` block; then the global top-level `variables:`; and finally deployment and predefined variables at the bottom. So a project-level `DEPLOY_ENV` silently overrides the one in your YAML, which is exactly the intended design — settings hold environment-specific and secret values, the file holds the defaults.

go deeper

for a junior

Remember the headline: a variable set in the project's CI/CD settings beats the same name written in .gitlab-ci.yml, and predefined variables sit at the bottom.

for a middle

Recite the order from manual-run values down through project, group, instance, dotenv inheritance and the two YAML levels, and explain why settings deliberately outrank the file.

for a senior

Diagnose a job receiving an unexplained value by walking that list, including pipeline-schedule variables and dotenv inheritance, and design where each value should live.

for a principal

Own the convention across many projects: which values belong to the instance, which to a group, which to a project, and how manual-run overrides are restricted so they cannot become an unaudited path to production.

## The one-line answer The project's CI/CD settings value wins. Anything defined **outside** the repository outranks anything defined **inside** it. ## The full order As of GitLab 17, from highest precedence to lowest: 1. Pipeline execution policy variables (from security policies, on the tiers that have them). 2. Trigger variables, scheduled pipeline variables, and variables typed into a manual pipeline run. 3. **Project** variables (Settings → CI/CD → Variables). 4. **Group** variables. If a project sits under nested groups and several define the name, the group closest to the project wins. 5. **Instance** variables (set by an administrator for the whole GitLab instance). 6. Variables inherited from an upstream job through a `dotenv` report and `dependencies`. 7. Job-level `variables:` in `.gitlab-ci.yml`. 8. Global (top-level) `variables:` in `.gitlab-ci.yml`. 9. Deployment variables contributed by integrations. 10. Predefined `CI_*` variables. Job-level YAML beats global YAML, which is the part everyone guesses correctly. The part people get wrong is that both of them sit **below** the settings screens. ## Why the design is that way round The file is versioned, readable by everyone with repository access, and identical for every environment. The settings screens are access-controlled, per project or group, and can hold secrets. If the file could override them, the value of a variable would depend on whoever last edited a branch — and anyone who can push a branch could redefine a production credential's name to something they control. Ranking settings above the file means a stored secret cannot be shadowed by YAML. The practical consequence: put safe **defaults** in the YAML and the real, environment-specific or sensitive values in settings. ```yaml variables: DEPLOY_ENV: "staging" # default; a project variable of the same name wins LOG_LEVEL: "info" deploy: variables: LOG_LEVEL: "debug" # beats the global block, still loses to a project variable script: - ./deploy.sh "$DEPLOY_ENV" ``` ## Scoping narrows the same list Precedence answers "which of two values wins". Scoping answers "is this value present at all": - **Environment scope.** A project or group variable can be limited to an environment pattern such as `production` or `review/*`. It is exported only to jobs whose `environment:` matches. Two variables with the same name and different scopes are the normal way to hold one credential per environment. - **Protection.** A protected variable is only exported to jobs running on protected branches or protected tags. On any other ref it is absent, not empty — see the separate protected/masked/file distinction. - **Inheritance.** Group and instance variables flow down to every project beneath them unless a nearer definition replaces them. ## Debugging a value you cannot explain When a job gets a value nobody admits to setting, work down the list. Check the pipeline's own variables first (a scheduled pipeline carries its own set, which is invisible in the YAML and in project settings). Then project, then each group from the project upward, then instance. `dotenv` inheritance is the sneaky one: a job that declares `artifacts:reports:dotenv` injects its variables into downstream jobs that depend on it, and those override the YAML. A quick diagnostic step is to echo the value at the very start of the failing job, since the exported environment is the resolved result. Debug tracing shows the whole environment but exposes secret values in the log, so it is gated by role and should not be left on. ## Overriding on a manual run The top of the list matters operationally: when you start a pipeline from the UI or the API you can supply variables that outrank everything except policy variables. That is how you do a one-off deploy to a different target without editing the repository — and also why the ability to run pipelines manually on a protected branch is itself a privilege worth restricting.

  • A parent group and a subgroup both define REGISTRY_URL. Which one reaches the project's jobs?
    The subgroup's — the group closest to the project wins, and the parent's value is shadowed. This is what makes nested groups usable for defaults: the top-level group sets the organisation-wide value, and any subgroup that needs something different overrides it locally without touching the parent or the project.
  • How can a variable that appears nowhere in the settings or the YAML still reach a job?
    Three common routes: it was typed into a manual or triggered pipeline run and so belongs to that pipeline; it was attached to the pipeline schedule; or an earlier job published it through `artifacts:reports:dotenv` and this job inherits it via `dependencies` or `needs`. Dotenv inheritance outranks the YAML, which makes it the hardest of the three to spot.
  • Why put defaults in .gitlab-ci.yml at all if settings always beat them?
    Because the file documents the contract. A reader sees which variables the pipeline expects and what a sane value looks like, forks and merge requests run without anyone provisioning settings, and the pipeline fails loudly rather than mysteriously when a value is missing. Settings then carry only what is environment-specific or secret.

saying these in an interview costs you the question

  • Assuming .gitlab-ci.yml overrides the project settings
  • Thinking the most recently edited definition wins
  • Believing a parent group beats a subgroup
  • Confusing precedence with protection or environment scope
  • Forgetting that dotenv inheritance outranks YAML variables

context

open as a page

In GitLab CI/CD, what is the difference between a protected variable, a masked variable, and a file-type variable?

level: middleimportance: must knowfreq 72%

basics

~20 s

They solve three unrelated problems. Protected controls which jobs receive the value at all — only jobs on protected branches or tags. Masked redacts the value if it is printed to the job log. File type writes the value to a temporary file and sets the variable to that file's path.

open as a page

In GitLab CI/CD, what are the predefined CI_* variables, and how does CI_COMMIT_REF_SLUG differ from CI_COMMIT_REF_NAME?

level: juniorimportance: should knowfreq 62%

basics

~20 s

GitLab injects predefined CI_* variables into every job, describing the commit, pipeline, project, job and runner. CI_COMMIT_REF_NAME is the raw branch or tag name; CI_COMMIT_REF_SLUG lowercases it, replaces every non-alphanumeric character with a hyphen and truncates it.

open as a page

What does the id_tokens: keyword do in a GitLab CI job, and what does the job actually receive?

level: middleimportance: should knowfreq 42%

basics

~20 s

id_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.

open as a page

A GitLab CI job printed a secret into its log even though the variable was marked Masked. What are the ways masking fails to protect you?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Masking replaces only exact, literal occurrences of the value in the log stream. Any transformation defeats it — base64, URL encoding, JSON escaping, line splitting — and it does not touch artifacts, reports or anything the job sends elsewhere.

open as a page

In GitLab CI, what does the secrets: keyword do, and why might your application receive a file path instead of the secret value?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

secrets: tells GitLab Runner to fetch a value from an external secret manager before the script runs and expose it as a CI/CD variable. By default the value is delivered as a file variable, so the variable holds a temporary file's path rather than the secret itself.

open as a page