GitOps says everything lives in Git — but a Kubernetes Secret is only base64
encoded, not encrypted. Committing one straight to a repository would expose your
passwords to anyone who can read it. So how do we manage secrets while still keeping Git
as the single source of truth? In this post we’ll look at the common patterns and set up
one of them end to end.
The problem
A plain Secret looks like this:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
password: c2VjcmV0cGFzc3dvcmQ= # base64, NOT encryption
echo c2VjcmV0cGFzc3dvcmQ= | base64 -d reveals secretpassword instantly. We need the
value in Git to be genuinely encrypted, and only decryptable inside the cluster.
The common approaches
There are four popular ways to do GitOps-friendly secrets:
- Sealed Secrets (Bitnami) — encrypt a Secret into a
SealedSecretcustom resource that only the in-cluster controller can decrypt. The encrypted form is safe to commit. - External Secrets Operator (ESO) — keep the actual secret in a manager like AWS
Secrets Manager, GCP Secret Manager, or Vault, and commit only a reference
(
ExternalSecret) that syncs the value into a real Secret. - SOPS + KSOPS — encrypt values with Mozilla SOPS (backed by KMS/age/PGP) and let a Kustomize plugin decrypt them during the ArgoCD render.
- HashiCorp Vault — inject secrets at runtime with the Vault Agent or the Vault Secrets Operator.
We’ll set up Sealed Secrets because it’s the simplest to reason about and fits the GitOps flow perfectly. Then we’ll show what ESO looks like for teams already using a cloud secret manager.
Sealed Secrets
The model is a public/private key pair. The controller running in the cluster holds the
private key; you encrypt with the public key. Because only the controller can decrypt, a
SealedSecret is safe to store in a public repo.
Install the controller (itself managed by ArgoCD, of course):
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
Install the kubeseal CLI locally, then encrypt a normal Secret into a SealedSecret:
# Create a Secret locally (do NOT commit this file)
kubectl create secret generic db-credentials \
--from-literal=password=secretpassword \
--dry-run=client -o yaml > secret.yaml
# Encrypt it using the controller's public key
kubeseal --controller-name sealed-secrets \
--controller-namespace kube-system \
--format yaml < secret.yaml > sealed-secret.yaml
The resulting sealed-secret.yaml contains ciphertext instead of the password:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: default
spec:
encryptedData:
password: AgBy3i4OJSWK+PiTySYZZA9rO... # encrypted, safe to commit
template:
metadata:
name: db-credentials
type: Opaque
This is what you commit. When ArgoCD applies the SealedSecret, the controller
notices it, decrypts it, and creates a matching real Secret in the cluster. Your app
consumes the Secret exactly as before — it never knows the difference.
External Secrets Operator
If your organization already stores secrets in AWS Secrets Manager, Vault, or similar, ESO is a great fit. You commit a reference, not the value.
Install ESO and define a SecretStore that knows how to talk to your provider:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
namespace: default
spec:
provider:
aws:
service: SecretsManager
region: us-west-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
Then commit an ExternalSecret that maps a remote secret into a Kubernetes Secret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: password
remoteRef:
key: prod/book-info/db
property: password
ESO periodically pulls the value from AWS Secrets Manager and keeps the in-cluster Secret in sync. Rotating the secret in AWS automatically flows into the cluster — no Git commit required.
Which one should I use?
- Sealed Secrets — simplest, self-contained, no external dependency. Great for small teams and homelabs. The trade-off: rotating a secret means re-sealing and committing.
- External Secrets Operator — best when you already have a central secret manager and want rotation handled outside Git. The trade-off: an extra operator and cloud dependency.
- SOPS/KSOPS — keeps the encrypted value in Git (like Sealed Secrets) but with KMS-backed keys and Kustomize integration.
Whatever you choose, the golden rule holds: plaintext secrets never touch Git. Only encrypted blobs or references do.
The example manifests are in 08-secret-management.
In the next post, we’ll stop managing Applications one by one and generate them in bulk with ApplicationSet.