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.
Table of Contents
- 1. What secrets does a Magento deployment have?
- 2. What makes sense in GitLab CI/CD variables
- 3. Using protected, masked, and environment scope correctly
- 4. env.php: the sensitive file in the shared directory
- 5. Composer auth and private repositories
- 6. What belongs externally: HashiCorp Vault and alternatives
- 7. Complete variable structure for Magento deployments
- 8. Comparing secret storage locations
- 9. Common mistakes in secrets management
- 10. Summary
- 11. FAQ
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.