Protected Variables, Masked Variables and Environment Scopes Explained
AI generated
CI/CD
.yml
GitLab · Variables · Secrets · Environment Scopes
Protected, Masked and Environment Scopes
GitLab CI/CD Variables Explained Properly

The three checkboxes when creating a GitLab variable look harmless, but the combination you choose for each variable determines the security of the entire deployment process. Protected, Masked and Environment Scope mean three different things that get confused constantly.

10 min read Protected · Masked · Scope · File-Type · Group Variables GitLab 16+ · Magento 2.4

1. Protected, Masked and Scope: Three Different Concepts

The most common misunderstanding around GitLab CI/CD variables is that Protected and Masked mean the same thing. They do not. Protected controls which branches and tags a variable is available on. Masked controls whether the variable's value is shown in the job log or hidden. Environment Scope controls which deployment environments a variable gets injected into. These three dimensions can be configured independently, and together they form the security model.

A concrete example: an SSH private key for production should be Protected (only available on main and tags), Masked (no plaintext in the log) and scoped to the production environment. If any one of these three properties is missing, a gap opens up: without Protected the key ends up in feature-branch pipelines, without Masked it is readable in the job log, without a scope it could affect staging jobs where it is not needed at all.

Understanding these three concepts is the foundation of a secure GitLab deployment process. Teams that mix them up end up configuring variables either too openly, which is a security risk, or too restrictively, which causes pipeline jobs to fail because variables are not available where they are expected. Both mistakes are common in practice and both are easy to avoid once the distinction is clear.

2. Protected Variables: Only on Protected Branches

A Protected Variable is only available in jobs that run on protected branches or protected tags. In GitLab, branches are marked as protected under Settings → Repository → Protected Branches. The typical setup for Magento: main and release/* as protected branches, v* as protected tags. A variable with the Protected flag is not available on feature branches at all, which prevents a developer running a feature-branch pipeline from ever touching production credentials.

The most important use case: production SSH keys, production database passwords and production API keys should always be created as Protected Variables. That makes them invisible on feature branches, even if someone tries to read them out with echo $VARIABLE in a job. Only jobs on main or on tags can see these variables. This restriction is not a convenience feature, it is an active safeguard against credential leakage through malicious or accidental code in merge requests.

# Variables configuration: protection levels for Magento deployment
variables:
  # Non-sensitive: available on all branches, no masking needed
  DEPLOY_PATH: /var/www/magento
  RELEASE_RETENTION: "5"
  MAGENTO_LOCALE: de_DE

# Protected + Masked + Production-scoped variables (set in GitLab UI):
# SSH_PRIVATE_KEY       : Protected: yes, Masked: yes, Scope: production
# COMPOSER_AUTH         : Protected: yes, Masked: yes, Scope: *
# DB_PASSWORD_PROD      : Protected: yes, Masked: yes, Scope: production
# DB_PASSWORD_STAGING   : Protected: no,  Masked: yes, Scope: staging

# Usage in deploy job, SSH_PRIVATE_KEY only injected on protected branches
deploy:production:
  stage: deploy
  environment:
    name: production
  rules:
    # Only runs on protected tags, Protected Variable will be injected
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
  before_script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - echo "Deploying to production..."

3. Masked Variables: No Plaintext in the Job Log

Masked Variables are replaced with [MASKED] in the job log whenever their value would otherwise appear in the output. This protects against secrets being accidentally displayed in pipeline logs that other team members can view. It is important to understand that masking is not a complete safeguard: if the secret value appears in a different format in the log, for example base64-encoded or split across lines, it will not be masked. Masking is one layer of security, not an absolute guarantee.

There are technical constraints on Masked Variables: the value must be at least 8 characters long and must not contain newlines. SSH private keys therefore cannot be created directly as Masked Variables, since they contain newlines. The workaround is to base64-encode the key, store it as a Masked Variable and decode it inside the job. In practice this is the most common pattern for handling SSH keys in GitLab pipelines.

4. Environment Scopes: A Variable per Environment

Environment Scopes are the most powerful and most frequently misunderstood feature of GitLab CI/CD variables. A scope determines which environments a variable gets injected into. The wildcard scope * means the variable is available in all environments. A specific scope such as production means the variable is only injected when the job runs in an environment named production.

For Magento deployments this means DEPLOY_HOST can be created twice: once with the value staging.example.com and scope staging, and once with the value production.example.com and scope production. The deploy job automatically receives the correct value depending on which environment it runs in. This eliminates conditional logic in the pipeline configuration and guarantees that staging jobs can never end up talking to production hosts.

# Environment-scoped variables allow the same job definition for both environments
# Variables configured in GitLab UI with scopes:
# DEPLOY_HOST   -> staging.example.com  (scope: staging)
# DEPLOY_HOST   -> production.example.com (scope: production)
# DEPLOY_USER   -> deploy (scope: *)
# REDIS_HOST    -> 10.0.1.10 (scope: staging)
# REDIS_HOST    -> 10.0.2.10 (scope: production)

# Single job definition, correct DEPLOY_HOST injected automatically per environment
.deploy-template: &deploy-template
  stage: deploy
  before_script:
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - echo "Deploying to ${DEPLOY_HOST} as ${DEPLOY_USER}"
    - rsync -az ./ "${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PATH}/releases/${RELEASE_ID}/"

deploy:staging:
  <<: *deploy-template
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

deploy:production:
  <<: *deploy-template
  environment:
    name: production
    url: https://shop.example.com
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+/'
      when: manual

5. File-Type Variables: SSH Keys and .env Files

File-Type Variables in GitLab CI/CD do not store their content as an environment variable, they store it as a temporary file whose path gets injected as the variable. This is the correct method for SSH private keys, which contain newlines and therefore cannot be used as normal variables. When an SSH key is created as a File-Type Variable, GitLab writes the key to a temporary file and sets the variable to that file's path, for example /tmp/gitlab-ci-key-XXXX.

The advantage over the base64 workaround is that there is no encoding or decoding in the job script, and no risk of the decoded content ending up in the log. Usage in the job is more direct: ssh-add "$SSH_PRIVATE_KEY", where $SSH_PRIVATE_KEY holds the path to the temporary file. That temporary file is automatically deleted after the job finishes. File-Type Variables are also well suited for composer auth.json and other configuration files with structured content.

6. Group vs. Project Variables: Controlling Visibility

GitLab lets you manage variables on three levels: instance, group and project. Group variables are inherited by every project inside the group, and project variables override group variables with the same name. For Magento agencies running multiple projects, the group level is the right place for shared secrets such as composer auth for private packages or general build-tool credentials.

Project-specific secrets such as production SSH keys, database passwords and API keys belong exclusively at the project level. That prevents one project from accidentally accessing another project's credentials, even when both sit in the same group. The hierarchy is: project variable overrides group variable, group variable overrides instance variable. When names collide, the more specific level always wins.

7. Magento Deployment Variables in Practice

For a typical Magento deployment with staging and production, the following variable scheme emerges: non-sensitive values such as paths and locale settings are defined directly in .gitlab-ci.yml as variables:, so they stay transparent and version-controlled. Semi-sensitive values such as hostnames and the deploy user are created in GitLab as non-protected, non-masked, with an environment scope. Highly sensitive values such as SSH keys, database passwords and API keys are created as Protected, Masked and environment-scoped.

This three-way split has a practical benefit: the pipeline configuration in .gitlab-ci.yml stays readable and traceable, because everything non-sensitive lives there. The GitLab UI only holds the actual secrets. New developers can understand the pipeline logic without needing access to GitLab settings, and changes to non-sensitive values can be tracked through merge requests.

8. Common Configuration Mistakes

The most common mistake is creating production credentials without the Protected flag. That makes them available in feature-branch pipelines, where they could in theory be read out through a merge request containing an echo $VARIABLE line in a script. The second most common mistake is skipping environment scopes because it initially seems simpler to create a variable globally. That leads to staging jobs operating against production hosts or credentials whenever a variable shares a name across environments but was only meant to differ in value.

A third, subtler mistake: SSH keys get created as a normal (non-File-Type) variable and then used in deployment scripts with echo "$SSH_KEY" | ssh-add -. This works in many cases, but it has two problems. The key content may end up in the job log if masking fails because the value is too long or gets split, and the key is expanded directly into the process environment, which can cause whitespace-handling issues under certain shell configurations. File-Type Variables avoid both problems.

9. Variable Types Compared

Choosing the right variable configuration depends on sensitivity and usage context. The table below shows the recommended configuration for typical Magento deployment variables.

Variable Protected Masked Scope Type
SSH_PRIVATE_KEY (prod) Yes n/a production File
COMPOSER_AUTH Yes Yes * Variable
DEPLOY_HOST No No staging / production Variable
DB_PASSWORD Yes Yes staging / production Variable
DEPLOY_PATH No No * In .gitlab-ci.yml

The table illustrates the principle: the more sensitive a variable, the more restrictive its configuration should be. Non-sensitive values belong directly in .gitlab-ci.yml, since they are transparent, version-controlled and do not need to be managed through the GitLab UI at all. Highly sensitive secrets should be Protected, Masked and environment-scoped, so they never show up in any log and stay inaccessible on feature branches.

10. Summary

Protected, Masked and Environment Scopes are three independent security dimensions that together determine the variable security model of a GitLab pipeline. Protected restricts access to protected branches, Masked prevents log output, and scope separates environments. Configure all three correctly and you ensure that staging credentials never land in production jobs, and that production secrets never surface in feature-branch logs.

The practical recommendation: define non-sensitive values directly in .gitlab-ci.yml as variables:. Give environment-specific hostnames and users a scope but no Protected or Masked flags. Make every secret Protected, Masked and environment-scoped. Store SSH keys as File-Type Variables. This system keeps the pipeline configuration understandable, the secrets safe and the environment separation reliable.

Configuring GitLab Variables Correctly: The Essentials at a Glance

Protected

Variable only available on protected branches and tags. Prevents access to production credentials from feature-branch pipelines.

Masked

Value gets replaced with [MASKED] in the job log. Not an absolute safeguard, but it prevents accidental plaintext exposure in pipeline logs.

Environment Scope

Variable is only injected into jobs running in the defined environment. Allows the same variable name to hold different values per environment.

File-Type

Content is stored as a temporary file. Required for SSH keys containing newlines. Avoids whitespace issues and accidental log output.

11. FAQ: Protected Variables, Masked Variables and Environment Scopes

1What is the difference between Protected and Masked?
Protected controls availability (protected branches/tags only). Masked controls log display (the value is replaced with [MASKED]). Both are configured independently of each other.
2Can I create an SSH key as a Masked Variable?
No, SSH keys contain newlines and Masked Variables do not allow newlines. Solution: create the SSH key as a File-Type Variable. GitLab stores it as a temporary file.
3What happens if I set Environment Scope to *?
The variable is injected into every job. Suitable for non-sensitive, environment-independent values. For sensitive credentials always set a specific scope.
4How do I prevent staging credentials from affecting production jobs?
Through environment scopes: staging variable with scope "staging", production variable with scope "production". GitLab automatically injects the correct version.
5Can group variables override project variables?
No. Project variables have higher priority than group variables. When names match, the more specific level always wins (project > group > instance).
6What are the minimum requirements for Masked Variables?
At least 8 characters, no newlines, only base64-compatible characters. JSON strings often cannot be created directly as a Masked Variable.
7How do I safely set up COMPOSER_AUTH in GitLab?
As a Protected plus Masked variable with scope *. JSON value: {"http-basic": {"repo.example.com": {"username": "user", "password": "token"}}}.
8Can I define variables in .gitlab-ci.yml instead of the GitLab UI?
Yes, for non-sensitive values that is recommended: transparent, version-controlled, traceable. Sensitive secrets always belong in the GitLab UI, never in version control.
9What does "Expand variable reference" mean in GitLab?
When enabled, other variable references inside the value get expanded. For secrets, always leave it disabled to avoid unexpected behavior.
10How do I check which variables are available in a job?
With export or env in the job script. Masked values show up as [MASKED], and Protected Variables simply do not appear at all on non-protected branches.