
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
A leaked .env file in Git history can expose payment keys overnight. CI CD secrets management best practices exist because pipelines need database passwords, API tokens, and SSH keys to build and deploy—yet those values must never live in source control. On production Laravel stacks I maintain with GitLab CI and Deployer 7, secrets handling is as critical as the deploy script itself. This guide covers storage, injection, scanning, rotation, and the mistakes that still show up in real audits.
What are CI CD secrets management best practices?
Secrets management in CI/CD is the discipline of keeping credentials out of repositories while still letting automated jobs authenticate. The core rule is simple: Git stores code, not keys. Everything else follows from that boundary.
In practice, a mature setup separates three layers. First, a secret store holds the canonical value. Second, the CI platform maps store entries to masked, protected variables. Third, the deploy target receives only what that environment needs—production keys never touch a feature-branch job.
I have seen teams paste Stripe keys into .gitlab-ci.yml because a staging deploy failed at 11 p.m. That fix works once. It also creates a permanent audit problem. Treat pipeline YAML like application code: public by default, secrets injected from outside.
Good secrets hygiene also means naming conventions. Prefix variables by environment: STAGING_DB_PASSWORD vs PROD_DB_PASSWORD. Scope them to protected branches. Document which service owns each secret in a runbook—not in the repo.
For background on pipeline design, see the Laravel GitLab CI step-by-step guide and build pipeline automation best practices. Both assume secrets arrive from outside the YAML file.
The minimum checklist every team should enforce
- Block commits containing high-entropy strings or
.envfiles via pre-commit hooks. - Enable masked and protected flags on all CI variables that hold credentials.
- Run secret scanning on every push and pull request.
- Restrict production secrets to protected branches and tagged releases.
- Audit variable access quarterly and after any team member offboarding.
- Keep production
.envon the server filesystem, not in the artifact bundle.
Where should you store secrets in a CI/CD pipeline?
You have four realistic storage tiers. Pick based on team size, compliance needs, and whether you run Kubernetes or plain VPS deploys.
| Storage option | Best for | Trade-offs |
|---|---|---|
| CI-native variables (GitLab, GitHub) | Small teams, single-repo Laravel or WordPress projects | Limited rotation automation; vendor lock-in per platform |
| HashiCorp Vault / cloud secret managers | Multi-app, multi-environment, compliance audits | Extra infra to operate; needs auth wiring into CI |
Server-side .env (Deployer shared dir) | Traditional PHP-FPM VPS deploys | Secrets live on disk; backup and permission discipline required |
| Encrypted files (Ansible Vault, SOPS) | Infra-as-code repos with few runtime secrets | Key management for the encryption key itself |
On sister legal-tech sites I deploy with Deployer 7, production credentials sit in a shared .env outside the release symlink. GitLab CI holds only the SSH deploy key and maybe a Sentry DSN for build-time checks. The app reads database passwords from the server at runtime. That split keeps the pipeline powerful without making it a password warehouse.
For encrypted repo files, Ansible Vault for secrets covers a pattern that works well for server provisioning repos. For cloud-native stacks, AWS Secrets Manager and External Secrets Operator with Vault show how Kubernetes pulls credentials without storing them in manifest YAML.
Managed platforms like GitHub Actions and GitLab CI expose first-class secret stores documented by the vendors themselves. GitHub documents encrypted secrets at the repository and environment level. GitLab documents masked variables, protected branches, and group-level inheritance in its CI/CD variables reference.
Environment-scoped vs project-scoped secrets
Project-scoped variables are fine for a single staging app. Once you run staging and production from one pipeline file, move production credentials to environment-scoped entries. GitLab environments and GitHub Actions environments both support approval gates before production jobs run.
A common mistake is duplicating the same API key across five microservice repos. Centralise at the group or organisation level. Rotate once, not five times.
How do you inject secrets safely into GitLab CI and GitHub Actions?
Injection means the job receives the secret as an environment variable or file at runtime. The YAML references the name, never the value. Masking tells the platform to redact the string if a script accidentally prints it.
GitLab CI example for a Laravel 12 deploy on PHP 8.3:
# .gitlab-ci.yml — values set in GitLab UI, not here
deploy_production:
stage: deploy
environment:
name: production
url: https://example.com
only:
- main
script:
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- dep deploy production -vvv
variables:
DEPLOYER_HOST: "$PRODUCTION_HOST"
Mark SSH_PRIVATE_KEY, PRODUCTION_HOST, and any API tokens as masked and protected. Protected limits them to pipelines on protected branches. Masked helps when a verbose Composer or npm log dumps environment details.
GitHub Actions equivalent:
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
env:
SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
APP_HOST: ${{ secrets.PRODUCTION_HOST }}
run: |
install -m 600 <(echo "$SSH_KEY") ~/.ssh/id_ed25519
./vendor/bin/dep deploy production
Never pass secrets as plain workflow inputs or workflow_dispatch arguments. Those appear in logs and rerun metadata. Use secrets context only.
For PHP projects, read GitLab CI/CD for PHP projects and CI/CD caching for Composer and npm. Both show how to keep Composer 2.10 and npm 12 installs fast without exposing registry tokens in cache keys.
File-based secrets work better for TLS certificates or Google service account JSON. GitLab supports file-type variables; GitHub Actions can base64-decode into a temp path. Delete the file in an after_script or trap handler.
How do you prevent secrets from leaking into logs and artifacts?
Masking is helpful, not foolproof. A secret split across two echo statements can bypass redaction. Base64 substrings sometimes slip through. Treat log hygiene as a developer habit, not a platform feature.
- Never
print_r($_ENV)orvar_dump(getenv())in CI test bootstraps. - Run PHPUnit and Pest with
--debugoff in CI unless you are chasing a specific failure. - Set
COMPOSER_AUTHonly for the install step, then unset it. - Exclude
.env*,*.pem, andid_rsafrom artifact uploads explicitly. - Turn off Docker buildKit cache export if layers embed ARG secrets.
Automated scanning catches what humans miss. Secrets scanning in Git and CI with Gitleaks walks through running Gitleaks on every merge request. Add it as a required check before deploy stages run.
# Example Gitleaks job in GitLab CI
secret_scan:
stage: test
image: ghcr.io/gitleaks/gitleaks:latest
script:
- gitleaks detect --source . --verbose --redact
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
OAuth and API integrations need the same discipline. See OAuth security best practices for token storage patterns that complement pipeline rules.
If a secret does leak, rotate immediately. Do not wait for the next sprint. Revoke the old key at the provider, update the CI variable, redeploy, and force-push is not a fix—history still holds the value. Use provider audit logs to check for unauthorised use.
How do you rotate secrets without breaking deployments?
Rotation fails when nobody knows which systems hold a given key. Maintain a secret inventory spreadsheet or Vault path list. Each row should name the owner, rotation interval, and dependent services.
A zero-downtime rotation pattern for database passwords on MySQL 8.4 or PostgreSQL 18:
- Create a second database user with the new password and identical grants.
- Update the staging CI variable and staging server
.env; verify connectivity. - Update production CI variable and shared
.envduring a low-traffic window. - Redeploy or reload PHP-FPM so workers pick up the new env without stale opcache holding old config in memory.
- Revoke the old database user after error rates stay flat for 24 hours.
Payment gateway keys for eSewa, Khalti, or Stripe need coordinated rotation. Update the provider dashboard, then CI, then server env, then run a small test transaction. On Laravel eCommerce projects, I schedule rotation outside peak order hours.
SSH deploy keys deserve annual rotation at minimum. Generate a new ed25519 keypair, add the public key to authorized_keys, update GitLab, run a test deploy, then remove the old public key. Document the steps in your internal wiki.
For long-lived apps, pair rotation with ongoing support and maintenance so credential updates are not emergency-only events. Strong Linux system administration practices—correct ownership on .env, mode 600, deploy user separation—reduce the blast radius when a key does compromise.
What tooling and access controls complete the picture?
Secrets management is half technology, half access control. Apply least privilege to who can view and edit CI variables. GitLab maintainer role should not default to everyone on the team.
Useful tooling layers:
- Pre-commit: git-secrets or Gitleaks hook locally before push.
- CI scan: Gitleaks or TruffleHog on every branch.
- Dependency audit:
composer auditandnpm auditin parallel test jobs. - Password generation: the password generator tool for initial random values—never reuse demo passwords.
- Encoding checks: the Base64 encoder/decoder when debugging file-type CI variables locally without committing samples.
Compare GitLab CI and GitHub Actions secret features in GitHub Actions vs GitLab CI for 2026. Multi-cloud teams should read multi-cloud secrets management before spreading credentials across AWS, GCP, and a VPS.
Server hardening complements pipeline rules. Ubuntu server security best practices covers firewall, fail2ban, and SSH hardening that protect secrets already on disk.
Reference the official GitLab documentation on CI/CD variables and the GitHub Actions encrypted secrets guide when configuring masked variables. OWASP Secrets Management Cheat Sheet summarises organisational policies that align with pipeline technical controls.
For enterprise Laravel builds with compliance requirements, enterprise application development and custom software development engagements typically include Vault setup, audit logging, and runbook delivery as part of the deploy pipeline—not as a late add-on.
Proof that these patterns ship real software: Adventure Third Pole Trek runs Laravel with Livewire booking on the same GitLab CI plus Deployer model described here. Notary Kathmandu shares the sister-site pipeline where only deploy keys live in CI and application secrets stay server-side.
Key Takeaways
- Never commit secrets to Git; store them in CI platforms, Vault, or server-side
.envfiles outside release directories. - Mark CI variables as masked and protected; scope production credentials to protected branches and environments.
- Run Gitleaks or equivalent on every merge request before deploy stages execute.
- Rotate keys on a schedule with a documented inventory and staging-first validation.
- Keep pipeline YAML free of credential values—reference variable names only.
- Combine pipeline controls with server hardening and least-privilege access to CI settings.
People Also Ask
Should secrets be stored in environment variables or files in CI/CD?
Environment variables suit most API keys and short tokens. Files work better for multi-line SSH keys, TLS certificates, and JSON service account credentials. Either way, inject at job runtime from the platform secret store. Delete temporary files in cleanup steps. Do not bake either form into Docker image layers.
What is the difference between masked and protected CI variables?
Masked variables are redacted in job logs when the platform detects the literal value. Protected variables are available only to pipelines running on protected branches or tags. Production database passwords should be both masked and protected. Staging-only keys can be masked without the protected flag if feature branches need them.
How often should CI/CD secrets be rotated?
Rotate SSH deploy keys and long-lived API tokens at least annually. Rotate immediately after any suspected leak, employee offboarding with CI access, or vendor breach notification. Database passwords benefit from quarterly rotation on high-value systems. Payment and OAuth credentials follow provider policy, often 90 days for high-risk scopes.
Can you recover secrets from Git history after a leak?
Removing a file in a new commit does not erase history. Use git filter-repo or BFG Repo-Cleaner to purge the blob, force-rotate every exposed credential, and treat old keys as compromised even after history rewrite. Scanning tools should run on full history during incident response.
Build pipelines that keep credentials out of Git
Strong CI CD secrets management best practices are not optional extras for mature teams. They are the baseline that keeps payment integrations, client portals, and production databases safe while automation moves fast. Start with secret scanning on every branch, move credentials into masked CI variables or Vault, and keep production .env on the server—not in artifacts.
If you want help auditing an existing GitLab or Deployer pipeline, review testing and optimization services or reach out via contact us. You can also browse the blog for related CI/CD guides or see more shipped work on the portfolio.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

