One of the first questions that comes up with a GitOps model is where secrets go. The answer most people land on is: not in Git, stored separately in Vault or a cloud secret manager. That works, but it means your cluster state is split across two sources of truth and the secrets store becomes its own operational concern.
The approach here is different: secrets live in Git, but encrypted. SOPS handles the encryption using an Age key that never touches the repository. The .sops.yaml config at the repo root defines which files get encrypted and which fields within them — only data and stringData keys are encrypted in Kubernetes secret manifests, leaving the rest of the file readable for review and diffing.
Editing an encrypted file is sops path/to/file.sops.yaml. SOPS decrypts it into the editor, re-encrypts on save. The Age private key needs to be present in the environment, set via SOPS_AGE_KEY_FILE. Lose the key, lose access to every secret in the repository, so the key itself needs its own backup strategy.
At runtime, Flux applies the encrypted manifests and Kubernetes stores the decrypted values. For applications that need secrets injected at deploy time, the external-secrets operator handles the bridge — it reads from a secret store (in this case the cluster’s own secrets, populated by Flux) and materialises them as Kubernetes Secret objects that application pods can consume normally.
The practical result is that the entire cluster state, secrets included, is in one place. Disaster recovery is a matter of having the Git repository and the Age key. There is no separate secret store to restore or reconcile against.
The main discipline this requires is being careful about what ends up in stringData versus being injected via environment variables or mounted files. Secrets that get logged or appear in application error messages are a real risk regardless of how well the storage side is handled.