Back to blog

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.

June 12, 2026by EnvManager Team
gitlabci-cdsecrets-managementdevopssecurity

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.

ScopeWhere it's definedWho can manage itTypical use
InstanceAdmin Area > Settings > CI/CD (self-managed only)Instance administratorsOrg-wide registry credentials, proxy settings
GroupGroup Settings > CI/CD > VariablesGroup owners/maintainersShared cloud credentials across all projects in a group
ProjectProject Settings > CI/CD > VariablesProject maintainersProject-specific API keys, deploy tokens
.gitlab-ci.ymlvariables: keyword in the pipeline fileAnyone who can push codeNon-sensitive config: image tags, flags, paths
Pipeline/jobManual run form, schedules, API triggers, downstream pipelinesAnyone who can trigger pipelinesOne-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.yml itself is committed to the repository and visible to everyone with read access. The variables: 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:

  1. 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."
  2. 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

  1. In your project or group, go to Settings > CI/CD and expand Variables.
  2. Select Add variable.
  3. Enter the Key (e.g. PROD_API_TOKEN) and Value.
  4. Choose Type: Variable (default) or File.
  5. Choose Visibility: Masked, or Masked and hidden for write-only secrets.
  6. Check Protect variable for production credentials.
  7. Set an Environment scope if the secret belongs to one environment.
  8. 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_TOKEN is itself a short-lived secret that authenticates to the GitLab API, registry, and package repositories — prefer it over personal access tokens inside pipelines.
  • Never echo a 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:

  1. Pipeline execution policy variables
  2. Scan execution policy variables
  3. Pipeline variables (manual run form, schedules, API/trigger variables, downstream and manual job variables)
  4. Project variables
  5. Group variables (closest subgroup wins)
  6. Instance variables
  7. Variables from dotenv artifact reports
  8. Job-level variables in .gitlab-ci.yml
  9. Top-level (default) variables in .gitlab-ci.yml
  10. Deployment variables
  11. 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

  1. Mask and protect every production secret. Masked (ideally masked and hidden) for log safety, protected so feature branches never receive it.
  2. Scope variables to environments. A production-scoped variable can't leak into a review-app job.
  3. 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.
  4. Prefer OIDC/ID tokens over static cloud keys. A static AWS_SECRET_ACCESS_KEY in GitLab is a standing liability; a per-job ID token expires in minutes.
  5. Restrict who can run pipelines on protected branches. Protected variables are only as strong as your branch protection rules.
  6. 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.
  7. 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_KEY your 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 .env import/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.

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Start your free trial

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.