Secrets Management for Infrastructure as Code: SOPS vs. Vault vs. Kubernetes Secrets

Secrets Management for Infrastructure as Code: SOPS vs. Vault vs. Kubernetes Secrets - Build Archive

Every “Infrastructure as Code” pitch runs into the same awkward question eventually: if everything is code, checked into git, reviewed and versioned — where does the database password go? Committing it in plaintext is the obvious mistake everyone knows to avoid; what’s less obvious is that the next three things people usually try instead each trade one problem for a different one, and picking the right tool depends on which trade-off actually fits your setup.

The Naive Fixes, and Why Each One Has a Ceiling

Environment variables injected by CI keep secrets out of git, but they end up in the CI system’s own logs and job definitions instead — a different plaintext location, not a solved problem. A .env file kept out of version control via .gitignore works until someone’s editor auto-saves it into a commit anyway, or it needs to reach a second machine and travels over Slack or email to get there. Cloud provider “secret” resources referenced directly in Terraform — an AWS Secrets Manager ARN, say — solve storage but not distribution: the values still need to reach a running process somehow, and that’s a separate problem from where they’re stored at rest.

I’ve mentioned Vault in passing before, as one piece of a best-of-breed DevOps stack, without ever weighing it against the alternative — worth doing properly here. The actual question worth asking isn’t “where do I hide this” — it’s “who and what needs to read this, and how do I prove that later.” That’s the fork in the road between the two real approaches below.

Approach One: Encrypt the File Itself, Commit the Ciphertext

Mozilla’s SOPS encrypts specific values inside a YAML or JSON file, leaving the structure readable and only the secret values themselves as ciphertext:

# secrets.yaml — after `sops -e -i secrets.yaml`
database:
    password: ENC[AES256_GCM,data:Kj3n...,iv:...,tag:...,type:str]
    host: db.internal.example.com   # not a secret, stays plaintext

The whole file is safe to commit — Terraform, or a Kubernetes controller via the SOPS operator, decrypts it at the point of use with a key from AWS KMS, GCP KMS, or age/PGP:

sops -d secrets.yaml | terraform apply -var-file=/dev/stdin

What this buys you: a normal git workflow. Diffs, blame, pull request review, branch history — all work exactly like they do for any other file, because structurally it is just a file, encrypted or not. What it doesn’t give you: rotation, expiry, or access auditing. If a password encrypted into that file needs to change, you re-encrypt and commit a new version — there’s no built-in mechanism forcing a 90-day rotation, and no log of who actually decrypted the file and when versus who merely had permission to.

Approach Two: A Service That Hands Out Secrets on Request

HashiCorp Vault takes the opposite shape: secrets aren’t files at all, they’re API responses, fetched at runtime by something that authenticates first:

$ vault kv get -field=password secret/database/prod
$ vault login -method=aws role=my-app-role   # short-lived token, not a static credential

Nothing is ever committed to git, because nothing secret ever lives in a file that could be committed — the value exists only in Vault’s storage and briefly in the requesting process’s memory. This is what unlocks the features SOPS structurally can’t offer: dynamic secrets (Vault can generate a fresh, uniquely-scoped database credential per request, with its own automatic expiry, instead of everyone sharing one static password), and a real audit log of every read — which identity asked for which secret, and when.

The cost is operational weight that’s easy to underestimate from the outside: Vault is itself a service that needs to be deployed, unsealed after every restart (or configured with auto-unseal against a cloud KMS, which is really the only sane option once you’re past a toy setup), backed up, and kept available — because the moment it’s down, every service that fetches secrets from it at boot fails to start. That’s a meaningful new single point of failure to take on, and it’s not proportional to every use case.

Kubernetes Native Secrets Are a Trap Disguised as a Solution

It’s worth naming this one directly because it looks like it solves the problem without adding either tool above: Kubernetes Secret objects are base64-encoded, not encrypted, by default.

$ kubectl get secret db-creds -o jsonpath='{.data.password}' | base64 -d
supersecret123

Base64 is an encoding, not encryption — anyone with read access to the Secret object, or to etcd‘s data at rest if it isn’t separately encrypted, can reverse it in one command with no key required at all. Kubernetes has a real fix (encryption at rest via a KMS provider), but it isn’t the default, and a raw Secret manifest committed to git — which people do, because it looks like just another YAML file — is functionally the same mistake as committing a plaintext password with extra steps.

Which One, for What

The honest answer is that these aren’t really competing for the same job. A homelab or a small team’s infrastructure, where the actual threat model is “don’t leak this in a public git history” and rotation happens by hand anyway, is well served by SOPS — it’s a file format change, not new infrastructure to run and keep alive. Vault earns its operational cost when you actually need what only a live service can give you: credentials that expire on their own, an audit trail regulators or a security team will actually ask for, or dynamic secrets issued per-workload instead of one shared value everyone reuses. Reaching for Vault because it’s the “more serious” tool, without a concrete need for dynamic secrets or auditing, mostly just adds a new thing that can go down.

The Checklist

  • Never commit a real secret in plaintext — including inside a Kubernetes Secret manifest, which is encoded, not encrypted.
  • SOPS for a git-native workflow where diff-and-review matters more than rotation or audit logs.
  • Vault when you specifically need dynamic, short-lived credentials or a real access audit trail — and only once you’re prepared to keep it as highly available as everything that depends on it.
  • If you enable Kubernetes’ native Secret encryption at rest, verify it actually applies — it’s a cluster-level config that’s easy to assume is on by default because the object type is called “Secret.”

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *