CI/CD variables, masking & secret managers
Secrets that do not leak into logs or forks.
Pipelines need secrets, and GitLab’s CI/CD variables are the built-in home for them — set in project settings, injected as environment variables into jobs, and (crucially) never committed to the repo. But the defaults are not enough on their own: a variable must be marked masked so it is hidden in job logs, and protected so only protected-ref pipelines can read it. Both flags, on every sensitive variable, is the baseline.
Masking is limited — do not fight it
Masking replaces the exact secret string with [MASKED] in logs, but like Jenkins it is best-effort: it only catches the literal value, so a secret that is base64-encoded, split, or transformed before printing slips through. It also has format constraints (minimum length, no newlines). Treat masking as a safety net, not a strategy — never deliberately echo a secret, and never write it to a file that gets artifacted or cached.
deploy:script:# GOOD: pass the masked variable straight to the tool via stdin/env- echo "$REGISTRY_TOKEN" | docker login -u ci --password-stdin registry.acme.internal# BAD: any of these defeat masking / leak the secret# - echo "$REGISTRY_TOKEN" # printed (masking may still catch it)# - echo "$REGISTRY_TOKEN" | base64 # transformed — masking misses it# - env > debug.txt # writes the secret to a file
The better pattern: short-lived, external secrets
A stored CI variable is a static, long-lived secret — better than a committed one, but still a standing credential. The stronger pattern fetches secrets at run time from a manager (HashiCorp Vault, or a cloud secrets manager) so they are short-lived and centrally rotated, or uses OIDC: the job presents its GitLab identity to the cloud/Vault and receives a short-lived token, with no long-lived secret stored in GitLab at all. GitHub Actions and Jenkins have the same OIDC pattern — it is the modern default.
deploy:id_tokens:VAULT_ID_TOKEN: { aud: https://vault.acme.internal }script:- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=ci jwt=$VAULT_ID_TOKEN)"- vault kv get -field=password secret/registry # short-lived, audited, nothing stored in GitLab