Kubernetes Secrets in GitOps (2026): SOPS vs Sealed Secrets vs ESO
The first time someone on a team I worked with committed a Kubernetes Secret to Git, they did it carefully: the values were base64, so they assumed the file was safe. It was not. Base64 is an encoding you can reverse with one shell command, and Git keeps every version forever. That gap between "looks scrambled" and "is encrypted" is where most Kubernetes secrets GitOps problems start. This post explains what a Kubernetes Secret actually protects, what KMS v2 encryption at rest fixes (and what it does not), and how the four common GitOps answers compare in 2026: SOPS, Sealed Secrets, External Secrets Operator with Vault or OpenBao, and the Secrets Store CSI Driver. At the end I give the choice I would make for each kind of team.
Are Kubernetes Secrets encrypted?
No, not by default, and not in the YAML you write. A Secret's data field is base64 so that binary values survive JSON and YAML. Here is the whole "decryption" process, run with a local kubectl client and no cluster:
# kubectl v1.32.3 client; --dry-run=client never talks to a cluster
kubectl create secret generic db-creds \
--from-literal=password='hunter2' \
--dry-run=client -o yaml
# apiVersion: v1
# data:
# password: aHVudGVyMg==
# kind: Secret
# ...
echo 'aHVudGVyMg==' | base64 -d
# hunter2
The Kubernetes Secret docs are blunt about the cluster side too: Secrets are "by default, stored unencrypted" in etcd, anyone with API access to them can read or change them, and anyone who can create a Pod in a namespace can read every Secret in that namespace, including indirectly through a Deployment. That last point gets missed. RBAC on secrets is not the only door; create on workloads is a side entrance. If you have not looked at who can create Deployments lately, the Kubernetes RBAC explainer is a good place to start.
So there are two separate problems. One is the copy in etcd. The other is the copy in Git. GitOps makes the second one unavoidable, because the whole point is that Git describes the cluster.
What does KMS v2 encryption at rest actually protect?
Encryption at rest is about etcd, its backups and its disks. The API server encrypts Secret objects before writing them to etcd, using an EncryptionConfiguration. The recommended provider is KMS: envelope encryption where a data encryption key (DEK) protects the object and a key encryption key (KEK) in an external KMS protects the DEK.
The Kubernetes KMS provider docs say to use KMS v2, which has been stable since v1.29. KMS v1 was deprecated in v1.28 and is disabled by default from v1.29 unless you set the KMSv1=true feature gate. In v2 the API server derives a single-use DEK per encryption from a seed, and the seed rotates when the KEK rotates, which cuts the number of round trips to the KMS. A minimal self-managed config looks like this:
# /etc/kubernetes/enc/encryption-config.yaml
# Pass to kube-apiserver with --encryption-provider-config
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms:
apiVersion: v2 # v1 is deprecated (1.28) and off by default (1.29+)
name: my-kms-plugin
endpoint: unix:///var/run/kms/socket.sock
timeout: 3s
- identity: {} # lets the API server read objects written before encryption
After changing it you rewrite existing Secrets so they pick up the new provider (kubectl get secrets -A -o json | kubectl replace -f -, per the same docs).
On managed clusters much of this is already done. Amazon EKS provides default envelope encryption with KMS v2 for all Kubernetes API data on clusters running 1.28 or higher. GKE encrypts at the storage layer by default and offers application-layer secrets encryption with your own Cloud KMS key as an extra step.
What KMS v2 does not do: it does nothing for a user or a compromised pod who can kubectl get secret, because the API server decrypts on read. It does nothing for the file in your Git repository. It is necessary, and it is one layer.
Why does GitOps make secrets harder?
A GitOps controller applies what is in a repository. If the database password is not in the repository, the controller cannot create it, and you are back to someone running kubectl create secret by hand at 11 pm. If it is in the repository in plaintext, it is in every clone, fork, CI cache and backup, for as long as the history exists. GitGuardian counted 28.65 million new hardcoded secrets on public GitHub in 2025 in its State of Secrets Sprawl 2026; I went through those numbers in the secrets sprawl post.
There are only two ways out. Either Git holds the secret encrypted, and something with the key decrypts it near the cluster (SOPS, Sealed Secrets). Or Git holds only a reference, and something in the cluster fetches the real value from a secret store (External Secrets Operator, the CSI driver, Vault Secrets Operator).
Argo CD's maintainers have picked a side. The Argo CD secret management docs "strongly recommend" populating secrets on the destination cluster with tools such as Sealed Secrets, External Secrets Operator or the CSI driver, and "strongly caution" against injecting secrets during manifest generation. Their reason is concrete: Argo CD stores generated manifests in plaintext in its Redis cache, and they are also reachable through the repo-server's gRPC API. Flux takes a different view and builds SOPS decryption directly into kustomize-controller; the Flux Kustomization spec names SOPS as its only supported decryption provider. My Argo CD vs Flux comparison covers the rest of that split.
SOPS vs Sealed Secrets vs External Secrets: the 2026 comparison
Versions below are the latest releases on GitHub as of 31 October 2026.
| Approach | Latest release | What is in Git | What decrypts or fetches | Key or source of truth |
|---|---|---|---|---|
| SOPS + age | v3.13.3 (23 Jul 2026) | Encrypted values, readable keys | Flux kustomize-controller, KSOPS, helm-secrets | age private key, or cloud KMS / Vault / OpenBao |
| Sealed Secrets | v0.41.2 (26 Oct 2026) | SealedSecret custom resource |
In-cluster controller | Controller's RSA key pair, per cluster |
| External Secrets Operator | v2.14.0 (23 Oct 2026) | ExternalSecret reference only |
ESO controller syncs a native Secret | Vault, OpenBao, AWS, GCP, Azure and others |
| Secrets Store CSI Driver | v1.6.1 (9 Sep 2026) | SecretProviderClass reference |
Node driver mounts files into the pod | Same external stores, via providers |
| HashiCorp Vault | v2.2.1 (22 Oct 2026) | Nothing | Vault itself, via ESO, VSO or agent | Vault (BSL 1.1) |
| OpenBao | v2.7.2 (26 Oct 2026) | Nothing | OpenBao, via ESO or agent | OpenBao (MPL-2.0) |
How SOPS and age work with Flux and Argo CD
SOPS encrypts the values in a YAML or JSON file and leaves the keys readable, so a diff still tells you which field changed. Each file gets its own data key, encrypted for every recipient listed in the file's sops metadata block, plus a MAC over the content so tampering is detected. The project was donated to the CNCF as a Sandbox project in 2023 and now lives under the getsops organisation.
age is the key type most teams use with it: one line of public key, one line of private key, no keyring. Two 2026 details are worth knowing. age v1.3.0 added native post-quantum recipients based on hybrid ML-KEM-768 (age-keygen -pq, recipients starting age1pq1), and the SOPS 3.12.0 changelog added support for those hybrid identities. Also, 3.13.2 shipped a MAC bug for YAML lists containing comments that 3.13.3 fixed, so pin 3.13.3 rather than "3.13".
On Flux, SOPS is native: you put the age private key in a Secret whose key name ends in .agekey and set spec.decryption.provider: sops on the Kustomization. On Argo CD, you add a plugin: KSOPS, a Kustomize exec plugin, or helm-secrets for Helm values. Both run in the repo-server, which is exactly the generation-time pattern Argo CD's docs caution against. Plenty of teams run it anyway, with network policies around Redis and the repo-server, which is the mitigation those docs suggest.
The weaknesses are operational. Whoever holds the age private key can decrypt every file encrypted for it, including old commits. Removing someone means re-encrypting and, if the key leaked, rotating the real credentials. The step-by-step version, including rotation, is in my SOPS and age guide.
Sealed Secrets: simple, per-cluster, and easy to lose
Sealed Secrets flips the model. A controller in the cluster generates an RSA key pair and publishes the certificate. Developers run kubeseal with that certificate to produce a SealedSecret resource that only the controller can decrypt. It works the same in Argo CD and Flux because decryption happens on the destination cluster, which is why Argo CD lists it as a recommended pattern.
Two details catch people. First, scope: by default a SealedSecret is strict, meaning the name and namespace are part of the encrypted data, so renaming it breaks decryption. namespace-wide and cluster-wide scopes loosen that. Second, keys: the controller adds a new sealing key every 30 days by default, keeping old keys so existing SealedSecrets still decrypt. The README is careful to say this renewal is not rotation of your secrets; if a sealing key leaks, you still have to change the underlying passwords.
The real failure mode is disaster recovery. The private keys live only in the cluster, as Secrets labelled sealedsecrets.bitnami.com/sealed-secrets-key. Rebuild the cluster without backing those up, and every SealedSecret in Git becomes unreadable. Back them up (the README shows the kubectl get secret -l command), and store that backup somewhere better than a laptop. Multi-cluster also means either one key per cluster and re-sealing per environment, or sharing a key and accepting the larger blast radius.
External Secrets Operator, Vault, OpenBao and the CSI driver
If the organisation already has a secret store, the best GitOps answer is often to keep secrets out of Git entirely. External Secrets Operator reads from Vault, OpenBao, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault and more, and writes a normal Kubernetes Secret. Git holds an ExternalSecret that says which key to fetch and how often. ESO reached v1.0.0 in November 2025 and v2.0.0 in February 2026, which removed the unmaintained Alibaba and Device42 providers. It is a CNCF project. My ESO with Vault guide covers setup and the 2025 release pause.
The store matters more than the operator. HashiCorp Vault is now on a 2.x line and is licensed under the Business Source License, with IBM named as licensor. OpenBao is the MPL-2.0 fork run under the OpenSSF, with releases on its own cadence. For an ESO user both look similar; for legal and procurement they do not. Cloud secret managers avoid running either, at the price of per-secret and per-call billing and a cloud dependency.
The Secrets Store CSI Driver goes one step further: it mounts secrets as files directly into the pod from the external store, and can optionally sync them into a Kubernetes Secret. If you skip the sync, there is no copy in etcd at all. The trade-off: pods cannot start when the store is unreachable, and apps must read files, not environment variables.
What all of these have in common is that the cluster needs an identity to talk to the store. Static tokens recreate the problem you were trying to solve. Use workload identity (IRSA or EKS Pod Identity, GKE Workload Identity, Vault's Kubernetes auth method) so no long-lived credential exists to leak. This matters more now that coding agents open pull requests against infrastructure repos: an agent can paste a token into YAML as easily as a tired human, and the fix is the same, a design with no token to paste.
Which Kubernetes secrets approach fits which team?
My rough decision rules:
- One or two clusters, Flux, small team, no vault: SOPS with age. Native support, nothing extra to run, and the encrypted file reviews well in pull requests. Put two recipients on every file (the cluster key and an offline break-glass key).
- Argo CD, no vault, want the simplest thing: Sealed Secrets. Decryption stays on the cluster, which matches Argo CD's own advice. Back up the sealing keys on day one.
- Many clusters, or a security team that already runs Vault, OpenBao or a cloud secret manager: External Secrets Operator. Rotation becomes a store operation rather than a Git commit, and offboarding a developer means removing their store access, not re-encrypting a repo.
- Regulated workloads that must not have secrets in etcd: the CSI driver without Secret sync, with the availability trade-off accepted.
Mixing is normal: ESO for application secrets, SOPS for the bootstrap secret ESO needs to authenticate. Whatever you choose, still turn on KMS v2 at rest, restrict who can create workloads in namespaces with Secrets, and run a secret scanner in CI, because the most common leak is a plaintext file nobody meant to commit.
FAQ
Is base64 encryption in Kubernetes Secrets?
No. Base64 is a reversible encoding used so binary data fits in YAML and JSON. Anyone who can read the manifest or the Secret object can decode it with base64 -d. Protection comes from RBAC, encryption at rest and keeping plaintext out of Git.
Is SOPS better than Sealed Secrets?
Neither is better in general. SOPS works across environments and is native in Flux, but you manage the decryption keys yourself. Sealed Secrets is simpler for Argo CD and decrypts only in the cluster, but each cluster's keys must be backed up or every SealedSecret is lost with the cluster.
Can I store Kubernetes Secrets in Git safely?
Yes, if they are encrypted before they are committed (SOPS or Sealed Secrets) or if Git holds only references to an external store (External Secrets Operator, CSI driver). Plaintext or base64 Secrets in Git should be treated as leaked, even in a private repository.
Do I still need encryption at rest if I use External Secrets Operator?
Yes. ESO writes ordinary Kubernetes Secrets, which are stored in etcd. Managed services like EKS 1.28 and later already apply KMS v2 envelope encryption by default; on self-managed clusters you configure an EncryptionConfiguration yourself.
This quarter, if I were taking over a cluster, I would do three things in order. Check that encryption at rest uses KMS v2, not v1 or nothing. Grep the GitOps repos and their history for kind: Secret with plaintext or base64 values, and rotate anything I found. Then pick one of the patterns above per environment and write it down, because the worst setup I see is not the wrong tool, it is three tools chosen by three people with no backup of anybody's keys.
Related: Kubernetes Security in 2026: First Attack in 18 Minutes, and What Stops It, Workload Identity Federation Explained (2026): OIDC Instead of Cloud Keys and How to Stop Secrets Leaking Into Git: Gitleaks, Pre-commit, CI (2026).
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
Hardened Container Images in 2026: DHI Free, Bitnami Gone, 515 CVEs to 0
Container base images in 2026 compared: Docker Hardened Images went free under Apache 2.0, Bitnami moved its catalog to bitnamilegacy, Chainguard cut its free tier to five images, and a grype scan of 12 base images found 515 CVEs in python:3.13 and zero in four minimal images.
12 minOct 20, 2026Kyverno vs Gatekeeper vs ValidatingAdmissionPolicy in 2026
Kubernetes policy engines compared for 2026: Kyverno 1.19 (CNCF graduated, ClusterPolicy deprecated for CEL), OPA Gatekeeper 3.23 with CEL and generated VAPs, built-in ValidatingAdmissionPolicy (GA 1.30) and MutatingAdmissionPolicy (stable 1.36), latency numbers, and when to use which.
13 minOct 20, 2026Container Runtime Security in 2026: Falco vs Tetragon vs KubeArmor
Kubernetes runtime security in 2026: what Falco 0.45, Tetragon 1.7 and KubeArmor 1.7 actually do, detection versus in-kernel prevention, the runc, NVIDIA toolkit and containerd escape CVEs of 2025 and 2026, and where CNAPP vendors fit.
13 min