GitLab CI Secrets with Vault Instead of Just Protected Variables
AI generated
CI/CD
.yml
GitLab · CI/CD · Security
GitLab CI Secrets with Vault
instead of just protected variables: dynamic credentials over static secrets

GitLab protected and masked variables are the standard way to use secrets in a pipeline, but they remain static, long-lived values that can circulate unchanged for months in the worst case. HashiCorp Vault takes a different route through the native GitLab CI integration: credentials are issued only when the pipeline runs, are short-lived, and expire automatically. This article shows how the integration works technically, what it gains over plain protected variables, and when the extra operational effort actually pays off for a Magento project.

17 min read HashiCorp Vault · Secrets Management GitLab CI/CD · OIDC

1. The fundamental problem with static CI/CD variables

Every GitLab variable marked as protected or masked is, at its core, a single text value stored in the project or group configuration and loaded into the job environment on every matching pipeline run. Unless that value is manually rotated, it stays exactly the same for months or years, which makes it an attractive target: whoever captures it once, for instance through a compromised runner or overly permissive debug logging, holds a permanently valid credential.

For many use cases, such as a single API key for a non-critical third-party service, this risk is acceptable. But once database credentials, SSH deploy keys for production servers, or Composer auth tokens for commercial Magento extensions are involved, a permanently valid, static secret carries far more weight, because a single leak can potentially be exploited indefinitely until someone manually triggers a rotation.

2. How GitLab protected and masked variables work, and where their limits lie

Protected variables are only available to pipelines running on protected branches or tags, which prevents an arbitrary feature branch from accidentally getting access to production secrets. Masked variables ensure the value is replaced with asterisks in job logs, provided the value meets certain format rules, such as containing no line breaks and having a minimum length.

Both mechanisms, however, only protect the transport path and visibility, not the secret's own lifespan. A database username and password once stored in the project settings remains exactly that one user with exactly that one password, no matter how often the pipeline runs. The GitLab variable itself has no mechanism for expiry, automatic rotation, or logging which specific pipeline run actually used the secret and when.

3. What HashiCorp Vault fundamentally does differently

Vault does not manage secrets as static values but can issue them dynamically at request time through so-called secrets engines. With the database secrets engine, for example, Vault generates a fresh database user with a randomly generated password on every request, along with a set lease time, after which Vault automatically removes that user from the database again, with no manual intervention required.

This fundamentally changes the security model: even if an attacker captures an issued credential, it becomes automatically worthless after a short time. Vault additionally keeps a complete audit log of every issuance and every access, so it becomes possible after the fact to trace exactly which pipeline received which credential and when, which makes a substantial difference for compliance requirements such as PCI DSS or ISO 27001.

4. The native GitLab Vault integration via JWT/OIDC ID tokens

Since GitLab 15.7, JWT-based ID tokens can be generated directly in every job, cryptographically signed and carrying claims such as project, branch, and pipeline ID. Vault can be configured through its JWT/OIDC auth method to verify this token and, based on it, issue a short-lived Vault session for exactly that job, with no statically stored Vault token in GitLab beforehand.

Which Vault policy a job receives is determined through bound claims in Vault, for instance a restriction to a specific GitLab project or a specific protected branch. As a result, a job from a feature branch automatically ends up with fewer permissions than a job running on the protected main branch, without any separate, manually managed credential having to exist for that anywhere.


deploy_production:
  stage: deploy
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.mironsoft.internal
  variables:
    VAULT_ADDR: https://vault.mironsoft.internal
  secrets:
    DB_PASSWORD:
      vault:
        engine:
          name: database
          path: database
        path: creds/magento-readwrite
        field: password
      token: $VAULT_ID_TOKEN
  script:
    - bin/magento setup:upgrade --keep-generated
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

5. Example: dynamic database credentials for the deploy job

In the classic approach, a single MySQL user with full privileges is stored permanently as a protected variable and used by every deploy job. With the Vault database secrets engine, Vault instead generates a fresh user on every pipeline run, whose permissions are scoped exactly to what setup:upgrade needs, and whose lease time typically spans a few hours, enough for the deploy job's runtime, but no longer.

The job itself barely needs adjustment for this: instead of reading credentials from a predefined environment variable, the secrets: keyword in the job definition fetches the values directly from Vault and exposes them as an environment variable, so bin/magento setup:upgrade works unchanged, just with a credential that automatically stops working once the lease time expires.


# Configure the Vault database secrets engine for MySQL
vault secrets enable database

vault write database/config/magento-mysql \
    plugin_name=mysql-database-plugin \
    connection_url="{{username}}:{{password}}@tcp(db.internal:3306)/" \
    allowed_roles="magento-readwrite" \
    username="vault-admin" \
    password="initial-admin-password"

vault write database/roles/magento-readwrite \
    db_name=magento-mysql \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
        GRANT SELECT, INSERT, UPDATE, DELETE ON magento.* TO '{{name}}'@'%';" \
    default_ttl="2h" \
    max_ttl="4h"

6. Operations: policies, auth methods, and rotation in everyday use

In day-to-day Vault operations, policies should be defined as granularly as possible, ideally one dedicated policy per pipeline purpose, for instance one for database migrations and a separate one for access to Composer auth tokens, rather than a single, broad policy for all CI jobs. Besides the JWT/OIDC method, Vault also supports AppRole auth, which can make sense for runner environments where no native GitLab JWT is available, such as self-hosted runners in isolated networks.

For static secrets that cannot be issued dynamically for technical reasons, such as API keys for external SaaS services, Vault's KV secrets engine still offers a central, versioned, and audited storage location, which at least substantially improves traceability, even though the value's own lifespan still has to be rotated manually.

7. Effort versus benefit: when Vault pays off for a Magento project

Operating a dedicated Vault cluster, including an unsealing strategy, high availability, and a backup concept, is itself a non-trivial infrastructure project that ties up staff time and ongoing operational effort. For a single small Magento project with few developers and a manageable number of secrets, this effort frequently is not in good proportion to the security gain over carefully managed protected variables with regular manual rotation.

Once multiple teams, multiple Magento instances, or strict compliance requirements such as PCI DSS come into play, however, the calculation shifts noticeably: a central Vault cluster operated jointly for all projects pays for itself quickly, because the operational cost is spread across many users while the security gain from short-lived credentials and a complete audit log stays the same per project.

8. Migration strategy: moving from protected variables to Vault step by step

A sensible starting point is not converting all secrets at once, but beginning with the single most sensitive value, in a Magento project frequently the database credentials for the production deploy job. That one job gets switched over to the Vault integration while every other job continues unchanged with protected variables, which allows experience with the integration to build up without risking the entire deployment at once.

Only once the JWT/OIDC integration has proven itself in production do further secrets follow gradually, such as Composer auth tokens or SSH deploy keys. The table below summarizes the properties of protected variables and Vault respectively, and where the line for a sensible migration typically runs.

9. Practical recommendation: combining Vault and protected variables deliberately instead of playing them off against each other

In practice the decision is rarely a pure either-or. For a single small Magento project without a dedicated operations team, carefully maintained protected and masked variables with a fixed, documented rotation schedule, for instance quarterly, usually remain the more pragmatic choice, because the operational effort of a dedicated Vault cluster is hard to justify. Non-critical values such as an API key for an external image service do not necessarily need to move to Vault just because the tool happens to be technically available.

Once a Vault cluster is already running for other projects within the company, or once several Magento instances exist with separately isolated production and staging databases, it pays off to consistently establish Vault as the single source of truth for all sensitive credentials, rather than switching between both approaches case by case. That consistency spares the team the recurring discussion of which secret deserves which protection mechanism, and makes audits toward customers or certification bodies considerably easier.

Property Protected/Masked Variables HashiCorp Vault Recommendation
Secret lifespan Static, until manually rotated Dynamic, automatic expiry Vault for sensitive credentials
Audit log per access Not available Complete, per issuance Vault under compliance duty
Operational effort Minimal Dedicated cluster required Protected variables for small projects
Access scoping Protected branch/tag Fine-grained policies per job Vault for multiple teams
Setup complexity A few minutes Several days to weeks Gradual migration

Mironsoft

CI/CD pipelines, zero-downtime deployments and release automation

Deployments that run without downtime and without the nail-biting?

We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.

Pipeline Review

Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.

Zero-Downtime Deployment

Building symlink releases, health checks and rollback strategies for Magento stores.

CI/CD Automation

Connecting tests, security scans and deployments into one reliable pipeline.

10. Summary

Vault vs. Protected Variables: The Essentials at a Glance

Core problem

Protected and masked variables are static and long-lived, a single leak can potentially be exploited indefinitely.

What Vault delivers

Dynamically issued, short-lived credentials with automatic expiry and a complete audit log via the secrets engine.

Technical path

GitLab JWT/OIDC ID tokens authenticate jobs against Vault, with no statically stored Vault token needed beforehand.

When it pays off

With multiple teams, multiple Magento instances, or strict compliance requirements, not necessarily for a single small project.

11. FAQ: Vault vs. Protected Variables: The Essentials at a Glance

1What is the fundamental difference between protected variables and Vault secrets?
Protected variables are static values that stay unchanged until manually rotated. Vault can issue credentials dynamically at request time, with a set lease time after which they automatically become invalid.
2Do I need a static Vault token stored in GitLab for the integration?
No. Through the JWT/OIDC auth method, the job authenticates directly against Vault using an ID token generated by GitLab for that specific pipeline run, with no Vault token stored in GitLab beforehand.
3From which GitLab version do JWT/OIDC ID tokens work?
The id_tokens syntax for JWT-based ID tokens has been available since GitLab 15.7. For older versions, the earlier, now deprecated CI_JOB_JWT variable has to be used instead.
4What happens if a dynamically generated database user is still actively used after the lease expires?
Vault automatically removes the user from the database once the lease time expires, which can cause active connections to fail with an authentication error. The lease time should therefore be chosen generously enough for the actual job runtime.
5Does Vault pay off for a single, small Magento project?
In most cases, no. The operational effort for a dedicated Vault cluster is rarely in good proportion to the security gain, unless multiple projects or strict compliance requirements justify the effort.
6Can Vault also manage static secrets, not just dynamic ones?
Yes, through the KV secrets engine, classic static values can also be stored centrally, versioned, and audited. The value's own lifespan, however, still has to be rotated manually.
7How granularly can Vault policies be restricted for GitLab jobs?
Through bound claims in the JWT/OIDC auth method, access can be restricted to a specific GitLab project, a specific protected branch, or even a specific job name, which is considerably more fine-grained than a single protected variable.
8What is the difference between AppRole and JWT/OIDC auth in Vault?
AppRole uses a separately distributed role ID and secret ID pair for authentication and suits environments without a native GitLab JWT. JWT/OIDC instead uses the ID token signed directly by GitLab and needs no additionally distributed secret.
9Does a Vault integration automatically satisfy PCI DSS requirements?
No, Vault is a tool that eases the technical implementation, but it does not replace the organizational measures and documentation needed for an actual PCI DSS certification.
10What is the most sensible way to start migrating from protected variables to Vault?
Best to start with the single most sensitive secret, frequently the database credentials for the production deploy, convert that one job, gather experience, and only then gradually migrate further secrets.