Secrets in a GitOps workflow
The repository is the source of truth, and then someone asks where the database password lives. That is the moment the GitOps rule collides with reality, because the answer cannot be "in the repo" and cannot be "not in Git at all" without doing some design work first.
There are two honest answers, and they solve different problems. Most teams need to know the difference before they pick one.
Where the secrets actually live
The first thing to get straight. The secret material itself belongs in a secrets store. AWS Secrets Manager, HashiCorp Vault, something purpose built. API keys, activation keys, SSH keys, registry credentials, all of it. The store is the source of truth for the value. The question is only how Git and the cluster get to it.
Pattern one, encrypt at rest in Git
SOPS encrypts the sensitive fields in a values file before the file ever reaches Git. The encryption key lives in KMS or with a GPG key, not in the repo. When Flux reconciles, the controller assumes an IAM role through a service account annotation, fetches the key from KMS, decrypts the values, and applies.
The nice part is reviewability. The file is in Git, you can diff it, you can see what changed, and the secrets inside are ciphertext. Everything flows through the same review pipeline as the rest of your manifests.
The part nobody mentions. After decryption, the value becomes a normal Kubernetes Secret in etcd. Base64 encoded, which is not encryption, and readable by anyone with read access to that namespace. SOPS protects the secret in Git. It does not protect it in etcd.
Pattern two, fetch at runtime
The External Secrets Operator and Secrets Store CSI take the other path. Nothing secret ever touches Git. The controller fetches the value from the external store and hands it to Kubernetes.
Here's the part most writeups get wrong, so let's be precise. ESO does not keep plaintext out of etcd. Its documented mechanism is to fetch from the external API, then generate a Kubernetes Secret object. That Secret is plaintext in etcd, exactly like the SOPS path. What ESO buys you is different. Nothing secret in Git, and rotation without a commit, because the store is the source of truth and the controller resyncs.
The one tool in this family that genuinely avoids the Secret object is Secrets Store CSI. It fetches at pod mount time and hands the value to the volume, so it never materializes as a Secret in etcd. That is a strictly smaller attack surface, and the cost is that the cluster depends on the store at runtime. If the store is unreachable, new pods can't mount their secrets.
Why this is a security boundary, not a feature debate
Both patterns are valid. I've run both. The honest framing. SOPS decrypts at reconcile and the plaintext lands in a Secret in etcd. ESO syncs from the store and the plaintext also lands in a Secret in etcd, but nothing touches Git and rotation doesn't need a commit. CSI mounts at pod start with no Secret object at all, and the price is a runtime dependency on the store.
So the decision is not "which one keeps plaintext out of etcd," because two of the three don't. It's a simple question. Do you want the secret reviewable in Git (SOPS), rotation without commits (ESO), or no Secret object in etcd at all (CSI)? Pick the property you actually need, and know the cost.
The boring conclusion
The sentence that wins the argument in a review. Decide where plaintext is allowed to exist, and then pick the tool that matches. SOPS means plaintext exists in etcd after reconcile, and so does ESO, the difference is that ESO keeps it out of Git and rotates without a commit. CSI is the only one of the three that keeps the value out of etcd entirely, and the cost is a runtime dependency. All three are defensible. Pretending the trade off doesn't exist is how secrets end up in ConfigMaps.
What's the secret you found in a repo that made you redesign the whole thing?