kubectl get secret app-db -o yaml returns a data block full of unreadable strings. It looks encrypted. It is not. Kubernetes Secret values are base64-encoded, and base64 is a transport encoding anyone can reverse in a single command:
kubectl get secret app-db -o jsonpath='{.data.password}' | base64 -d
That one fact drives every decision below. A native Secret protects nothing on its own. What actually guards it is RBAC (who is allowed to get the object) and, at rest, whether etcd itself is encrypted.
What native Secrets do and do not protect
By default, etcd stores Secret objects without encryption. Encryption at rest is opt-in, configured with an EncryptionConfiguration on the API server:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc:
keys:
- name: key1
secret: <32-byte base64 key>
- identity: {}
Without it, an etcd backup is a backup of every credential in plaintext (base64). With it, the value is encrypted in the datastore but still plain base64 to anyone holding API read access. Those are two different threat models, and native Secrets address the first one only once you deliberately turn it on.
The problem native Secrets never solve is Git. You cannot commit a plaintext Secret manifest to a repository. That gap is where the other two approaches live.
Sealed Secrets: make the manifest safe to commit
Sealed Secrets (the Bitnami controller) adds a CRD and an in-cluster controller holding an asymmetric key pair. You encrypt with the public certificate using kubeseal, and only the controller’s private key can decrypt:
kubectl create secret generic app-db \
--from-literal=password='s3cr3t' --dry-run=client -o yaml \
| kubeseal --format yaml > sealed-app-db.yaml
The resulting SealedSecret is safe in Git. The controller watches for it, decrypts it, and materialises an ordinary Secret in the namespace. Your GitOps pipeline now describes secrets the same way it describes everything else.
Two properties define the boundaries. Sealing is cluster-bound: a SealedSecret is encrypted for one controller’s key, so restoring manifests into a fresh cluster fails until you have backed up and restored the sealing key (itself a Secret in kube-system). Lose that key and every SealedSecret in Git becomes unrecoverable. The default scope is also strict, meaning the ciphertext is bound to the exact name plus namespace; you cannot rename or move a SealedSecret without re-sealing it.
Sealed Secrets stores encrypted values. It does not rotate them, expire them, or record who reads them. Rotating a password stays an edit-and-commit.
Vault: dynamic secrets, leasing, audit
External Vault changes the model. Rather than storing a static credential, Vault can generate one on demand and hand it back with a lease:
vault write database/roles/app-db \
db_name=app-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" max_ttl="24h"
An application authenticates (Kubernetes auth exchanges a pod’s ServiceAccount token for a Vault token), reads database/creds/app-db, and receives a fresh Postgres user valid for one hour. When the lease expires, Vault revokes the credential at the source. A leaked credential then has a blast radius measured in minutes, and every read lands in an audit log.
Pulling those values into pods is usually done through the External Secrets Operator or the Vault Secrets Operator, which sync a Vault path into a native Secret so workloads consume it the normal way.
The cost is operational and concrete: an HA Vault means Raft storage across three or more nodes, an unseal strategy (auto-unseal via a cloud KMS, or human holders of Shamir key shares), tested backups, and an upgrade cadence to follow. Since Vault moved to the BSL license, OpenBao is the open-source fork worth putting on the table.
Common traps
- Treating base64 as a security boundary. The single most frequent mistake. Restrict RBAC and enable encryption at rest; never read
data:as protection. - Not backing up the Sealed Secrets key. Teams commit SealedSecrets, feel safe, then discover after a cluster rebuild that Git alone cannot restore them.
- Deploying Vault for static secrets. If nothing you store is dynamic and you never touch leasing or audit, you have taken on a distributed system’s operational burden to do what one Sealed Secrets controller already does.
- Forgetting that a Secret is referenced by name. Rolling back a Deployment does not roll back the Secret it points to.
How to choose
Team size and compliance requirements draw the line:
- Small team, no external store, GitOps already in place: native Secrets with encryption at rest, plus Sealed Secrets so the repository is the source of truth. Lowest operational cost, and it covers most small and mid-size shops.
- Static secrets, a cloud provider available: a managed secret store behind External Secrets Operator moves separation of environments into IAM, which is exactly where it belongs.
- Dynamic credentials, real audit or rotation mandates, a mixed estate (VMs plus Kubernetes plus CI): Vault earns its keep, but only when you can name today the person who will patch it in eighteen months.
Takeaways
Native Secrets are base64, not encryption; RBAC and etcd encryption at rest are what actually protect them. Sealed Secrets make a manifest safe to commit and fit a GitOps repository, at the price of one cluster-bound key you must never lose. Vault buys dynamic secrets, leasing and audit, at the price of running a stateful distributed system. Match the tool to a fact you can point at (a compliance clause, a rotation mandate, a hybrid estate), not to an aspiration. More material in the automatisation category, and this kind of arbitration is part of industrialising your CI/CD.