GitLab CI/CD Variables & Secrets: The Complete Guide (2026)
Master GitLab secrets and CI/CD variables: masked vs protected variables, file type variables, precedence, Vault integration, and best practices for 2026.
GitLab CI/CD Variables & Secrets: The Complete Guide
GitLab CI/CD variables are how you get API keys, database passwords, and deploy tokens into your pipelines without hardcoding them in .gitlab-ci.yml. Used well, they keep secrets out of your repository and out of your job logs. Used carelessly, they become the easiest way to leak credentials in an organization — one echo $AWS_SECRET_ACCESS_KEY in a debug commit and your secret is sitting in a job log forever.
This guide covers everything you need to manage GitLab secrets properly in 2026: the different variable types and scopes, masked vs protected vs file variables, the exact precedence order GitLab uses when variables collide, the native HashiCorp Vault and OIDC integrations, and the honest limitations you'll hit when your team grows beyond a handful of projects.
If you're new to the broader topic, our primer on what secrets management actually is is a good starting point. Here, we go deep on GitLab specifically.
GitLab CI/CD Variable Types and Scopes
GitLab lets you define variables at five levels. Where you define a variable determines who can see it, which pipelines receive it, and what happens when the same key is defined twice.
| Scope | Where it's defined | Who can manage it | Typical use |
|---|---|---|---|
| Instance | Admin Area > Settings > CI/CD (self-managed only) | Instance administrators | Org-wide registry credentials, proxy settings |
| Group | Group Settings > CI/CD > Variables | Group owners/maintainers | Shared cloud credentials across all projects in a group |
| Project | Project Settings > CI/CD > Variables | Project maintainers | Project-specific API keys, deploy tokens |
.gitlab-ci.yml | variables: keyword in the pipeline file | Anyone who can push code | Non-sensitive config: image tags, flags, paths |
| Pipeline/job | Manual run form, schedules, API triggers, downstream pipelines | Anyone who can trigger pipelines | One-off overrides for a single run |
Two scope details matter more than people expect:
- Group variables inherit downward. A variable set on a parent group is available to every project in every subgroup. If a subgroup defines the same key, the closest subgroup wins. This is powerful for sharing credentials — and equally powerful for accidentally over-sharing them with projects that shouldn't have production access.
- Environment scopes let you restrict a variable to specific environments (
production,staging,review/*). Project-level environment scoping is available on all tiers; group-level environment scoping requires Premium or Ultimate, as of June 2026.
There are also limits to be aware of: variable values are capped at 10,000 characters, projects support up to 8,000 variables, and keys may only contain letters, numbers, and underscores.
Important: anything defined in
.gitlab-ci.ymlitself is committed to the repository and visible to everyone with read access. Thevariables:keyword is for configuration, never for secrets.
Masked vs Protected vs File Variables
These three settings are the heart of GitLab secrets hygiene, and they solve three different problems. They're often confused, so let's separate them clearly.
Masked variables
Problem solved: secrets appearing in job logs.
When a variable is masked, GitLab replaces its value with [MASKED] in job output. To be maskable, a value must meet certain requirements: at least 8 characters, a single line, and limited to a restricted character set (Base64-style characters plus a few extras like @, :, . — check GitLab's masking docs for the current exact rules).
Two caveats:
- Masking is log redaction, not access control. Anyone who can run a job that receives the variable can exfiltrate it — encode it, split it, write it to an artifact. GitLab's own documentation states masking is "not a guaranteed way to prevent malicious users from accessing variable values."
- Multi-line secrets can't be masked. Private keys and JSON service-account files don't qualify. Use a file variable instead (and base64-encode if you need masking on top).
GitLab 17.4 added masked and hidden variables: in addition to log masking, the value can never be revealed in the UI again after creation. As of GitLab 18.3, new project, group, and instance variables default to Masked visibility — a sensible hardening of the defaults.
Protected variables
Problem solved: any branch being able to access production secrets.
A protected variable is only injected into pipelines running on protected branches or protected tags. Without protection, anyone who can push a branch to your repository can write a job that prints your variables — that's the single most common GitLab secrets leak.
The workflow: protect your main branch and release tags (Settings > Repository), mark every production credential as protected, and production secrets simply never exist in feature-branch pipelines.
File variables
Problem solved: tools that want a file path, not an environment variable.
With Type: File, GitLab writes the value to a temporary file on the runner and sets the variable to that file's path. This is the right shape for kubeconfigs, GCP service-account JSON, certificates, and .npmrc files:
deploy:
stage: deploy
script:
# KUBECONFIG is a file-type variable: the env var holds a path
- kubectl --kubeconfig "$KUBECONFIG" apply -f k8s/
The three settings combine. A production kubeconfig should typically be file type + protected; a production API token should be masked (and hidden) + protected.
How to Add and Use GitLab Secrets
Adding a variable in the UI
- In your project or group, go to Settings > CI/CD and expand Variables.
- Select Add variable.
- Enter the Key (e.g.
PROD_API_TOKEN) and Value. - Choose Type: Variable (default) or File.
- Choose Visibility: Masked, or Masked and hidden for write-only secrets.
- Check Protect variable for production credentials.
- Set an Environment scope if the secret belongs to one environment.
- Save.
(Exact UI labels shift between releases — this reflects GitLab as of June 2026.)
Using variables in .gitlab-ci.yml
Variables are injected as environment variables in the job's shell. Reference them with $VARIABLE_NAME:
stages:
- test
- deploy
variables:
# Non-sensitive config only — this file is in version control
NODE_ENV: "production"
DEPLOY_REGION: "eu-west-1"
test:
stage: test
image: node:22
script:
- npm ci
- npm test
deploy_production:
stage: deploy
image: alpine:latest
environment: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
# PROD_API_TOKEN is a masked + protected project variable
- apk add --no-cache curl
- >
curl --fail --silent
--header "Authorization: Bearer $PROD_API_TOKEN"
"https://api.example.com/deploy?region=$DEPLOY_REGION"
A few practical notes:
- Quote variables in shell commands (
"$VAR") to survive spaces and special characters. - GitLab also provides predefined variables (
CI_COMMIT_SHA,CI_JOB_TOKEN,CI_ENVIRONMENT_NAME, and many more).CI_JOB_TOKENis itself a short-lived secret that authenticates to the GitLab API, registry, and package repositories — prefer it over personal access tokens inside pipelines. - Never
echoa secret, even masked ones. Masking protects logs, but transformed output (base64, reversed, split) is not detected.
Variable Precedence: Who Wins When Keys Collide
When the same key is defined in multiple places, GitLab resolves it in a strict order. From highest to lowest precedence, as of June 2026:
- Pipeline execution policy variables
- Scan execution policy variables
- Pipeline variables (manual run form, schedules, API/trigger variables, downstream and manual job variables)
- Project variables
- Group variables (closest subgroup wins)
- Instance variables
- Variables from dotenv artifact reports
- Job-level variables in
.gitlab-ci.yml - Top-level (default) variables in
.gitlab-ci.yml - Deployment variables
- Predefined CI/CD variables
The practical takeaways: UI-defined settings beat your YAML, project beats group, and someone manually triggering a pipeline can override almost everything. That last one is a real attack surface — which is why GitLab 18.x added group-level controls to disable pipeline-supplied variables in projects that don't need them. If your pipelines don't consume trigger variables, turn them off.
External Secrets: Vault, OIDC, and ID Tokens
Storing secrets directly in GitLab works, but the values live in GitLab's database and are injected as plain environment variables. For stronger guarantees, GitLab (Premium and Ultimate tiers) integrates natively with external secret managers: HashiCorp Vault, Google Cloud Secret Manager, Azure Key Vault, and AWS Secrets Manager, as of June 2026.
The mechanism behind all of them is ID tokens — short-lived OIDC JWTs that GitLab mints per job. The external provider verifies the token and returns the secret. No long-lived credential is ever stored in GitLab:
deploy:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
DATABASE_PASSWORD:
vault: production/db/password@ops # path/field@mount
token: $VAULT_ID_TOKEN
file: false # default is a file path; false injects the raw value
script:
- ./deploy.sh
You configure VAULT_SERVER_URL (and optionally VAULT_AUTH_ROLE, VAULT_AUTH_PATH) as ordinary CI/CD variables, and Vault is configured to trust GitLab's OIDC issuer. Note that secrets: values are delivered as file-type variables by default — set file: false if your tooling expects the raw value.
Even on the Free tier, you can use id_tokens manually with any provider that accepts OIDC JWTs — you just write the token exchange yourself with the provider's CLI. This is the same keyless pattern we cover for GitHub in our GitHub Actions secrets guide.
Best Practices and Honest Limitations
Best practices
- Mask and protect every production secret. Masked (ideally masked and hidden) for log safety, protected so feature branches never receive it.
- Scope variables to environments. A
production-scoped variable can't leak into a review-app job. - Put shared credentials at the group level, sparingly. Inheritance is convenient, but every inherited production credential widens your blast radius. Audit what each subgroup actually needs.
- Prefer OIDC/ID tokens over static cloud keys. A static
AWS_SECRET_ACCESS_KEYin GitLab is a standing liability; a per-job ID token expires in minutes. - Restrict who can run pipelines on protected branches. Protected variables are only as strong as your branch protection rules.
- Rotate on departure and on any suspected leak. GitLab can't rotate secrets for you — see our secrets management best practices for a rotation playbook.
- Disable pipeline variables where unused. Removes a precedence-based override and injection vector.
Limitations to be aware of
- No native versioning or rollback for variables. If someone overwrites
DATABASE_URL, the old value is gone. There's no diff, no history of values, no restore. - Coarse access control. Variable management maps to GitLab roles (Maintainer+). You can't grant someone "may edit staging variables but only view production keys."
- Audit visibility is limited. Audit events tell you a variable changed and who changed it, but reconstructing what your config looked like last Tuesday is on you.
- GitLab-shaped silo. Variables live per project/group, in GitLab. The same
STRIPE_SECRET_KEYyour team uses in GitLab CI also lives in Vercel, in someone's local.env, maybe in a Kubernetes secret — each copy updated by hand, each one a chance to drift. - External integrations require Premium/Ultimate. The native Vault/cloud secret manager integrations aren't on the Free tier, as of June 2026.
For small teams with one or two projects, none of this bites hard. At ten projects across three platforms, it does.
Where a Dedicated Secrets Manager Fits
GitLab CI/CD variables answer "how do I get a secret into this pipeline?" They don't answer "where is the source of truth for this secret, who touched it, and is it the same everywhere we deploy?"
That's the gap a tool like EnvManager fills alongside GitLab:
- One source of truth, synced everywhere. Keep secrets in EnvManager and feed GitLab via
.envimport/export or the CLI's real-time sync — the same store also pushes to GitHub Actions, Vercel, Railway, Render, Dokploy, Coolify, AWS Secrets Manager, and GCP Secret Manager. Update a key once instead of in five dashboards. - Client-side encryption. Values are encrypted with AES-256-GCM in your browser before they're stored — not just encrypted at rest server-side.
- Real RBAC. Admin, Editor, and Viewer roles per environment, instead of "Maintainer can edit everything."
- Audit logs and history. Every read and change is logged, and previous values aren't lost when someone overwrites a variable.
Pricing is deliberately simple: a 14-day free trial (no credit card), then a flat $9/month ($7.50/month billed annually) for your whole team — unlimited members, no per-seat charges. Enterprise adds SAML SSO and self-hosting. See pricing for details, or compare options in our roundup of the best secrets management tools.
Start your 14-day free trial and make GitLab one consumer of your secrets instead of one of their many homes.
FAQ
What's the difference between masked and protected variables in GitLab?
Masked variables have their values replaced with [MASKED] in job logs, protecting against accidental exposure in output. Protected variables are only passed to pipelines running on protected branches or tags, preventing arbitrary branches from accessing them. They address different risks, and production secrets should normally be both.
How do I hide a secret in GitLab CI/CD?
Add it under Settings > CI/CD > Variables with visibility set to Masked (or Masked and hidden, which also prevents the value from ever being revealed in the UI), enable Protect variable, and never define it in .gitlab-ci.yml, which is stored in version control.
Why is my GitLab variable not being masked?
The value probably doesn't meet GitLab's masking requirements: at least 8 characters, a single line, and a restricted character set. Multi-line values such as private keys cannot be masked — store them as file-type variables, or base64-encode them first.
What is the precedence order for GitLab CI/CD variables?
From highest to lowest: pipeline/security policy variables, pipeline-trigger variables, project variables, group variables, instance variables, dotenv report variables, job-level YAML variables, top-level YAML variables, deployment variables, and finally predefined variables. In short: UI settings override your YAML, and project overrides group.
Can GitLab pull secrets from HashiCorp Vault?
Yes. On Premium and Ultimate tiers, the secrets:vault keyword combined with id_tokens fetches secrets from Vault at job runtime using short-lived OIDC tokens, so no long-lived Vault credential is stored in GitLab. Native integrations also exist for AWS Secrets Manager, Google Cloud Secret Manager, and Azure Key Vault.