Learn GitLab CI/CD - Variables, Masks, & Secret Management
Episode 5 of 21

Learn GitLab CI/CD - Variables, Masks, & Secret Management

Managing the values in a pipeline: GitLab's built-in predefined variables and custom CI/CD variables, understanding the protected, masked, and expand security flags, then retrieving secrets securely from HashiCorp Vault directly in .gitlab-ci.yml with id_tokens.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 4 you learned to control when jobs and pipelines run with rules and workflow. Now for the most important question in production pipelines: how does the pipeline "know" things about its context — which commit, what branch, what build number — and how do you store sensitive data like API keys or database passwords without leaking them?

The answer lies in variables and secret management. Understanding both is the point where your pipeline changes from "a script GitLab runs" into "a smart and secure program".

Main Discussion

Predefined Variables

GitLab injects dozens of built-in variables into every job. Their values don't need to be set — GitLab fills them in automatically. The most commonly used:

VariableContent
$CI_COMMIT_SHAHash of the commit being processed
$CI_COMMIT_REF_NAMEName of the branch or tag being processed
$CI_COMMIT_BRANCHBranch name (empty for tags)
$CI_PROJECT_DIRLocation of the repository checked out on the runner
$CI_PROJECT_PATHProject path, e.g. mygroup/my-app
$CI_PIPELINE_IDUnique pipeline ID
$CI_PIPELINE_SOURCEPipeline source: push, merge_request_event, schedule, etc.
$CI_JOB_TOKENJob-specific token for authenticating to the GitLab API

A real-world example — naming Docker images uniquely per commit:

Predefined variables for naming images
build:
  stage: build
  script:
    - echo "Image tag: $CI_COMMIT_SHA"
    - echo "Branch  : $CI_COMMIT_REF_NAME"

Imagine how dangerous it would be if image tags weren't unique: two different commits could overwrite the same image and deployments become misleading. With $CI_COMMIT_SHA, every build has an identity that can't be confused.

Custom CI/CD Variables

Besides predefined variables, you can create your own. There are three places with different priorities:

  • Project SettingsSettings → CI/CD → Variables. Applies to one project, and values can be changed from the UI without a commit. Values here override values in the file.
  • Group SettingsGroup → Settings → CI/CD → Variables. Applies to all projects in one group; can be overridden at the project level.
  • The .gitlab-ci.yml file — declared with the variables keyword, either globally or per job.
Declaring variables in .gitlab-ci.yml
variables:
  APP_ENV: production
  MAX_RETRY: 3
 
deploy:
  stage: deploy
  variables:
    DEPLOY_USER: deployer
  script:
    - echo "Environment: $APP_ENV"
    - echo "User: $DEPLOY_USER"

The global variables applies to all jobs, while variables inside a job overrides the global value for that job only. Remember the priority order: variables from the UI project settings win, then group settings, then those in the file.

Security Flags: Protected, Masked, Expand

When creating a variable from the project settings UI, there are three toggles you must understand:

  • Protected — the variable is only injected into pipelines running on protected branches or tags (usually main). This prevents production values from leaking into feature-branch pipelines that are still being developed carelessly.
  • Masked — the variable's value is shown as asterisks in the job output log. It requires at least 8 characters and must not start or end a line. Protects against log exposure, but isn't a substitute for encryption.
  • Expand — controls whether the variable can use other variables inside its value. For example a value of REGISTRY_IMAGE=$CI_REGISTRY_IMAGE/my-app only works if Expand is enabled.
FlagPurpose
ProtectedOnly for protected branches and tags
MaskedHidden (asterisks) in output logs
ExpandAllows variable expansion within values

Warning

Masked is not encryption — the value remains visible to users who have access to the project. For secrets that need centralized rotation and strict auditing, don't store them in GitLab variables. Use an external secret manager like HashiCorp Vault.

External Secret Manager Integration: HashiCorp Vault

The problem with CI/CD variables: secrets are stored in GitLab, spread across many projects, hard to rotate, and with minimal auditing. The industry solution is HashiCorp Vault — a centralized secret manager where secrets are stored and retrieved according to policy. GitLab can fetch secrets directly from Vault while a job runs, without ever storing the secret value in GitLab.

Setting Up a JWT Auth Role in Vault

The first step on the Vault side: enable JWT auth and create a role that binds a specific GitLab project:

Set up a JWT auth role for GitLab in Vault
vault auth enable jwt
 
vault write auth/jwt/role/gitlab-ci \
  bound_issuer="https://gitlab.example.com" \
  bound_audiences="https://gitlab.example.com" \
  user_claim="sub" \
  bound_subject="project_path:myorg/myapp:*" \
  token_policies="ci-app" \
  token_ttl="10m"

A quick explanation: bound_issuer and bound_audiences match the JWT claims issued by GitLab, while bound_subject binds the role to the myorg/myapp project — the * at the end allows all branches of that project.

Configuring the Vault Server in GitLab

On the GitLab side, tell it where Vault is and which role to use. Go to Project → Settings → CI/CD → Vault server, and fill in:

FieldExample Value
Vault server URLhttps://vault.example.com:8200
Vault auth rolegitlab-ci

After that, write .gitlab-ci.yml with id_tokens and the secrets keyword:

id_tokens and secrets for retrieving secrets from Vault
deploy:
  stage: deploy
  image: node:20-alpine
  id_tokens:
    VAULT_JWT_TOKEN:
      aud: https://gitlab.example.com
  secrets:
    DB_PASSWORD:
      vault: kv/data/ci/app DB_PASSWORD@kv
    API_KEY:
      vault: kv/data/ci/app API_KEY@kv
      file: false
  script:
    - npm run deploy

Three important things are happening here:

  • id_tokens creates an OIDC JWT with a specific aud claim. The runner uses this token to log in to Vault — no long-lived credentials are stored anywhere.
  • The secrets: keyword tells the runner: fetch this secret from Vault and turn it into an environment variable.
  • The vault: format is vault: <path> <field>@<mount>. For example kv/data/ci/app DB_PASSWORD@kv means reading the DB_PASSWORD field from the kv/data/ci/app path on the kv mount.

When the job runs, DB_PASSWORD and API_KEY are available as environment variables — the repository stores no secrets at all. Rotating a secret is simply done in Vault, and all pipelines automatically use the new value.

Tip

For long secrets or ones containing strange characters, use file: true on the secrets: keyword. The secret value is written to a temporary file instead of an environment variable, reducing the risk of its value being printed to the log.

Common Mistakes

  1. Masked less than 8 characters. GitLab rejects masked variables that are too short — set your secret values to satisfy the requirement.
  2. Storing secrets inside .gitlab-ci.yml. This file lives in the repository and leaks to anyone with repo access. Use UI variables or Vault for sensitive things.
  3. The aud claim doesn't match. If the aud in id_tokens doesn't match the role's bound_audiences in Vault, authentication fails with an access denied error. Make sure both are exactly the same.

Closing

In this episode 5, you've mastered the value system in pipelines:

  • Predefined variables like $CI_COMMIT_SHA, $CI_COMMIT_REF_NAME, $CI_PROJECT_DIR, and $CI_PIPELINE_ID.
  • Custom CI/CD variables in project settings, group settings, and .gitlab-ci.yml with a clear priority order.
  • Security flags: Protected (protected branch/tag only), Masked (hidden in logs), and Expand (variable expansion).
  • HashiCorp Vault integration with id_tokens and the secrets keyword for secure, centralized, easily rotated secrets.

Your pipeline can now talk, store configuration, and keep secrets safe. In episode 6 we'll move into container integration — Docker in GitLab CI/CD: running jobs with custom images, comparing Docker-in-Docker vs Kaniko, and publishing images to the GitLab Container Registry. See you in episode 6!

Learn GitLab CI/CD - Variables, Masks, & Secret Management | Learn GitLab CI/CD