skip to content

In GitLab CI, what does the secrets: keyword do, and why might your application receive a file path instead of the secret value?

level: seniorimportance: nice to knowfreq 28%

answer

  1. the runner fetches it, before your script
  2. one authoritative store, no copy in GitLab
  3. authenticated by the job's own ID token
  4. the default delivery is a file
  5. set the flag when the app wants a value

basics

~20 s

secrets: tells GitLab Runner to fetch a value from an external secret manager before the script runs and expose it as a CI/CD variable. By default the value is delivered as a file variable, so the variable holds a temporary file's path rather than the secret itself.

solid answer

~50 s

A `secrets:` entry names a CI/CD variable and says where its value comes from — a Vault path, or an Azure, GCP or AWS secret-manager entry — plus a `token:` that points at an ID token declared with `id_tokens:`. GitLab Runner authenticates to the manager with that JWT, retrieves the value and makes it available to the job before the first script line runs, so nothing is stored in project settings. The gotcha is the `file:` key: `secrets:` entries default to **file** delivery, meaning the runner writes the value to a temporary file and sets the variable to that path. An application that reads the variable as a password then tries to authenticate with something like `/builds/group/project.tmp/DATABASE_PASSWORD`. Set `file: false` when the application wants the value inline, or leave the default and have the job read the file.

go deeper

for a junior

Know that secrets: pulls a value from an external secret store at job time instead of storing it in the project's CI/CD settings.

for a middle

Explain that the runner fetches the value before the script runs, authenticated by an id_tokens JWT named in token:, and that delivery defaults to a file variable.

for a senior

Diagnose the file-versus-value mismatch from its symptom, and weigh the added runtime dependency and trust-policy maintenance against keeping a copy of the credential in GitLab.

for a principal

Own where credentials live organisation-wide: which teams get an external manager as the single source of truth with per-job audit, and which stay on protected project variables because the operational cost is not justified.

## The shape of the keyword ```yaml deploy: id_tokens: VAULT_ID_TOKEN: aud: https://vault.example.com secrets: DATABASE_PASSWORD: vault: production/db/password@ops token: $VAULT_ID_TOKEN file: false script: - ./deploy.sh ``` Three things are happening. `DATABASE_PASSWORD` is the variable the job will see. `vault:` names the path within the secret store, in GitLab's shorthand where the part after `@` is the mount. `token:` names the ID token the runner should present to authenticate. Alongside `vault:`, GitLab supports entries for Azure Key Vault, Google Secret Manager and AWS Secrets Manager, each with their own key and their own set of connection variables. Connection details for Vault come from variables the runner reads: the server URL, and optionally the auth path and role. Those are configuration, not secrets — which is the point, because there is no static credential anywhere in this pipeline. ## Who does the fetching, and when The **runner** resolves `secrets:` entries, not the GitLab server, and it does so before the job's script begins. The value is present in the environment from the first line. That ordering matters when you are debugging: a failure to resolve a secret fails the job before any of your commands run, so the error comes from the runner rather than from your script. ## The file-by-default trap This is the part that produces the support ticket. `secrets:` entries are delivered as **file**-type variables unless you say otherwise. So `$DATABASE_PASSWORD` holds a path, and code that treats it as a password sends the path as the password. The symptom is an authentication failure with a nonsense credential, and the log — if anything is printed at all — shows a plausible-looking temporary path rather than an obviously wrong value. The two correct responses: - Add `file: false` when the tool wants the value inline as an environment variable. - Keep the default and have the job use the file, which is what you want for a kubeconfig, a certificate, a service-account JSON or anything multi-line. Read it with `cat "$VAR"` or point the tool at the path; do not `echo` the variable expecting content. The default is not arbitrary: writing a secret to a file avoids it appearing in the process environment, which is visible to anything running in the container and often ends up in crash dumps and diagnostic output. ## Why use it rather than a project variable A project CI/CD variable is a copy. Once the value is pasted into GitLab, it lives in two systems, rotates on two schedules, and is readable by everyone with access to a job on a qualifying ref, forever. With `secrets:`, the value stays in one authoritative store, rotation happens in one place, the fetch is authenticated per job by a token that expires with the job, and the store's own audit log records which pipeline read which secret and when. The costs are real too: the runner needs network reachability to the manager, an outage there fails every deploy, the trust policy on the manager side has to be written and reviewed, and the feature is available on paid GitLab tiers. For a small project with two credentials, protected and masked project variables remain a defensible choice. ## Interaction with masking A value fetched at job time is not automatically covered by masking rules configured in project settings, and a file-delivered secret is not a candidate for masking anyway. The usual disciplines still apply: never echo it, keep it out of artifacts and reports, and prefer the file form so it is not sitting in the environment of every child process.

  • A deploy fails with an authentication error and the log shows a temporary path where a password should be. What is wrong?
    The `secrets:` entry is being delivered as a file variable, which is the default, so the variable holds the path to a temporary file rather than the secret. Either add `file: false` so the value is exported inline, or change the application to read the file at that path. Nothing is wrong with the fetch itself.
  • What does the token: key on a secrets: entry point to?
    An ID token declared by the same job's `id_tokens:` block — a short-lived JWT GitLab signs for that job. The runner presents it to the secret manager, which verifies the signature and the claims against its own trust policy before returning the value. Without it the runner has no way to authenticate, and no static credential is involved.
  • When is a protected, masked project variable still the better choice over secrets:?
    When the operational cost outweighs the benefit: a small project with one or two credentials, no existing secret manager, or runners that cannot reach one. `secrets:` adds a hard runtime dependency — if the manager is unreachable, every deploy fails before its first command — plus a trust policy to maintain, and it needs a paid GitLab tier.

saying these in an interview costs you the question

  • Expecting the variable to hold the value by default
  • Thinking the GitLab server fetches the secret at pipeline creation
  • Believing secrets: needs a static token stored in project settings
  • Assuming a fetched secret is automatically masked in logs
  • Echoing a file-delivered secret and wondering why a path prints

context