GitLab CI variables are a powerful way to inject configuration and secrets into your pipelines, but using them insecurely can expose sensitive information. Masked and protected variables are GitLab’s built-in mechanisms to help you manage this risk.

Let’s see how they work in practice. Imagine you have a deployment script that needs an API key. Instead of hardcoding it, you’d store it as a GitLab CI/CD variable.

deploy_job:
  stage: deploy
  script:
    - echo "Deploying with API key: $MY_API_KEY"
    - ./deploy_script.sh --api-key $MY_API_KEY

If MY_API_KEY is just a regular variable, its value will appear in your job logs. This is bad.

Here’s where masked and protected variables come in.

Masked Variables

When you define a variable in your GitLab project’s Settings > CI/CD > Variables, you’ll see a "Mask variable" checkbox. If you check this, GitLab will attempt to redact the variable’s value from job logs.

How it works: GitLab scans the output of your CI jobs and replaces any occurrence of the masked variable’s value with ********.

Important Caveats:

  • Length: Masked variables must be at least 8 characters long. Shorter values won’t be masked.
  • Format: They cannot contain whitespace.
  • Regex Matching: GitLab uses a simple string replacement. If your value is a substring of another string in the logs, it might still be partially visible or redacted in an unexpected way. For example, if your secret is abc123def and your log shows deploying with token abc123def, it will be masked. But if your log shows deployment_id: abc123def_staging, the abc123def part will be masked, leaving deployment_id: ********_staging.
  • Not Foolproof: A determined attacker with access to the CI job logs can still potentially infer or reconstruct the value if they have other context or see it in less obvious places (e.g., if the variable is used to construct a URL that is logged).

To set a masked variable:

  1. Navigate to your project’s Settings > CI/CD.
  2. Expand the Variables section.
  3. Click Add variable.
  4. Enter MY_API_KEY as the Key.
  5. Enter your actual API key (e.g., sk_live_abcdef1234567890) as the Value.
  6. Check the Mask variable box.
  7. Click Add variable.

Now, in your CI logs, you’ll see echo "Deploying with API key: ********" and ./deploy_script.sh --api-key ********.

Protected Variables

Protected variables add another layer of security, controlling where your sensitive variables can be used.

How it works: A protected variable is only available to CI jobs running on protected branches or protected tags.

  • Protected Branches: These are branches that require merge requests to be merged into them (e.g., main, develop).
  • Protected Tags: These are tags that have specific permissions set for who can create them (e.g., v1.0.0).

When you define a variable and check the Protected variable box, that variable will not be available to jobs running on non-protected branches or tags.

Why use this? Imagine you have a variable that deploys to your production environment. You absolutely do not want that variable (e.g., production database credentials) to be accessible by a job running on a feature branch. Protecting the variable ensures it’s only exposed when running on your designated secure deployment targets.

To set a protected variable:

  1. Follow steps 1-3 for setting a masked variable.
  2. Enter PRODUCTION_DB_PASSWORD as the Key.
  3. Enter your production database password as the Value.
  4. Check the Protected variable box.
  5. Click Add variable.

Now, if a job runs on a non-protected branch (like feature/new-ui), the $PRODUCTION_DB_PASSWORD variable will be empty or unset. If the same job runs on a protected branch (like main), the variable will be available.

Combining Masked and Protected

You can and often should use both. A variable that is both masked and protected will have its value hidden in logs and will only be available on protected branches/tags. This is ideal for production secrets.

Example Scenario: You have a variable PRODUCTION_DEPLOY_KEY.

  • Masked: Yes. You don’t want its value showing up in any job logs, even on protected branches.
  • Protected: Yes. You only want this key to be usable when deploying from your main branch or a tagged release.

When to use which, and when to use both:

  • Masked Only: For secrets that might be needed on any branch but whose values should never appear in logs. Example: A third-party API key for a non-critical service used in testing.
  • Protected Only: For configuration values that are safe to log but should only be applied in production/staging environments. Example: An environment name variable like DEPLOY_ENV=production.
  • Masked AND Protected: For highly sensitive credentials required for production deployments. Example: Production database passwords, cloud provider credentials for production environments.

Environment Scopes

GitLab CI/CD variables can also be scoped to specific environments. This adds another layer of control. When you add a variable, you can select an environment from the "Environment scope" dropdown.

How it works: A variable with an environment scope is only available to jobs that are deployed to that specific environment. This is extremely useful for managing different sets of credentials for development, staging, and production.

Example: You might have:

  • DATABASE_URL with scope development
  • DATABASE_URL with scope staging
  • DATABASE_URL with scope production

A job that deploys to the staging environment will automatically pick up the DATABASE_URL variable scoped to staging.

Combining Environment Scopes with Masked and Protected: You can combine all these features. For example, a PRODUCTION_DB_PASSWORD variable could be:

  • Masked: Yes
  • Protected: Yes
  • Environment Scope: production

This means the password will be hidden in logs, only available on protected branches/tags, and only when the job is explicitly configured to deploy to the production environment. This is the gold standard for managing production secrets.

The "Gotcha" Paragraph

Many people assume that if a variable is masked, it’s completely hidden everywhere. This isn’t true. GitLab’s masking works by redacting values in the job logs. If your script itself prints the variable’s value to stderr or if another tool invoked by your script logs the value directly, masking won’t prevent that. Furthermore, if the variable is used to construct a URL or another string that is logged, and that string contains the variable’s value, only the variable’s value part will be masked. Always test your CI/CD variable configurations by running jobs and carefully inspecting the logs to ensure sensitive data is not leaking.

The next level of complexity you’ll encounter is managing secrets across multiple projects using GitLab’s instance or group-level CI/CD variables.

Want structured learning?

Take the full Gitlab-ci course →