Git, CI/CD & GitOpsCIII · Infrastructure Repository Anti-PatternsAntiPatterns
Committed secrets in IaC — credentials that should never reach Git
What you'll learn
- Identify the four forms a committed secret takes in an IaC repository
- Explain why git history, reflogs, pack files, and forks keep a secret alive after deletion
- Distinguish prevention (external secret stores, SOPS) from detection (gitleaks, push protection)
- Plan the rotation-first response when a secret has already been committed
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
The fastest way to lose a production credential is to put it in a Terraform file and push. The second fastest is to put it in an Ansible variable file and push. The third is to put it in a Kubernetes manifest and push. The pattern is the same; only the syntax changes.
The four forms
A committed secret in an IaC repository is almost always one of four shapes:
- Inline literal. A password, API key, or token typed
directly into a
*.tf,*.yaml, or*.ymlresource block. - Variable default. A
variable "db_password" { default = "..." }line that ships a real value alongside the schema. - Dotenv file. A
.env,.envrc, orsecrets.yamlchecked in alongside manifests. - Generated artifact. A
terraform.tfstate,kubeconfig, or~/.aws/credentialsfile that a workflow copies into the repository by accident.
flowchart LR
A["inline literal"] --> D["committed secret"]
B["variable default"] --> D
C["dotenv file"] --> D
E["generated artifact"] --> D
D --> F["every clone"]
D --> G["every fork"]
D --> H["CI logs"]
D --> I["backups"]
All four are the same violation. The fix differs by shape but the response is the same: rotate the credential, then prevent the shape from recurring.
Why deletion does not delete
A git rm secrets.yaml and a deletion commit change only the
current tree. The original blob is content-addressed: its
SHA-1 resolves to the same bytes the moment the credential is
pushed. Anyone who cloned between the leak and the deletion has
the bytes. The reflog references the old commit. The pack file
keeps the unreachable blob for the gc.pruneExpire window.
Forks owned by other users cannot be reached at all.
This is not a tooling limitation. It is the design of a content-addressed object store. The lesson of Part XXXV applies in full: cleanup is forensic, rotation is the fix, and the secret is compromised from the moment the commit lands.
What prevention looks like
Three patterns stop secrets from reaching the repository in the first place:
- External secret stores. HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager. The repository references a name; the value is fetched at runtime by an identity that does not have its own long-lived credential.
- Encrypted-in-repo values. SOPS-encrypted YAML and JSON files. The encrypted blob is safe to commit; the decryption key lives in KMS, in a CI secret, or in an age recipient set.
- Push protection. GitHub, GitLab, and Bitbucket all offer push-time scanners that reject commits matching known credential patterns before they reach the remote.
The discipline that ties them together is the same as in Part XXXV: secrets are not committed because they are not files in the repository; they are references resolved at runtime by identities that are themselves short-lived.
Production discipline
- Reference, never inline. Terraform variables, Ansible vars, and Kubernetes Secrets references should hold names. Values come from external stores.
- Push protection at the forge. Enable the platform’s
built-in scanner; supplement with a pre-commit
gitleakshook for patterns the platform misses. - Rotation is the only response to a leak. Rotate first, rewrite history second, audit consumers third.
- Treat the postmortem as a control update. Every leak
becomes a new
gitleaksrule, a new pre-commit check, or a new architectural rule.
Cross-course references
- This course, Part XXXV (SecretsInGit) covers the mental-model error that makes engineers believe a deletion commit deletes the secret.
- This course, Part XXXVI (HistoryCleanup) covers
git filter-repoand BFG for the forensic cleanup step. - Terraform for Production Sysadmins Parts IX-XII cover state, where secrets-in-state is the second major form of IaC credential exposure.
- Ansible for Production Sysadmins Part XXXVII covers Ansible Vault and external credential helpers.
Quiz
Knowledge check · 4 questions
Q1. An engineer pushes a Terraform file containing an inline AWS access key, realises the mistake, runs git rm, force-pushes, and notifies the team. Has the secret been contained?
Q2. A SOPS-encrypted YAML file containing a database password is safe to commit to a public repository.
Q3. Name the three patterns that prevent secrets from reaching an IaC repository, and the one that responds after they have already reached it.
Q4. Decide the response order for a committed IaC secret, and identify the channels that are now compromised.
A Terraform variable file containing a production RDS password is committed to a private monorepo. The team's pre-commit hook was disabled two months ago. CI runs terraform plan on every PR and the password appears in three CI logs. The repository has 12 internal forks and a mirror in a CI vendor's cache. The password is in use by one production service.
Passing score: 75%. Answers are checked in this browser.