Safeguard
Application Security•

How do you handle secrets in GitLab CI without leaking them?

Store GitLab CI secrets as masked, protected variables or fetch them from an external manager at run time. Masking hides logs only; it does not stop theft.

Safeguard Security Team
5 min read

Keep GitLab CI secrets out of your repository, define them in the UI as masked and protected variables, and prefer short-lived credentials fetched from an external secrets manager at run time. Masking only scrubs job logs. It does not stop a malicious job script from sending a value elsewhere.

What does masking actually protect?

Masking replaces a variable's value with the text [MASKED] when a job prints it. GitLab's documentation is direct about the limit: if a process outputs the value in a slightly modified form, such as a shell adding backslashes before special characters, the value is no longer matched and appears in clear. The same page notes that enabling service debug logging might reveal a masked value.

So masking is a seatbelt for accidents. The documentation separates two failure modes: an accidental leak, where a script echoes a password while logging in, and a malicious job, where someone adds a line that posts the variable to a server they control. Masking helps with the first. Review and scoping are the only defence against the second.

Which variable settings matter most?

Three settings do most of the work.

Visibility. Current GitLab docs list Visible, Masked (the default) and Masked and hidden. The hidden option stops the value being shown again in the settings page, and it can only be chosen when you create a variable, not on an existing one. If you have old variables set to visible, recreate them.

Protected. A protected variable is only available to pipelines that run on protected branches or protected tags. Put deployment credentials here, so that a pipeline on a throwaway feature branch never receives them.

Environment scope. Premium and Ultimate let you limit a variable to a specific environment or a wildcard. A staging token should not exist in a production job and the reverse.

Also remember that a masked variable has format limits (a minimum length of 8 characters, among others), and a masked value cannot reference other variables.

Why prefer file variables and external secrets?

Variables are normally environment variables, and any step can run env or printenv. GitLab suggests file type variables and external secrets for this reason: the value sits in a temporary file path, so a casual dump does not print it.

Better still, do not store a long-lived credential at all. With the id_tokens keyword, a job can request a signed OpenID Connect token and exchange it with a secrets provider or cloud for short-lived access. The aud claim names the intended service, and the service can reject tokens whose audience does not match. A job token is only valid while the job runs, so a stolen copy expires with the job.

A constructed example

Imagine a team with a deploy job that uses a cloud access key stored as a plain variable at group level. Every project in the group can read it, merge request pipelines from any branch receive it, and the value is visible to any maintainer in settings.

After a review, they move to OIDC. The job declares an ID token with the cloud provider as audience. The cloud trust policy is bound to the project ID and to protected refs only. The old key is deleted. Now a pipeline on a feature branch gets nothing useful, and nothing durable sits in the CI settings to be copied.

What to do

  1. List every CI/CD variable at project, group and instance level and delete the ones nobody uses.
  2. Recreate any visible secret as masked and hidden.
  3. Mark deployment secrets as protected and scope them to one environment.
  4. Replace long-lived cloud keys with OIDC ID tokens and a trust policy tied to stable identifiers such as project ID.
  5. Never trigger a pipeline from a merge request you have not read, since malicious code can reach both masked and protected values.
  6. Run runners securely: avoid privileged Docker mode on reused machines, and avoid the shell executor when jobs share a host.
  7. Scan the repository and its history for committed secrets so a leaked value is found before an attacker finds it.

Safeguard includes secrets scanning as part of its scanners, and you can try the flow in the secrets scanning demo.

Quick check

Is a masked variable safe from a malicious merge request?

No. Masking only edits job logs. A job script can still read the variable and send it out, which is why review of pipeline changes and protected variables matter.

What is the safest way to give a job cloud access?

Use an OIDC ID token with a trust policy bound to the project and protected branches, so the job gets short-lived access and no stored key exists to steal.

Sources

Last reviewed: 18 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.