The Problem: Secrets in Git
GitOps means everything in Git. But secrets canβt be stored in plaintext. Three solutions dominate:
| Solution | Encryption | Key Management | GitOps Native |
|---|---|---|---|
| Sealed Secrets | Asymmetric (RSA) | In-cluster controller | β |
| SOPS | Symmetric (age/KMS) | External (AWS KMS, GCP, age) | β οΈ (needs decrypt step) |
| External Secrets | None (reference only) | External vault | β |
1. Bitnami Sealed Secrets
Encrypts secrets client-side; only the in-cluster controller can decrypt.
# Install controller
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Encrypt a secret
kubectl create secret generic db-creds \
--from-literal=password=super-secret \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-db-creds.yaml# This is safe to commit to Git!
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-creds
spec:
encryptedData:
password: AgBy3i4OJSWK+PiTySYZZA9rO... # RSA-encryptedPros/Cons
| β Pros | β Cons |
|---|---|
| Simple workflow | Secrets re-encrypted per cluster |
| No external dependencies | Key rotation requires re-seal all |
| Native K8s Secret output | Canβt use outside Kubernetes |
| Commit encrypted secrets to Git | Losing the private key = lost secrets |
2. Mozilla SOPS
Encrypts YAML/JSON values in-place using age, AWS KMS, GCP KMS, or Azure Key Vault.
# Create age key
age-keygen -o key.txt
# Encrypt
sops --encrypt --age age1... secret.yaml > secret.enc.yaml
# Decrypt (CI/CD or Flux)
sops --decrypt secret.enc.yaml | kubectl apply -f -# Encrypted file β keys visible, values encrypted
apiVersion: v1
kind: Secret
metadata:
name: db-creds
data:
password: ENC[AES256_GCM,data:kE3l...,type:str]
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----With Flux CD
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
spec:
decryption:
provider: sops
secretRef:
name: sops-age # Age private key stored as K8s secretPros/Cons
| β Pros | β Cons |
|---|---|
| Works anywhere (not K8s-specific) | Extra tooling in CI/CD |
| Multiple key recipients | Need SOPS binary everywhere |
| AWS/GCP KMS integration | More complex key management |
| Diff-friendly (keys visible) | Not native to Argo CD (plugin needed) |
3. External Secrets Operator
Doesnβt store secrets in Git at all β just references.
# Only the reference is in Git (no secret data)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-creds
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
data:
- secretKey: password
remoteRef:
key: production/database
property: passwordPros/Cons
| β Pros | β Cons |
|---|---|
| No secrets in Git (even encrypted) | Requires external vault |
| Auto-rotation | Runtime dependency on vault |
| Audit trail in vault | More infrastructure to manage |
| Works with any vault | Cluster needs vault access |
Decision Matrix
| Requirement | Sealed Secrets | SOPS | External Secrets |
|---|---|---|---|
| Small team, simple setup | β Best | β οΈ | β Overkill |
| Multi-cloud/multi-cluster | β | β | β |
| Compliance (secret never in Git) | β | β | β Best |
| Existing HashiCorp Vault | β | β οΈ | β Best |
| Air-gapped environments | β | β | β |
| Non-Kubernetes workloads | β | β | β |
My Recommendation
For most organizations:
- Start with Sealed Secrets β lowest barrier to entry
- Graduate to External Secrets β when you have a vault and need rotation
- Use SOPS β for non-Kubernetes config or multi-tool environments