Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

By updated

Improve this page

GitLab logo

Do you need to pass information to your jobs (URLs, tokens, configuration) without hardcoding it? GitLab CI/CD variables solve this problem. And for sensitive data (passwords, API keys), the protected and masked options keep it secure.

This guide shows you how to configure your pipelines and secure your secrets, two essential skills for any production pipeline.

This guide is for you if...

Read this guide through the section that matches your need. If you configure a pipeline (URLs, versions, settings passed to jobs), start with the different types of variables. If you manage secrets (API keys, passwords, certificates), go to Securing secrets. If a variable does not have the right value because of a conflict, the answer is in variable precedence. To separate dev and prod with different variables depending on the environment, see best practices.

What you will learn

By the end of this guide, you will be able to pass values to your jobs without hardcoding them, and to protect the secrets among them:

  • Tell apart environment variables, file-type variables and predefined CI_* variables, and know where to define them (YAML, project, group)
  • Protect a secret with the Protected, Masked and Hidden options, knowing the limits of masking
  • Predict the value of a variable defined in several places thanks to the nine-level precedence
  • Apply the rules for managing secrets: no secret in the YAML, environment scope, rotation, pipeline inputs
  • Separate dev and prod with distinct variables, clear prefixes and a documented list of required variables
  • Diagnose an empty variable, an unexpected value or a secret visible in the logs

Prerequisites

Before continuing, make sure you are comfortable with:

The different types of variables

GitLab CI/CD offers several types of variables, each suited to a specific need. Understanding these types is essential to avoid configuration mistakes.

Environment variables (the most common)

Environment variables are text strings available in job scripts. They store configuration, URLs, versions, anything that may change between environments or runs.

variables:
DATABASE_URL: "postgres://db.example.com:5432/myapp"
APP_VERSION: "1.2.3"
test:
script:
- echo "Connecting to $DATABASE_URL"
- echo "Version: $APP_VERSION"

Where to define them?

LevelScopeUse case
In the YAMLThis pipeline onlyProject-specific configuration
Project (Settings > CI/CD)All the project's pipelinesSecrets, production URLs
Group (Settings > CI/CD)All the group's projectsTokens shared between projects

File-type variables

Sometimes you need a file rather than a simple string: an SSL certificate, a configuration file, a private key. GitLab creates a temporary file and stores its path in the variable.

deploy:
script:
# $DEPLOY_KEY contains the PATH to the file, not the content
- scp -i "$DEPLOY_KEY" app.zip user@server:/deploy/

To create a file variable:

  1. Go to Settings > CI/CD > Variables
  2. Click Add variable
  3. Select Type: File
  4. Paste the content of the file in Value
  5. GitLab will create the file for each job and put the path in the variable

Protected variables

Protected variables are only available in protected branches or tags. This is the first line of defense for your production secrets.

Why does it matter?

Without protection, anyone can create a hack branch, add echo $PROD_API_KEY to the .gitlab-ci.yml, and see your secret in the logs.

With a protected variable, since the hack branch is not protected, the variable is not provided to the job: the echo prints nothing of the secret.

To protect a variable:

  1. Go to Settings > CI/CD > Variables
  2. Check Protect variable
  3. The variable will only be injected into the pipelines of protected branches/tags

Masked variables

Masked variables are replaced with [MASKED] in the logs. They are indispensable to avoid accidental leaks of secrets.

job:
script:
- echo "Token: $API_TOKEN" # Prints: Token: [MASKED]

Masked and Hidden (introduced in GitLab 17.4, generally available in 17.6). Since GitLab 17.4, behind a feature flag enabled by default, and generally available since 17.6, you can check Hidden in addition to Masked. The variable can then no longer be displayed in the Settings > CI/CD > Variables interface after it is created. A variable can only be hidden when it is created, never afterwards. Useful for very sensitive secrets.

Predefined CI_* variables

GitLab automatically injects dozens of variables into every job. The most useful ones:

VariableContentUsage example
$CI_COMMIT_BRANCHBranch nameConditions in rules
$CI_COMMIT_SHACommit hashVersioning artifacts
$CI_PIPELINE_IDPipeline IDIdentifying a run
$CI_JOB_TOKENTemporary tokenAccessing the GitLab registry
$CI_PROJECT_DIRCheckout pathReferencing files
$CI_ENVIRONMENT_NAMEEnvironment nameDeployment scripts
build:
script:
- echo "Building branch $CI_COMMIT_BRANCH"
- docker build -t myapp:$CI_COMMIT_SHA .

Variable precedence

The problem: you define DATABASE_URL in the YAML, but also in the project settings. Which one wins?

GitLab applies a precedence hierarchy with nine levels. The diagram reads from top to bottom, left column then right column: each level wins over the one that follows it, from the pipeline variables (at the top) down to the predefined CI_* variables (last).

Precedence of GitLab CI/CD variables in nine levels: pipeline, project, group, instance, then dotenv, job, global YAML, deployment and predefined CI_*, each level winning over the next.Lower priority, continuedwins overwins overwins overwins over5 · dotenv variablesreports:dotenv6 · Job variablesjob variables:7 · Global variablestop-level variables:8 · Deployment variablesenvironments9 · Predefined variablesCI_*, injected by GitLabHigher prioritywins overwins overwins over1 · Pipeline variablesUI, API, triggers, schedules2 · Project variablesSettings › CI/CD3 · Group variablesclosest subgroup4 · Instance variablesself-managed only
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

The following list repeats the same levels, from highest to lowest priority:

  1. Pipeline variables (UI, API, triggers, schedules, downstream)
  2. Project variables (Settings > CI/CD)
  3. Group variables (the closest subgroup wins)
  4. Instance variables (self-managed only)
  5. Variables from dotenv reports (reports:dotenv artifacts)
  6. Job variables (in the job's variables:)
  7. Global YAML variables (root variables: section)
  8. Deployment variables (tied to environments)
  9. Predefined variables (CI_*)

In practice:

# Global YAML: DATABASE_URL = "dev-db"
variables:
DATABASE_URL: "dev-db"
test:
# If the project defines DATABASE_URL = "test-db" in Settings,
# "test-db" will be used (project > YAML)
script:
- echo $DATABASE_URL

Securing secrets

A poorly managed secret can compromise your whole infrastructure. Here are the rules to follow.

Rule 1: Never put a secret in the YAML

# ❌ DISASTER: the secret is in the Git history
variables:
API_TOKEN: "sk-1234567890abcdef"
# CORRECT: the secret comes from the GitLab interface
job:
script:
- curl -sS -H "Authorization: Bearer $API_TOKEN" https://api.example.com

Rule 2: Protected + Masked, systematically

For every secret:

  1. Protected: limits access to protected branches/tags
  2. Masked: prevents accidental display in the logs

Masked only acts on display, so you still have to avoid triggering that display. The masking example above is only a demonstration: in a real pipeline, never write echo "$API_TOKEN", and avoid verbose modes: curl -v prints the Authorization header with the token value, and set -x prints every command with its variables expanded. Call curl -sS instead (-s removes the progress meter and also the error messages, -S brings the latter back when -s is present): the job log stays readable. -sS does not protect a token on its own; it is the absence of a verbose mode, combined with Masked, that prevents the leak.

Rule 3: Environment scope whenever possible

You can limit a project variable to a specific environment (production, staging, or a wildcard such as review/*):

  1. Go to Settings > CI/CD > Variables
  2. In Environment scope, select the target environment
  3. The variable will only be injected for the jobs that use this environment
deploy_prod:
environment:
name: production
script:
- echo $PROD_SECRET # Available only here

Rule 4: Regular rotation

Change your secrets periodically. If a secret is compromised, the attacker only has a limited window.

Tip: use a secrets manager (HashiCorp Vault, Infisical (in French)) to automate rotation.

Rule 5: Prefer pipeline inputs (GitLab 17.7+)

Since GitLab 17.7, pipeline inputs (spec:inputs) offer a more governable alternative to pipeline variables for non-secret parameters. They let you define default values, validations and built-in documentation. Reserve CI/CD variables for real secrets.

Best practices

Separate environments

Use different variables for dev, staging and prod:

stages:
- test
- deploy
test:
stage: test
variables:
DATABASE_URL: $DEV_DATABASE_URL
script:
- pytest --db=$DATABASE_URL
deploy_prod:
stage: deploy
variables:
DATABASE_URL: $PROD_DATABASE_URL
environment:
name: production
script:
- deploy.sh
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual

Name things clearly

Use prefixes to identify origin and usage:

PrefixMeaning
DEV_, STAGING_, PROD_Target environment
SECRET_, KEY_, TOKEN_Sensitive data
APP_, DB_, API_Component concerned

Example: PROD_DB_PASSWORD, DEV_API_BASE_URL

Document the required variables

In your README or a dedicated file, list the variables to configure:

## Required CI/CD variables
| Variable | Type | Protected | Description |
|----------|------|-----------|-------------|
| `PROD_DEPLOY_KEY` | File | Yes | SSH key for the production server |
| `DOCKER_REGISTRY_TOKEN` | Var | Yes | Token to push to the registry |
| `SLACK_WEBHOOK_URL` | Var | No | Notifications (optional) |

Common mistakes

SymptomLikely causeSolution
Empty variable in the jobProtected variable, unprotected branchProtect the branch or remove the protection
Unexpected valuePrecedence: project overrides YAMLCheck Settings > CI/CD > Variables
Secret visible in the logsVariable not masked, or value transformed (encoded, split)Enable Mask variable, never print a transformed secret
$VAR printed literallySingle quotes instead of doubleUse "$VAR" not '$VAR'

Hands-on lab

The lab "Get the secret out of the repository" replays this guide on a GitLab CE forge of your own (a GitLab server you host yourself), set up on a virtual machine with its runner and its container registry (the server that stores Docker images), rather than on gitlab.com: no minute quota, no identity verification, and a project you can break as often as you need.

A database password sits in plain text in two places of the repository, and a job prints it in its log. The lab has you take it out, declare it as a masked and protected variable, then prove it no longer leaks. Grading is taken on three states the forge shows: the repository, the project variables and the job logs; a secret removed from the file but still visible in a log counts as a leak.

Three points the lab relies on are worth keeping in mind. One of the two places is the fallback value of an environment lookup in the code: the rule "never a secret in the YAML" applies there too, and a fallback only ever carries a harmless development value. When you declare the variable, put the password alone in it, not a whole connection URL: you mask the secret, not the address. And removing a secret from the default branch does not erase it from the Git history, which is why a secret that was ever committed must be rotated (Rule 4 above).

The lab starts with dsoxlab run gl-05-sortir-les-secrets, after dsoxlab provision prepares the virtual machine, its runner and its registry. Installing DSOXLab (the command-line tool that runs this site's labs) and its prerequisites are described in the DSOXLab documentation.

Key takeaways

  1. Environment variables for configuration, files for certificates/keys.
  2. Protected limits to protected branches, Masked hides in the logs, Hidden hides in the UI.
  3. Precedence: pipeline > project > group > instance > dotenv > job YAML > global YAML > deploy > CI_*.
  4. Never a secret in the YAML, use the GitLab interface.
  5. Never export or printenv without a filter, as it exposes all the secrets; no echo of a secret, no curl -v or set -x.
  6. Environment scope to isolate dev/staging/prod secrets.
  7. Mandatory MR review, a malicious MR can exfiltrate your secrets.

Test your knowledge

Knowledge check

Check what you have learned with this interactive quiz

10 questions
5 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn