Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

CI CD Secrets Management Best Practices

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.

CI/CD Secrets FlowDeveloperNo secrets in GitGit RepoCode onlyCI PlatformMasked varsSecret StoreVault / native storeProductionRuntime .envSecrets never committed — injected at job runtime
CI CD secrets management best practices: credentials flow from a secret store through the CI platform into production, never through Git history.

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

  1. Block commits containing high-entropy strings or .env files via pre-commit hooks.
  2. Enable masked and protected flags on all CI variables that hold credentials.
  3. Run secret scanning on every push and pull request.
  4. Restrict production secrets to protected branches and tagged releases.
  5. Audit variable access quarterly and after any team member offboarding.
  6. Keep production .env on 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 optionBest forTrade-offs
CI-native variables (GitLab, GitHub)Small teams, single-repo Laravel or WordPress projectsLimited rotation automation; vendor lock-in per platform
HashiCorp Vault / cloud secret managersMulti-app, multi-environment, compliance auditsExtra infra to operate; needs auth wiring into CI
Server-side .env (Deployer shared dir)Traditional PHP-FPM VPS deploysSecrets live on disk; backup and permission discipline required
Encrypted files (Ansible Vault, SOPS)Infra-as-code repos with few runtime secretsKey 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.

Safe vs Unsafe InjectionUnsafeHardcoded in YAMLecho $SECRET in scriptSecrets in Docker layersSafeCI masked variablesRuntime env injectionServer-side .env fileLaravel production patternCI holds deploy key onlyApp reads DB password from shared .env on VPS
Safe CI CD secrets injection keeps credentials in platform stores and server runtime files, not in committed pipeline definitions.

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) or var_dump(getenv()) in CI test bootstraps.
  • Run PHPUnit and Pest with --debug off in CI unless you are chasing a specific failure.
  • Set COMPOSER_AUTH only for the install step, then unset it.
  • Exclude .env*, *.pem, and id_rsa from 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.

Pipeline Secret GatesPushGitleaksFail on leakTestBuildDeployProtectedBlocked pathLeaked secret stops pipeline before deployNo masked echo can undo a commit in history
Secret scanning gates in CI/CD block deploy stages when credentials appear in commits, enforcing CI CD secrets management best practices before production.

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:

  1. Create a second database user with the new password and identical grants.
  2. Update the staging CI variable and staging server .env; verify connectivity.
  3. Update production CI variable and shared .env during a low-traffic window.
  4. Redeploy or reload PHP-FPM so workers pick up the new env without stale opcache holding old config in memory.
  5. 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 audit and npm audit in 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.

Secret Store Decision TreeNew project?Single VPSDeployer + .envMulti envCI variablesK8s / multi cloudVault / ESOProduction outcomesAudit trail, fast rotation, no Git exposureWorks with Laravel 12, PHP 8.3+, GitLab CI
Choose CI/CD secret storage by deployment target: VPS shared .env, native CI variables, or Vault for Kubernetes and multi-cloud workloads.

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 .env files 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

Store credentials outside Git in a vault or CI secret store, inject at runtime via masked variables, scan commits, rotate on schedule, and never echo secrets into logs or artifacts.

Pick from four realistic tiers. CI-native variables in GitLab or GitHub suit small single-repo Laravel or WordPress projects but offer limited rotation automation. HashiCorp Vault or cloud secret managers fit multi-app, compliance-heavy setups at the cost of extra infrastructure. On VPS PHP-FPM deploys with Deployer 7, production credentials belong in a shared server-side .env outside the release symlink. Ansible Vault or SOPS work for infra-as-code repos with few runtime secrets, though you still must protect the encryption key itself.

Masked variables are redacted in job logs when the platform detects the literal value. Protected variables run only on protected branches or tags. Production database passwords should be both; staging keys can be masked alone if feature branches need them.

Environment variables suit most API keys and short tokens. Files work better for multi-line SSH keys, TLS certificates, and JSON service account credentials. Inject either form at job runtime from the platform store, delete temp files in cleanup steps, and never bake them into Docker image layers.

Reference variable names in YAML, never literal values. In GitLab CI, set SSH_PRIVATE_KEY, PRODUCTION_HOST, and API tokens as masked and protected, scope deploy_production jobs to the production environment on main, and pipe the SSH key through ssh-add at runtime. In GitHub Actions, pull values from the secrets context inside an environment: production job block. Never pass credentials via workflow_dispatch inputs or plain workflow arguments—they appear in logs and rerun metadata. For PHP builds, set COMPOSER_AUTH only during the install step, then unset it.

Masking helps but is not foolproof—split echoes and base64 substrings can bypass redaction. Never print_r($_ENV) or var_dump(getenv()) in CI test bootstraps. Run PHPUnit and Pest without debug in CI unless chasing a failure. Exclude .env, .pem, and id_rsa from artifact uploads explicitly. Turn off Docker BuildKit cache export if layers embed ARG secrets. Run Gitleaks on every merge request as a required gate before deploy stages. If a secret leaks, rotate immediately at the provider, update the CI variable, redeploy, and check provider audit logs—deleting the file in a new commit does not fix history.

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. Database passwords on high-value systems benefit from quarterly rotation; payment and OAuth credentials follow provider policy, often 90 days for high-risk scopes.

Maintain a secret inventory naming each owner, rotation interval, and dependent services. For MySQL 8.4 or PostgreSQL 18, create a second database user with the new password and identical grants, update staging CI variables and server .env first, verify connectivity, then update production during a low-traffic window and reload PHP-FPM so workers pick up fresh config. Revoke the old database user after error rates stay flat for 24 hours. For Stripe, eSewa, or Khalti keys, update the provider dashboard, then CI, then server env, then run a small test transaction outside peak order hours. SSH deploy keys: generate a new ed25519 pair, add the public key, update GitLab, test deploy, remove the old key.

Removing a file in a new commit does not erase history—the blob remains reachable. Use git filter-repo or BFG Repo-Cleaner to purge exposed blobs, then force-rotate every credential that ever appeared in the repository. Treat old keys as compromised even after a history rewrite. Scanning tools should continue running on future pushes to catch regressions. Force-push alone is not a fix.

Block commits containing high-entropy strings or .env files via pre-commit hooks. Enable masked and protected flags on all CI variables holding 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 .env on the server filesystem, not in the artifact bundle. Prefix variables by environment—STAGING_DB_PASSWORD versus PROD_DB_PASSWORD—and document which service owns each secret in a runbook outside the repo.

Project-scoped variables are fine for a single staging app. Once one pipeline file deploys both staging and production, move production credentials to environment-scoped entries with approval gates before production jobs run. GitLab environments and GitHub Actions environments both support this pattern. Avoid duplicating the same API key across five microservice repos—centralise at group or organisation level so you rotate once, not five times.

Match storage to deployment target. Small teams on single-repo Laravel 12 projects often use GitLab or GitHub native variables for deploy keys and build-time tokens. Kubernetes and multi-cloud workloads benefit from HashiCorp Vault with External Secrets Operator or AWS Secrets Manager. Traditional PHP-FPM VPS deploys with Deployer 7 should keep application database passwords in a shared server .env while CI holds only the SSH deploy key and optional build-time values like a Sentry DSN. Vault adds operational overhead but pays off when compliance audits require centralised paths and access logging.

Layer three checks. Pre-commit: git-secrets or a Gitleaks hook locally before push. CI: Gitleaks or TruffleHog on every branch and merge request, configured as a required check before deploy stages execute—example pattern uses ghcr.io/gitleaks/gitleaks:latest with gitleaks detect --source . --verbose --redact. Dependency jobs: composer audit and npm audit in parallel test stages. Automated scanning catches what masking and developer habits miss, especially when verbose Composer 2.10 or npm 12 logs dump environment details.

On Deployer 7 VPS deploys I maintain, GitLab CI holds only the SSH deploy key and maybe a Sentry DSN for build-time checks. Database passwords and application secrets live in a shared .env outside the release symlink on the server. The app reads them at runtime after deploy. That split keeps the pipeline powerful without turning it into a password warehouse. Production keys never touch feature-branch jobs, and artifact bundles do not carry a full .env file that could leak if misconfigured upload rules slip through.

Secrets management is half technology, half access control. Apply least privilege to who can view and edit CI variables—the GitLab maintainer role should not default to everyone on the team. Audit variable access quarterly and immediately after offboarding. Pair pipeline rules with server hardening: correct ownership on .env, mode 600, deploy-user separation on Ubuntu reduces blast radius if a key compromises. Reference OWASP Secrets Management Cheat Sheet for organisational policies that align with masked variables, protected branches, and environment approval gates on production deploys.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: