Secrets Management for Magento: What Stays in GitLab and What Should Live Externally
AI generated
CI/CD
.yml
GitLab · Secrets · Magento · Security
Secrets Management for Magento:
What stays in GitLab, and what does not

Every Magento deployment handles sensitive data: SSH keys, database passwords, Composer auth tokens, Redis connections, and API keys. The question of where these secrets are stored determines how exposed a deployment process is in the event of an incident. GitLab CI/CD variables are a practical starting point, but they are not the best choice for every secret.

16 min read env.php · SSH keys · Composer auth · Vault · Protected variables GitLab 16+ · Magento 2.4 · PHP 8.4

1. What secrets does a Magento deployment have?

A typical Magento deployment handles a surprisingly large number of sensitive values across several categories. The first category holds connection data: database host, database name, username and password, Redis connection string, RabbitMQ credentials, and OpenSearch configuration. This data flows into env.php and must be present on the production server, but it never needs to pass through the CI/CD pipeline once the file already lives on the server in the shared directory.

The second category holds deployment credentials: the SSH private key for accessing the production server, SSH known hosts for fingerprint verification, the deploy user, and the deploy host. These are only needed during the deploy job and should therefore be stored as GitLab variables protected on the production branch. The third category holds build credentials: the Composer auth token for private repositories (Adobe Commerce, Hyva), NPM tokens for private packages, and Magento Marketplace credentials. These are only needed in the build job and should also live in GitLab variables, though not necessarily with production scoping.

The most important insight here: not every secret belongs to the same category, and therefore not in the same place. Database passwords that never need to flow through the pipeline should not be stored in GitLab either. That minimizes the attack surface: a compromised GitLab account grants no access to the production database if the database password was never stored there in the first place.

2. What makes sense in GitLab CI/CD variables

GitLab CI/CD variables are suited to secrets that actively need to travel through the pipeline, meaning they are required at runtime as an environment variable inside a job. The SSH private key is a classic example: the deploy job needs it to establish an SSH connection to the production server. Without the key in the job, there is no connection. The same applies to Composer auth tokens: the build job runs composer install and needs the auth token for private repositories to do so.

What does not need to flow through the pipeline should not end up in GitLab variables either. The database password is the most common mistake in this category: teams store it in GitLab because it seems convenient, even though it is only ever needed on the server inside env.php, and the deploy job never uses it directly. If env.php already lives in the server's shared directory and the deploy job only creates a symlink, GitLab never needs to see the database password at all. The fewer secrets stored in GitLab, the smaller the damage if a GitLab account is ever compromised.

3. Using protected, masked, and environment scope correctly

GitLab offers three independent protection mechanisms for CI/CD variables that should be combined consistently. The protected flag restricts a variable's visibility to jobs running on protected branches or protected tags. A deploy SSH key with the protected flag is not visible in a feature branch job, even if the branch uses the same job name. This prevents developers from gaining access to production secrets by manipulating .gitlab-ci.yml in a feature branch.

The masked flag prevents a variable's value from appearing in job logs. It is not a complete protection: the value still exists as an environment variable within the job context and can be printed by code that GitLab does not mask. But it does prevent accidental leaking through set -x in shell scripts or debug output. The environment scope restricts a variable to a specific environment name (production, staging). Staging database credentials get the staging scope, production SSH keys get the production scope. That way it is structurally impossible for a staging job to see production credentials, or vice versa.

4. env.php: the sensitive file in the shared directory

In Magento, env.php is the central configuration file that holds the database connection, cache backend, session storage, crypt key, and other critical settings. It lives in the shared directory of the release model (/var/www/magento/shared/app/etc/env.php) and gets linked into every new release directory via symlink on each deployment. That is the correct approach: the file lives on the server and is never transported through the pipeline.

A common anti-pattern is generating env.php from GitLab variables at deploy job runtime. That means every credential it contains, the database password, the Redis password, and the crypt key, has to be stored in GitLab. In a server backup, env.php can be secured without exposing passwords because the crypt key is managed separately; in the GitLab-generated variant, all credentials sit in GitLab permanently. The better approach: create env.php manually on the server once, place it in the shared directory, and then never touch it through the pipeline again. Updates to env.php happen directly on the server, not through CI/CD.

5. Composer auth and private repositories

Magento projects using Adobe Commerce or the Hyva theme need Composer auth tokens to access private repositories. The COMPOSER_AUTH format in GitLab allows passing credentials as a JSON string, which Composer interprets as auth.json. This variable belongs in GitLab CI/CD variables with the masked flag, since it is actively needed in the build job and the credentials then exist temporarily inside the build container.

# .gitlab-ci.yml - Build stage with Composer auth injection
build:magento:
  stage: build
  image: php:8.4-cli
  variables:
    # COMPOSER_AUTH is a GitLab CI/CD variable (masked, protected for build scope)
    # Format: {"http-basic":{"repo.magento.com":{"username":"...","password":"..."}}}
    COMPOSER_HOME: "/tmp/composer"
  script:
    # Inject auth.json from COMPOSER_AUTH variable - never commit auth.json to Git
    - mkdir -p "$COMPOSER_HOME"
    - echo "$COMPOSER_AUTH" > "$COMPOSER_HOME/auth.json"
    - composer install --no-dev --prefer-dist --no-interaction --no-progress
    # Remove auth.json from artifact - credentials must not persist in artifact
    - rm -f "$COMPOSER_HOME/auth.json"
  artifacts:
    paths:
      - vendor/
      - generated/
    expire_in: 2 hours
  cache:
    key: "composer-${CI_COMMIT_REF_SLUG}"
    paths:
      - .cache/composer/

Important: auth.json must never be stored as a build artifact. Once composer install completes, it must be deleted before the artifact is created. An artifact containing auth.json could potentially expose the credentials to anyone who can download it, and in GitLab that can mean every member of the project. The rm -f step before the end of the script block ensures the file is not left in the artifact path.

6. What belongs externally: HashiCorp Vault and alternatives

For teams with stricter compliance requirements, or for credentials with very high damage potential if compromised, for example code signing private keys or master database credentials, external secret management systems such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault offer stronger isolation than GitLab CI/CD variables.

GitLab natively supports integration with HashiCorp Vault via JWT-based authentication: the pipeline authenticates with Vault using a short-lived JWT token that GitLab issues for each job. Vault verifies the job's identity based on claims such as project_id, ref, and environment_name, and only releases the secrets the job is configured for. The secret then only exists for the duration of the job as an environment variable, is never permanently stored in GitLab, and is no longer accessible once the job ends.

7. Complete variable structure for Magento deployments

The overview below shows which variables should be configured in which scope, and with which flags, for a typical Magento deployment with staging and production. It follows the principle of minimal access: every variable is visible only where it is genuinely needed.

# GitLab CI/CD Variables Configuration - Magento Deployment
# Settings > CI/CD > Variables

# --- Deployment Credentials ---
# SSH_PRIVATE_KEY: Protected, Masked, Scope: production
#   Private key for SSH connection to production server
# SSH_KNOWN_HOSTS: Protected, Not Masked, Scope: production
#   Content of known_hosts for production server fingerprint verification
# DEPLOY_USER: Protected, Not Masked, Scope: production
#   Deploy user on production server (e.g., deploy)
# DEPLOY_HOST: Protected, Not Masked, Scope: production
#   Hostname or IP of production server
# DEPLOY_PATH: Protected, Not Masked, Scope: production
#   Base path on server (e.g., /var/www/magento)

# --- Staging Variants ---
# SSH_PRIVATE_KEY: Protected, Masked, Scope: staging
# SSH_KNOWN_HOSTS: Protected, Not Masked, Scope: staging
# DEPLOY_USER: Protected, Not Masked, Scope: staging
# DEPLOY_HOST: Protected, Not Masked, Scope: staging
# DEPLOY_PATH: Protected, Not Masked, Scope: staging

# --- Build Credentials ---
# COMPOSER_AUTH: Protected, Masked, Scope: *
#   JSON string with Composer credentials for private repos
# MAGENTO_PUBLIC_KEY: Not Protected, Masked, Scope: *
#   Magento Marketplace public key
# MAGENTO_PRIVATE_KEY: Not Protected, Masked, Scope: *
#   Magento Marketplace private key (= COMPOSER_AUTH password)

# --- Pipeline Configuration ---
# RELEASE_RETENTION: Not Protected, Not Masked, Scope: *
#   Number of old releases to keep (e.g., 5)
# GIT_STRATEGY: Not Protected, Not Masked, Scope: *
#   fetch (reuse workspace) or clone (fresh clone per job)

Three principles guide this structure. First, production SSH keys are stored only in the production scope, so no staging job can see them. Second, every credential is stored as masked to prevent accidental logging. Third, GitLab holds no database passwords or crypt keys, because those are managed in env.php on the server and never need to flow through the pipeline.

8. Comparing secret storage locations

Different secrets have different security requirements and different usage patterns in the pipeline. The right storage location follows from the combination of these factors.

Secret GitLab Variables Server File External Vault
SSH deploy key Yes (protected, masked) Not practical Optional for high compliance
Database password No, not needed Yes (env.php) For high compliance
Composer auth token Yes (masked) Alternatively on build server Optional
Magento crypt key No, never needed Yes (env.php) Yes (recommended)
Redis password No, not needed Yes (env.php) For high compliance

The table shows the most important principle: secrets that do not need to flow through the pipeline do not belong in GitLab. Database password, crypt key, and Redis password are used exclusively by Magento on the server, not by the CI/CD process. The env.php file in the shared directory is the right place for them. Only secrets the CI/CD process actively needs at runtime, such as the SSH key and Composer auth, belong in GitLab variables, with appropriate protection and masking.

9. Common mistakes in secrets management

The most common mistake is storing the entire env.php in a GitLab variable in order to write it to the server during deployment. The result: every database password, the crypt key, and every other configuration value sit permanently in GitLab, are retrievable through the API by admins, and can potentially appear in job logs if the variable is not masked correctly. The correct alternative: create env.php manually on the server once, then link it in only via symlink.

A second common mistake is missing environment scope on credentials. If SSH_PRIVATE_KEY is stored without a scope, it is visible in every job, including jobs on feature branches that have no need to connect to the production server. A third mistake is checking auth.json or .env files into the Git repository, even if only in a past commit. Git history holds these files permanently and can be searched with tools like git log -S 'password'. Secret scanning tools such as GitGuardian or GitLab's built-in secret detection can catch such cases automatically, but only after the file has already been committed.

10. Summary

Good secrets management for Magento starts with clear categorization: what needs to flow through the pipeline (SSH key, Composer auth), what lives on the server (env.php, crypt key), and what should sit in an external vault (highly critical credentials under compliance requirements). The fewer secrets stored in GitLab, the smaller the attack surface if an account is ever compromised.

GitLab variables needed in the pipeline must be configured with the protected flag (visible only on protected branches), the masked flag (no log output), and an environment scope (visible only in the right environment). env.php lives in the shared directory on the server and is never transported through the pipeline. Composer auth tokens are injected in the build job and deleted before the artifact is created. With these ground rules, the result is a deployment process whose secrets management can withstand a security review.

Secrets Management for Magento: The Essentials at a Glance

Minimal GitLab exposure

Only store secrets in GitLab that are actively needed in the pipeline. Database passwords and crypt keys do not belong there.

env.php on the server

Create it manually once, place it in the shared directory. The deploy job only sets a symlink, no secret flows through the pipeline.

Protected + masked + scope

All pipeline credentials with protected flag, masked, and environment scope. No staging job sees production SSH keys.

Composer auth cleanup

Delete auth.json after composer install, before the artifact is created. Never check it into Git, never store it as an artifact.

11. FAQ: Secrets Management for Magento in GitLab

1Database password in GitLab variables?
No. It is only needed on the server in env.php, never in the pipeline. The deploy job only sets a symlink, so no database password travels through the pipeline.
2Protected vs. masked variables?
Protected: only on protected branches. Masked: not shown in logs. Independent options; for production credentials combine both plus an environment scope.
3Composer auth not in the artifact?
Delete auth.json after composer install. Set COMPOSER_HOME to a temp directory outside the artifact path. Never store it as an artifact, never check it into Git.
4What is the environment scope?
Restricts a variable to a GitLab environment (production/staging). Structurally prevents staging jobs from seeing production credentials, and vice versa.
5When HashiCorp Vault instead of GitLab?
Under strict compliance requirements, for secrets with very high damage potential, or when secret rotation and audit logs are required.
6Finding secrets in Git history?
git log -S 'search term' or GitLab secret detection. If something is found, rotate the credential immediately, then remove it from history (git filter-repo).
7COMPOSER_AUTH as a file variable?
Yes, as a GitLab variable of type 'File'. GitLab writes the content to a temp file and passes the path. More elegant than the JSON string approach for auth.json use.
8Store env.php in GitLab?
No. It contains database passwords and the crypt key. It belongs in the shared directory on the server. Create it manually once, then link it in via symlink.
9Rotating a compromised secret?
Invalidate it immediately. Replace it with a new secret in GitLab. Check pipelines that ran between the compromise and the rotation for unauthorized access.
10SSH_KNOWN_HOSTS without masked?
Yes, that is fine. SSH_KNOWN_HOSTS only contains public fingerprints, not sensitive data. The protected flag and scope are sufficient.