Never commit raw secrets to Git: compare Sealed Secrets, the External Secrets Operator, and SOPS, then practice encryption, rotation, and RBAC for secrets.

In episode 11 we automated Application creation with ApplicationSet. All those manifests — Application, Deployment, ConfigMap — we dare to store in Git because they're safe to read publicly. But there's one type of resource that must not be: Secret. Database passwords, API tokens, TLS keys, cloud credentials — if stored raw in Git, one leaked repository means the whole infrastructure is exposed. In this episode we discuss how to store secrets safely in GitOps, from simple to enterprise.
Why does this matter? "Never commit secrets" is rule number one of GitOps, but in practice many teams break it because they don't know the alternatives. Yet the solutions are mature and varied: Sealed Secrets for encryption in Git, the External Secrets Operator for syncing from external storage, and SOPS for file encryption. Understanding when to use which is a mandatory skill for platform engineers.
GitOps demands everything lives in Git — but secrets are exactly what's most dangerous in Git. Three fundamental problems:
The basic principle: never store raw secrets, only encrypted representations or references to another source.
| Solution | How it works | Git stores | Secret source |
|---|---|---|---|
| Sealed Secrets | Asymmetric encryption, only the controller can decrypt | Encrypted SealedSecret | Git |
| External Secrets Operator | Sync from an external backend | ExternalSecret (reference) | Vault, cloud managers |
| SOPS | Encrypts YAML/JSON files with GPG/age | Encrypted file | Git + keys |
| Vault (native) | Template + direct access | None (or reference) | Vault |
The most popular approach to start with: the secret is encrypted before it enters Git, and only a controller inside the cluster holds the private key to decrypt it.
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets -n kube-system \
--set-string fullnameOverride=sealed-secrets-controller \
sealed-secrets/sealed-secrets
kubeseal --versionThe controller creates a key pair: the private key is stored as a Secret in the cluster (never leaves it), the public key is exported for the sealing process.
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: billing-prod
type: Opaque
stringData:
DB_PASSWORD: s3cr3t-rahasiakubeseal --format yaml < secret.yaml > sealed-secret.yaml
kubectl apply -f sealed-secret.yaml
cat sealed-secret.yamlThe resulting sealed-secret.yaml file is safe to commit to Git — its content is encrypted and can only be opened by the controller in the cluster holding the private key. The controller decrypts it and creates the real db-credentials Secret in the destination namespace.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: billing-secrets
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/devnull/billing-repo.git
path: secrets
destination:
server: https://kubernetes.default.svc
namespace: billing-prodTip
Because sealing needs the public key, export its certificate for use in CI: kubeseal --fetch-cert > pub-cert.pem. A CI pipeline that builds manifests can seal secrets from CI variables without cluster access. The private key never leaves the cluster.
In contrast, ESO doesn't store secrets in Git at all — Git only holds a reference called an ExternalSecret, and the operator syncs the actual values from external storage.
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets -n external-secrets \
external-secrets/external-secrets --create-namespaceSecretStore defines the connection to a backend — Vault, AWS Secrets Manager, GCP Secret Manager, and dozens of other providers:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: billing-prod
spec:
provider:
vault:
server: https://vault.example.com
path: secret/data/billing
auth:
kubernetes:
mountPath: kubernetes
role: billing-roleapiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: billing-prod
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
data:
- secretKey: DB_PASSWORD
remoteRef:
key: billing/db
property: passwordESO reads the value from Vault and creates the db-credentials Secret in the cluster. What gets committed to Git is only the SecretStore and ExternalSecret — no secret values. Bonus: rotation only happens in Vault, and ESO syncs automatically according to refreshInterval.
SOPS encrypts files (YAML, JSON, env) directly in Git using GPG or age keys:
sops -e -i secrets/values.prod.yaml
sops -d secrets/values.prod.yaml
git add secrets/ && git commit -m "chore: encrypt prod values"The encrypted file remains YAML so diffs between commits are still readable. Integration with ArgoCD is usually through a plugin (SOPS encrypt/decrypt on the repo-server) or by using kustomize + generators. SOPS is a good choice when the team wants values to stay "in Git" but secure.
For enterprise scale, secrets are placed in centralized storage: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault. The values never touch Git — Git only stores references, and ESO (or Vault's template engine) connects the two. This model gives a single source of truth for secrets and a clear audit trail.
Warning
All the solutions above protect secrets at rest, not at runtime. Make sure Secrets in the cluster remain RBAC-restricted, and avoid exposing secrets in logs or environment variables that can easily leak from the application.
target.name and the namespace match what the Deployment needs.refreshInterval never fires, the Secret in the cluster goes stale.This episode answered the classic GitOps dilemma: how to store secrets without leaking them into Git. We compared Sealed Secrets (encryption in Git), the External Secrets Operator (reference + sync from a backend), SOPS (file encryption), and Vault and cloud secret managers as a centralized source, closing with best practices and common pitfalls.
The points you should take with you:
Deployments are now safe and contain properly stored secrets. But there's one dimension we haven't controlled yet: the order and side effects during sync. In the next episode 13 we discuss Resource Hooks & Lifecycle: PreSync, Sync, PostSync, and SyncFail hooks for database migrations, smoke tests, and rollbacks — plus controlling execution order with sync waves. See you in episode 13!