Keeping Sensitive Data Out of Git in Magento Projects
AI generated
git
HEAD
Git · Security · Magento 2 · Secrets Management
Keeping Sensitive Data Out of Git in Magento Projects
Managing env.php, auth.json, and secrets safely

A single accidental commit containing database credentials or the crypt key from app/etc/env.php can compromise an entire Magento project, even after the repository is later set back to private. This article explains which files must never end up in Git, what a clean gitignore baseline looks like, and how a secrets manager and pre commit scanning reliably prevent it.

14 min. read env.php · auth.json · .gitignore Secrets manager · pre-commit hooks · gitleaks

1. Why sensitive data is especially at risk in Magento projects

A Magento project isn't just application code, it also includes generated configuration files that are created automatically during installation. The file app/etc/env.php is produced by every setup:install run and contains database credentials, the crypt key, and cache and session backend configuration. Because this file sits in the regular app/etc directory, right next to versioned configuration files like config.php, it gets accidentally committed alarmingly often in practice, especially with a quick git add -A and no prior review.

Magento repositories are also frequently shared with multiple parties: agencies, freelance developers, CI runners, and sometimes even public GitHub forks used for showcases. Automated scanners actively search public repositories for known patterns such as Magento crypt keys or Composer auth.json tokens, often within minutes of a push. Once a secret enters the Git history, it stays reachable via git log, git blame, and every clone already pulled, even after it's deleted in a later commit. That's exactly why prevention beats cleanup after the fact here.

2. What must never be committed to a Magento repository

Five categories of sensitive data regularly slip into commits on Magento projects. First, app/etc/env.php with database credentials and the crypt key. Second, auth.json with the Composer credentials for repo.magento.com and the Magento Marketplace. Third, .env files that hold real credentials for databases, Redis, or Elasticsearch in Docker setups or deployment scripts. Fourth, private keys and certificates, such as SSH deploy keys or SSL certificates for local HTTPS development. Fifth, hardcoded API tokens in di.xml, view models, or cron jobs, for example for payment providers, shipping services, or external APIs.

The crypt key deserves particular attention: Magento uses it to encrypt sensitive values in core_config_data, such as stored API credentials for payment modules. Anyone who obtains both the crypt key and a database dump, whether from a leaked repository or a backup, can fully decrypt those values. A leaked crypt key is therefore not a theoretical risk, it opens a direct path to payment integrations and other third-party access in the store.

3. app/etc/env.php in detail: database credentials and the crypt key

env.php is generated automatically by bin/magento setup:install and contains, among other things, the db block with host, username, and password in plain text, the crypt block with the crypt key, and configuration for session and cache backends like Redis. Since this file is environment-specific and differs between development, staging, and production, it fundamentally does not belong in the repository, regardless of whether that repository is private or public.

The established pattern: instead of versioning env.php, check in an env.php.dist or env.php.example with placeholders instead of real values. Every environment generates its own env.php locally or during the deployment pipeline, either by rerunning setup:install or via a script that replaces placeholders with values from environment variables. On Magento Cloud, the platform handles this step automatically through the MAGENTO_CLOUD_RELATIONSHIPS environment variable, so developers never need to touch a real env.php at all.

4. auth.json: credentials for Magento Marketplace and repo.magento.com

Composer needs to authenticate against repo.magento.com for Magento projects in order to download commercial extensions and Magento core via the http-basic mechanism. These credentials, the public key and private key from the Magento Marketplace account, land by default in a file called auth.json in the project root, right next to composer.json. That's exactly where developers regularly commit them by accident, since it looks like an ordinary configuration file.

A leaked auth.json key lets attackers install paid extensions in the account's name or exhaust download quotas. The safe path: store auth.json globally outside the project under COMPOSER_HOME (usually ~/.composer/auth.json), instead of per project. In CI/CD pipelines, the file is instead injected at runtime from a secret store, for example via composer config --global http-basic.repo.magento.com with values from masked pipeline variables, so it never exists in the repository's filesystem at all.

5. A practical .gitignore baseline for Magento 2

A correct .gitignore for Magento 2 needs to cover far more than just env.php. Generated and temporary directories such as var/, generated/, pub/static/, and most of pub/media/ (except for versioned placeholder images) should be excluded just like vendor/ and node_modules/, since both are reproducible via Composer and npm respectively and would otherwise bloat the repository unnecessarily. IDE directories such as .idea/ and .vscode/ are project-specific and also have no place in a shared repository.

The snippet below shows a baseline that can be dropped directly into most Magento 2 projects and extended with project-specific directories, such as custom log paths, as needed. It's important to commit .gitignore rules already at project setup, before the first setup:install run even generates env.php and generated/, otherwise those files may already end up in the first commit.


# .gitignore: Magento 2 baseline - never commit generated files or secrets

# Sensitive configuration - contains DB credentials and the crypt key
/app/etc/env.php
/app/etc/config.local.php

# Composer authentication for repo.magento.com / Magento Marketplace
/auth.json

# Environment files with real secrets (keep only .env.example versioned)
.env
.env.*
!.env.example

# Generated and cached application code
/generated/
/var/
/pub/static/
/pub/media/*
!/pub/media/.htaccess

# Dependencies (reproducible via composer/npm, do not version)
/vendor/
/node_modules/

# Private keys and certificates
*.pem
*.key
*.crt
id_rsa*

# IDE and editor directories
/.idea/
/.vscode/

# OS artifacts
.DS_Store
Thumbs.db

6. Environment variables instead of hardcoded configuration

Instead of writing credentials directly into env.php or module configuration, read them at runtime via getenv() or $_ENV from environment variables that are set differently per environment, without the code itself ever changing. This pattern consistently separates configuration from code, a principle the twelve-factor app methodology also defines as a ground rule for portable applications. On Magento Cloud, the automatically provided MAGENTO_CLOUD_* variables fill exactly this role for database and cache access.

In self-hosted environments, the same approach can be replicated with a small wrapper script that generates env.php from a template during deployment, replacing placeholders with values from the process environment. Important here: the environment variables themselves must not appear in plain text in deployment scripts or Dockerfiles, they must come from a protected source, such as a CI/CD system's secrets or a secrets manager, otherwise you've merely moved the problem from one file to the next.


<?php
/**
 * bin/generate-env.php - build app/etc/env.php from environment variables.
 * Never commit the real env.php; this script runs during deployment only.
 */
declare(strict_types=1);

$required = ['DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASSWORD', 'CRYPT_KEY'];
foreach ($required as $var) {
    if (getenv($var) === false) {
        fwrite(STDERR, "Missing required environment variable: {$var}\n");
        exit(1);
    }
}

$config = [
    'db' => [
        'connection' => [
            'default' => [
                'host' => getenv('DB_HOST'),
                'dbname' => getenv('DB_NAME'),
                'username' => getenv('DB_USER'),
                'password' => getenv('DB_PASSWORD'),
            ],
        ],
    ],
    'crypt' => [
        'key' => getenv('CRYPT_KEY'),
    ],
    'cache' => [
        'frontend' => [
            'default' => [
                'backend' => 'Cm_Cache_Backend_Redis',
                'backend_options' => [
                    'server' => getenv('REDIS_HOST') ?: '127.0.0.1',
                    'port' => getenv('REDIS_PORT') ?: '6379',
                ],
            ],
        ],
    ],
];

file_put_contents(
    __DIR__ . '/../app/etc/env.php',
    "<?php\nreturn " . var_export($config, true) . ";\n"
);

7. Secrets managers: Vault, AWS Secrets Manager, and CI/CD secret stores

Plain environment variables solve the commit problem, but not the questions of rotation, access control, and an audit trail. A dedicated secrets manager such as HashiCorp Vault or AWS Secrets Manager closes that gap: credentials are managed centrally, issued with a limited lifetime, and can be rotated without touching application code or deployment scripts. AWS Secrets Manager, for example, can automatically and periodically rotate database passwords for RDS, while Vault issues dynamic, short-lived database credentials per application instance.

For most Magento teams, starting with the secret stores of their own CI/CD platform is enough, for example GitHub Actions secrets or masked, protected GitLab CI variables. These get injected as environment variables at build or deploy time and appear masked in logs by default. Moving to Vault or AWS Secrets Manager usually only pays off once multiple environments, teams, or microservices need the same credentials with different permission levels.


# .gitlab-ci.yml excerpt: inject secrets at deploy time, never store them in the repo
deploy_production:
  stage: deploy
  variables:
    DB_HOST: $PROD_DB_HOST
    DB_NAME: $PROD_DB_NAME
    DB_USER: $PROD_DB_USER
    DB_PASSWORD: $PROD_DB_PASSWORD      # masked + protected CI/CD variable
    CRYPT_KEY: $PROD_CRYPT_KEY          # masked + protected CI/CD variable
  script:
    - php bin/generate-env.php
    - php bin/magento setup:upgrade --keep-generated
    - php bin/magento cache:flush
  environment:
    name: production
  only:
    - main
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

8. Pre-commit hooks and scanning: git-secrets, gitleaks, and friends

Prevention through discipline alone isn't enough in practice, because a single unfocused moment is all it takes for a secret to enter the history. Tools such as gitleaks and git-secrets automatically scan diffs for known patterns, such as AWS access keys, private RSA key headers, Magento crypt key formats, or generic high-entropy strings that look like tokens. Wired up as a pre-commit hook, they block the commit locally before sensitive data even leaves the developer's machine.

Magento teams should also add a scan step in the CI pipeline, independent of the local hook, since not every developer reliably installs hooks or some bypass them with --no-verify. A gitleaks step in the pipeline that fails the build on a hit catches exactly these cases. Rule-based configuration files can additionally be extended with project-specific patterns, such as the characteristic shape of an auth.json or your own internal API key formats.


#!/usr/bin/env bash
# .git/hooks/pre-commit - block commits containing leaked secrets
set -euo pipefail

echo "Running gitleaks on staged changes..."

if ! command -v gitleaks &> /dev/null; then
  echo "[WARN] gitleaks not installed, skipping local scan (CI will still catch it)"
  exit 0
fi

# Scan only the staged diff, not the entire history
if ! gitleaks protect --staged --redact --verbose; then
  echo "[BLOCKED] Potential secret detected in staged changes."
  echo "Remove the secret, or add a documented exception to .gitleaks.toml."
  exit 1
fi

# Extra guard: never allow auth.json or env.php to be staged at all
if git diff --cached --name-only | grep -qE '(^|/)auth\.json$|app/etc/env\.php$'; then
  echo "[BLOCKED] auth.json or env.php must never be committed."
  exit 1
fi

exit 0

9. Best practices compared and what to do about secrets already pushed

The table below sets out the most common sensitive data types in Magento projects against the wrong way of handling them and the correct approach.

File / data type Never do this Correct approach Why it matters
app/etc/env.php Commit plain text into the repository Add to .gitignore, version only env.php.dist with placeholders DB access and crypt key compromised instantly
auth.json (Composer) Commit it into the project root Store globally in COMPOSER_HOME or inject via CI secret Marketplace access and cost risk if abused
.env / Docker Compose secrets Check in with real values Version .env.example with placeholders, ignore the real .env Database and service credentials stay local
Private keys / certificates Place them in the repository Manage via secrets manager or deployment pipeline Prevents SSH and SSL compromise
API tokens in di.xml/PHP Hardcoded in source code Load via environment variables or a secrets manager Enables rotation without code changes

Despite all precautions, it happens: a secret gets pushed by accident. In that case, speed matters more than thoroughness. The single most important step is immediately rotating the affected credentials: change the database password, regenerate the crypt key and re-encrypt affected encrypted values, revoke API keys with the respective provider, before even thinking about the Git history. A simple git rm --cached removes the file from future commits, but the secret remains fully readable and recoverable in older commits.


# Immediate response when a secret was committed but not yet fully contained
# (rotating the credential itself is still the first and most important step)

# 1. Remove the file from the index, keep it locally, and ignore it going forward
git rm --cached app/etc/env.php
echo "app/etc/env.php" >> .gitignore

# 2. Commit the removal
git commit -m "Remove app/etc/env.php from version control"

# 3. If the commit was already pushed, notify the team immediately -
#    the file is still readable in the Git history for anyone with a clone
git push origin main

# Note: this does NOT remove the secret from history.
# Rewriting history (git filter-repo / BFG Repo-Cleaner) is a separate,
# more involved process covered in a dedicated article.

Fully removing a secret from the entire Git history, for example with git filter-repo or BFG Repo-Cleaner, is a separate, significantly deeper topic with its own pitfalls around already-cloned copies, open pull requests, and force pushes on shared branches. This article deliberately covers only prevention; history cleanup is described in detail elsewhere on the blog.

Mironsoft

Security reviews, CI/CD pipelines, and secrets management for Magento teams

Ready to reliably keep sensitive data out of your repository?

We audit existing Magento repositories for secrets already committed, set up a clean .gitignore baseline, and implement pre-commit scanning and secrets management for your team.

Repository audit

Check existing commits and history for leaked credentials and keys

Secrets management setup

Set up Vault, AWS Secrets Manager, or CI/CD secret stores for your stack

Pre-commit scanning

Establish gitleaks and git-secrets as a binding standard across the team

10. Summary

Keeping sensitive data out of Git in Magento projects isn't a one-time cleanup, it's a consistent pattern applied across the entire project lifecycle. app/etc/env.php with database credentials and the crypt key, along with auth.json holding the Marketplace credentials, belong in .gitignore from day one, not added after the fact. A complete, Magento-specific .gitignore baseline covering var/, generated/, pub/static/, pub/media/, vendor/, node_modules/, plus .idea/ and .vscode/, prevents the most common mistakes structurally from the start.

Environment variables consistently separate configuration from code, while secrets managers such as Vault or AWS Secrets Manager additionally provide rotation, access control, and an audit trail where plain environment variables reach their limits. Pre-commit hooks with gitleaks or git-secrets, plus an extra scan step in the CI pipeline, catch human error before it turns into a security incident. And if a secret does slip through: immediately rotating the credentials always takes priority over the more involved job of cleaning up the Git history.

Sensitive Data in Magento Projects: The Essentials at a Glance

env.php & auth.json

Hold database credentials, the crypt key, and Marketplace access. Never version them, commit only .dist templates with placeholders.

.gitignore baseline

Consistently exclude var/, generated/, pub/static/, pub/media/, vendor/, node_modules/, .idea/, .vscode/, and env.php.

Environment variables & secrets manager

Separate configuration from code, manage credentials via getenv(), CI/CD secrets, or Vault/AWS Secrets Manager.

Scanning & response

Run gitleaks/git-secrets as a pre-commit hook and CI step. Rotate immediately on a leak, handle history cleanup separately.

11. FAQ: Keeping Sensitive Data Out of Git in Magento Projects

1Which Magento files must never end up in Git?
Above all app/etc/env.php with DB credentials and crypt key, auth.json with Composer credentials, real .env files, private keys/certificates, and hardcoded API tokens in code.
2What happens if the crypt key from env.php gets leaked?
The crypt key decrypts sensitive values in core_config_data. Combined with a DB dump, those values can be fully decrypted, so regenerate it immediately.
3Where should auth.json for Composer be stored?
Globally under COMPOSER_HOME (~/.composer/auth.json), not in the project root. In CI/CD, generate it at runtime from masked variables.
4What belongs in a Magento-specific .gitignore?
env.php, auth.json, var/, generated/, pub/static/, most of pub/media/, vendor/, node_modules/, .idea/, and .vscode/, committed before the first setup run.
5How do I replace hardcoded credentials with environment variables?
Read them at runtime with getenv()/$_ENV. A deployment script generates env.php from a template, replacing placeholders with real values.
6When does a dedicated secrets manager like Vault pay off?
When you need rotation, fine-grained access control, or an audit trail across multiple environments/teams. Smaller projects manage fine with CI/CD secret stores.
7What does gitleaks do and how does it differ from git-secrets?
Both scan for known secret patterns. gitleaks uses configurable rules, good for CI. git-secrets leans on AWS patterns, often used as a local hook.
8How do I set up a pre-commit hook for secret scanning?
Install gitleaks, wire it into .git/hooks/pre-commit, run gitleaks protect --staged. Also run the same scan as a required CI pipeline step.
9I accidentally committed env.php, what do I do?
Rotate credentials immediately (DB password, crypt key), only then remove the file from version control with git rm --cached and add it to .gitignore.
10Is it enough to just delete a file from the repository afterward?
No, git rm --cached only affects future commits, older commits still contain the secret. Full removal from history needs git filter-repo or BFG Repo-Cleaner.