Safeguard
Security Guides•

How do protected variables work in GitLab CI?

Protected variables reach only pipelines on protected branches or tags. Masking hides values in logs but is not a guarantee. Here is how to set both up.

Safeguard Security Team
4 min read

A protected GitLab CI/CD variable is available only to pipelines running on protected branches or protected tags, so a feature branch cannot read your production credentials. Masking is a separate setting that hides the value in job logs. GitLab says masking is not a guaranteed way to stop malicious users reading a value.

Three settings, three jobs

GitLab variables have several visibility controls. They do different things, and teams often mix them up.

SettingWhat it doesWhat it does not do
Protect variableLimits the variable to pipelines on protected branches and tagsDoes not hide it in logs
Mask variableReplaces the value with [MASKED] in job logsDoes not stop a script from sending it elsewhere
Hidden variableKeeps the value from being revealed again in the UI after savingDoes not control which branches see it

The docs also describe file type variables, which put the value in a temporary file and give the job the path. GitLab suggests external secrets and file type variables to keep commands such as env or printenv from printing secrets.

Set it up

  1. Make sure your release and production branches are protected. A protected variable is only useful if the branch rules are real.
  2. In the project or group, open Settings, then CI/CD, then Variables.
  3. Edit each production credential and tick Protect variable.
  4. Tick Mask variable too, and clear the Expand variable reference option, as GitLab recommends.
  5. Check the value meets masking rules: a single line with no spaces, at least 8 characters, and not matching the name of an existing variable.
  6. Put test credentials, which have limited access, in unprotected variables so feature branches can still run tests.

Know the limits of masking

GitLab's docs give a plain example. If a process outputs the value in a slightly modified way, for instance the shell adds backslashes to escape special characters, the value is not masked. And when CI_DEBUG_SERVICES is on, a value may be revealed.

So treat masking as a seatbelt for accidents, not a barrier against a person who can edit the pipeline file. The barrier is the protected branch.

A merge request detail

GitLab notes that merged results pipelines and merge request pipelines can optionally access protected variables. That setting decides whether code from an unmerged branch can see them. Review it if you run pipelines for merge requests, because it widens who can reach the secret.

Prefer short-lived credentials

Where the target service supports it, replace stored cloud keys with ID tokens. GitLab's docs describe ID tokens as JSON web tokens generated per job, with an audience claim you set with the id_tokens keyword so a token only works with the service it names. A stolen token then has a narrow audience and a short life. Our post on OIDC versus static credentials in CI covers the move in more depth.

Where Safeguard fits

Variable protection is a GitLab setting. Safeguard covers the code side: dependency and SBOM scanning, and policy gates that can stop a risky change before it reaches a protected branch.

Quick check

If a variable is masked, is it safe on any branch?

No. Masking only changes how logs display it. A script can still read and send the value, so also protect it.

Why does my deploy job say a variable is empty on a feature branch?

That is the point of protection. The variable is withheld from unprotected branches. Run the deploy job only on protected branches.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.