Managing Secrets in Infrastructure as Code Without Leaking Them
Secrets in IaC are where good intentions go to die in git history. Here's a practical approach to secret management across tools — and the AI guardrails to use.
- #iac
- #secrets
- #security
- #vault
- #encryption
- #gitops
The single most common security incident I’ve seen across 25 years isn’t a sophisticated breach. It’s a database password committed to git in 2021, sitting in the history of a repo a contractor cloned in 2024. IaC made this worse, not better — because IaC encourages putting everything in a repo, and “everything” too often includes secrets.
Here’s how to handle secrets in IaC so they never hit your git history, across the common tools, plus where AI helps and where it’s a liability.
The cardinal rule
A plaintext secret never enters version control. Ever.
Everything else is implementation detail in service of that rule. Once a secret is in git, it’s in git forever — across every clone, fork, and backup — even if you delete it in the next commit. Rotation is the only real remediation, and rotation is painful. So the goal is prevention.
Four patterns, roughly in order of preference
1. External secret stores (best). The secret lives in a dedicated manager — HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager. Your IaC stores only a reference (a path or ARN), and resolves it at apply/runtime. The secret value never touches your repo or your state in plaintext.
# Ansible pulling from Vault at runtime — no secret in the repo
db_password: "{{ lookup('community.hashi_vault.hashi_vault',
'secret=secret/data/prod/db:password') }}"
2. Encrypted-in-repo. The secret lives in the repo but encrypted, decrypted only by authorized parties. Ansible Vault, SOPS, and git-crypt do this. Good when you want everything in one place and a secret store is overkill.
# SOPS encrypts values with a KMS key; the file is safe to commit
sops --encrypt --kms arn:aws:kms:...:key/abc secrets.yaml > secrets.enc.yaml
3. Injected at deploy time. The secret lives in your CI/CD system’s secret store and is injected as an environment variable during the pipeline run. Never written to disk, never in the repo.
4. Manual / out-of-band (last resort). A human sets the secret directly on the target. Doesn’t scale, isn’t reproducible, but sometimes it’s the pragmatic bootstrap for the one credential everything else depends on.
Mind the state file
Here’s the trap people miss: even if your source is clean, many IaC tools write resolved secrets into their state file in plaintext. A database password passed to a resource ends up readable in terraform.tfstate or equivalent.
So treat state as a secret itself: store it in an encrypted backend, lock down access, and never commit it. The cleanest fix is to not pass secrets through IaC at all where possible — have the resource pull from a secret store at runtime so the IaC only handles the reference.
Detection: catch leaks before they merge
Prevention needs a backstop. Run a secret scanner in CI and as a pre-commit hook — gitleaks, trufflehog, or detect-secrets. They scan diffs for things that look like credentials and fail the build.
# pre-commit hook
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
This is your seatbelt. It will occasionally false-positive on a test fixture, but it has stopped real secrets from merging on every team I’ve put it on. Add it before you do anything else — it’s the highest-leverage 10 minutes here.
Where AI helps — carefully
AI is useful around secret management, but this is the area where you must be most careful about what you paste.
Good uses:
- Refactoring secrets out of code. Paste a sanitized playbook (secrets replaced with
REDACTED) and ask “rewrite this to pull these values from Vault instead of inline.” It restructures the lookups for you. - Generating scanner config and pre-commit hooks for your stack.
- Explaining a leak’s blast radius — “this AWS key was committed; what can it do and what’s my rotation checklist?”
- Reviewing for the state-file trap — ask “does this config write any secret into state in plaintext?”
The hard rule: never paste a real secret into a model. Treat every prompt like a public Slack post. Redact before you send. If you wouldn’t screenshot it into a public channel, don’t put it in a prompt. I keep secret-handling prompts that bake this redaction discipline in.
A reference layout
Putting it together, here’s the shape I aim for:
repo/
├── infra/ # IaC — references only, no plaintext secrets
├── secrets.enc.yaml # SOPS-encrypted, safe to commit
├── .sops.yaml # which keys decrypt what
├── .gitleaks.toml # scanner config
└── .pre-commit-config.yaml
Runtime secrets come from Vault. A small set of bootstrap secrets are SOPS-encrypted in-repo. CI injects deploy-time credentials. Scanners gate every commit. State lives encrypted in a locked-down backend.
The checklist
Before any IaC touches secrets, I confirm:
- No plaintext secret is in the repo or will be.
- Secrets resolve from a store or encrypted file, not inline.
- The state file is encrypted and access-controlled.
- A secret scanner runs in pre-commit and CI.
- There’s a written rotation runbook for when one leaks anyway.
- Any AI prompt involving secrets is redacted first.
Secret management isn’t glamorous and it’s rarely the thing anyone celebrates. But it’s the difference between a quiet Tuesday and a breach notification. Get the cardinal rule right — nothing plaintext in git — and build everything else around defending it. Keep your redaction-safe prompts in a prompt library and let the scanners be your backstop.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.