In GitLab CI/CD, what is the difference between a protected variable, a masked variable, and a file-type variable?
answer
- three flags, three unrelated problems
- one is access, one is logging, one is ergonomics
- absent, not empty, on the wrong ref
- the value becomes a path
basics
~20 sThey solve three unrelated problems. Protected controls which jobs receive the value at all — only jobs on protected branches or tags. Masked redacts the value if it is printed to the job log. File type writes the value to a temporary file and sets the variable to that file's path.
solid answer
~50 sThese three flags get conflated constantly, and they are independent. **Protected** is access control: GitLab exports the variable only to jobs running on a protected branch or protected tag, so a feature branch or a fork's pipeline simply does not receive it — the variable is absent, not empty. **Masked** is a logging backstop: the runner scans job output and replaces exact occurrences of the value with `[MASKED]`. It only works if the value qualifies — a single line, at least eight characters, no whitespace, and drawn from a restricted character set — and it protects nobody who can read the value another way. **File** is an ergonomics setting: instead of exporting the content, GitLab writes it to a temporary file on the runner and sets the variable to that file's path, which is what tools like `kubectl` and the Google Cloud SDK expect. You typically want production credentials protected *and* masked, and a kubeconfig or service-account JSON as a file variable.
go deeper
Be able to state the three in one sentence each: protected limits which branches get it, masked hides it in logs, file turns it into a path on disk.
Explain masking's eligibility rules and its limits, why a protected variable is absent rather than empty elsewhere, and which combination fits a kubeconfig versus an API token.
Show that masking is an accident backstop rather than a control, tie protection to actual branch protection strength, and prefer short-lived credentials where a stored value would be too broad.
Own the policy: which classes of credential may live as GitLab variables at all, who may modify protected refs, and where the organisation switches to federated, job-scoped credentials instead.
## Three flags, three problems In GitLab's Settings → CI/CD → Variables screen, each variable has a type (`Variable` or `File`) and checkboxes for `Protect variable` and `Mask variable`. Candidates routinely describe all three as "making the secret safe", which is wrong in a way that produces real incidents. ## Protected: who gets the value A protected variable is exported **only** to jobs whose pipeline runs on a protected branch or a protected tag. Everywhere else it does not exist. This is the mechanism that keeps production credentials away from ordinary feature branches, from merge request pipelines whose source branch is unprotected, and from contributors who can push a branch but cannot push to `main`. The most common support question follows directly: "my deploy job works on `main` but fails on my branch with an empty variable". That is protection working. The value is absent, and `"$TOKEN"` expands to the empty string, so the failure looks like a broken tool rather than a permissions decision. A defensive pipeline checks explicitly: ```yaml deploy: script: - test -n "$DEPLOY_TOKEN" || { echo "DEPLOY_TOKEN missing: is this a protected ref?"; exit 1; } - ./deploy.sh ``` Protection is only as strong as the branch protection behind it. If everybody can push to the protected branch, the variable is effectively public to everybody. ## Masked: what the log shows Masking happens on the runner, in the trace stream. As output flows to GitLab, the runner replaces occurrences of the value with `[MASKED]`. It is a safety net for the accidental `echo`, the verbose tool that prints its own arguments, and the stack trace that includes a connection string. GitLab only accepts a value as masked if it can reliably match it. The documented requirements are that the value is a single line, at least 8 characters long, contains no whitespace, is not itself built from another variable, and consists only of characters from the Base64 alphabet plus a small set of punctuation such as `@`, `:`, `.` and `~`. A passphrase with spaces, a multi-line PEM key, or a short password cannot be masked at all — the settings form refuses the combination. Newer GitLab versions add a **masked and hidden** option, where the value can never be read back in the UI after it is saved; that is a separate protection against a curious colleague, not against the log. Masking is a backstop, never access control. Anyone able to run a job on a ref that receives the variable can send the value anywhere they like. Treat it as protection against accidents only. ## File: where the value lives Some tools take a secret as a value; many take it as a *file path*. `KUBECONFIG`, `GOOGLE_APPLICATION_CREDENTIALS`, a CA bundle, an SSH key, a `.netrc` — all of these want a file. With a `File`-type variable, GitLab writes the content to a temporary file inside the job's working area and exports the variable containing that **path**. The classic confusion runs in both directions. If you mark a kubeconfig `File` and then run `echo "$KUBE_CONFIG" > config`, you write a path into the file instead of the config — you needed `cp "$KUBE_CONFIG" config` or nothing at all. If you mark a plain password `File` by mistake, the application authenticates with a string like `/builds/group/project.tmp/PASSWORD` and reports bad credentials. The file type is set in the settings UI or API; the `variables:` keyword in `.gitlab-ci.yml` defines plain values only. File variables are usually not maskable either, because certificates and JSON blobs contain whitespace and newlines. ## Combining them The useful combinations: - Production API token: **protected + masked**, plain type. - Kubeconfig or cloud service-account JSON: **protected + file**, masking unavailable. - A non-secret setting such as a registry hostname: neither. And note what none of them do: none of them encrypt anything on the runner, none restrict which *job* in a pipeline can read the value (any job on a qualifying ref gets it), and none survive a determined pipeline author. For a value that must never sit in GitLab at all, the answer is short-lived credentials fetched at job time rather than a stored variable.
- A job on a feature branch fails with an empty token that works on main. What happened?The variable is protected, so GitLab exports it only on protected branches and tags. On the feature branch it is not present at all and expands to an empty string, which surfaces as an authentication failure rather than a clear permissions message. Either run the job on a protected ref, or have the job assert the variable is non-empty and fail with a message that says so.
- Why can a passphrase like "correct horse battery" not be saved as a masked variable?Masking requires a value the runner can match reliably in the log stream: a single line, at least eight characters, no whitespace, and characters from a restricted set. Whitespace breaks that, so GitLab refuses the mask option. Either use a value that qualifies — a generated token rather than a phrase — or accept that the log has no safety net for it.
- When does a file-type variable actively cause a bug?Whenever code treats it as content. `echo "$KUBE_CONFIG"` prints a path, so redirecting it into a file produces a one-line file containing a path, and the tool then fails with a parse error. The inverse also bites: a plain password marked File makes the application send the temporary file's path as the password.
saying these in an interview costs you the question
- Saying masking stops a determined job from leaking the value
- Thinking protected means encrypted or hidden
- Expecting a protected variable to be empty rather than absent
- Treating a file variable as if it held the content
- Assuming any value can be marked masked