Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCV · GitOps Anti-PatternsPlaintextSecrets

Plaintext secrets in Git — the secrets that the repository remembers

Advanced⏱ ~24 mingitgitleaks

What you'll learn

  • Identify the four shapes a plaintext secret in a GitOps repository takes
  • Apply pre-commit and CI scanning to catch secrets before they reach the default branch
  • Distinguish Sealed Secrets, SOPS, and External Secrets as the three replacement patterns
  • Design a vault-bound reference for credentials the controller cannot store locally

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Git remembers. A secret committed to a Git repository in plaintext is a secret the repository returns to every clone, every fork, every CI runner that has ever fetched it, every backup the team has ever taken, every laptop that has ever held a working copy. The credential does not have to be exfiltrated by an attacker; it is exfiltrated by the act of being committed.

The four shapes the violation takes

A plaintext secret in a GitOps repository can take any of four shapes, each equally recoverable by anyone who clones the repository at any point in its history.

    flowchart LR
        A["Kubernetes Secret in YAML"] --> E["Plaintext in repo"]
        B[".env file committed"] --> E
        C["Helm values.yaml credential"] --> E
        D["Cloud key in CI variable"] --> E
        E --> F["Every clone inherits"]
        E --> G["Every CI runner fetches"]
        E --> H["History retains after removal"]
  • Kubernetes Secret manifests. A Secret resource with stringData.password: hunter2.
  • .env files. A .env file in the chart’s templates directory.
  • Helm values.yaml credentials. A values.production.yaml with database credentials.
  • CI variable files. A terraform.tfvars committed for “convenience”.

All four converge: the credential lives in a Git commit, and the Git commit lives forever.

The encrypted or referenced replacement

The replacement is one of three patterns. Each replaces the value in the repository with a reference whose key holder lives outside the repository.

sops --encrypt --age "$SOPS_AGE_RECIPIENT" \
     secret.yaml > secret.enc.yaml
  • Sealed Secrets. A controller-side key holder. The controller runs kubeseal against the plaintext to produce a SealedSecret whose ciphertext decrypts only on the target cluster. Cluster-bound; not portable.
  • SOPS. A file-level encryption tool. The repository holds the ciphertext; the controller holds the private key. Multi-recipient; portable; diff-friendly.
  • External Secrets. A controller that synchronises secrets from an external vault (AWS Secrets Manager, HashiCorp Vault). The repository holds an ExternalSecret manifest.

The choice depends on the trust boundary. The wrong choice is to commit plaintext and call the choice “temporary”.

The pre-commit and CI gates

The replacement patterns assume the secret never reaches the repository. The gates that enforce that assumption:

gitleaks protect --staged --redact

gitleaks protect --staged runs against the staged changes in a pre-commit hook and blocks the commit if a secret is detected. The same scanner runs in CI on every pull request; the merge button is disabled when a secret is found.

The discipline has three layers:

  • Pre-commit. The developer’s first defence.
  • CI scan. The repository’s defence.
  • Forge scanning. The vendor’s defence. The alert arrives after the merge.

Production discipline

  1. No plaintext secrets in the repository. Ever. The exception process is the rotation, not the commit.
  2. Pre-commit, CI, and forge scanning are all on.
  3. The reference pattern matches the trust boundary. Sealed Secrets for cluster-bound, SOPS for multi-cluster, External Secrets for credentials that already live in a vault.
  4. Rotation is the response to a leak. Removing the secret in a later commit is not the response.

Cross-course references

  • This course, Part LXXXII (GitOpsSecrets) - Sealed Secrets, SOPS, External Secrets in depth.
  • This course, Part CIII (CommittedSecrets) - pre-commit and CI scanning, the rotation response.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer commits a Kubernetes Secret with a database password to the GitOps repository in plaintext, then removes the file in a later commit. What is the state of the credential?

  2. Q2. A pre-commit hook is a sufficient defence against plaintext secrets in a GitOps repository.

  3. Q3. Name the three replacement patterns for plaintext secrets in a GitOps repository and the trust boundary each fits.

  4. Q4. Diagnose a plaintext-secret incident and recommend the architecture that prevents it.

    An engineer commits a `.env.production` file with a database password to the GitOps repository's templates directory. The commit is merged. A week later, a former contractor's laptop, which still holds a clone from three months ago, is compromised. The contractor never had access to the production database; the credential they exfiltrated did.

Passing score: 75%. Answers are checked in this browser.