Introduction
GitOps promises a single source of truth: everything in Git, everything versioned, everything auditable. But there’s an obvious problem—you can’t commit secrets to Git. Database passwords, API keys, TLS certificates—these need to exist in your cluster, but they can’t live in your repository in plaintext.
This tension has spawned an entire category of tools designed to bridge the gap between GitOps principles and secret management reality. Two approaches have emerged as the dominant solutions in the Kubernetes ecosystem: Sealed Secrets and the External Secrets Operator (ESO).
This article compares both approaches, explains when to use each, and provides practical implementation guidance for teams adopting GitOps in 2026.
The GitOps Secrets Problem
In a traditional deployment model, secrets are injected at deploy time—CI/CD pipelines pull from Vault, inject into Kubernetes, done. But GitOps inverts this model: the cluster pulls its desired state from Git. If secrets aren’t in Git, how does the cluster know what secrets to create?
Three fundamental approaches have emerged:
- Encrypt secrets in Git: Store encrypted secrets in the repository; decrypt them in-cluster (Sealed Secrets, SOPS)
- Reference external stores: Store pointers to secrets in Git; fetch actual values from external systems at runtime (External Secrets Operator)
- Hybrid approaches: Combine encryption with external references for different use cases
Sealed Secrets: Encryption at Rest in Git
Sealed Secrets, created by Bitnami, uses asymmetric encryption to allow secrets to be safely committed to Git.
How It Works
┌─────────────────────────────────────────────────────────────┐
│ SEALED SECRETS FLOW │
│ │
│ Developer Git Repo Kubernetes │
│ │ │ │ │
│ │ kubeseal │ │ │
│ │ ──────────► │ │ │
│ │ (encrypt) │ SealedSecret │ │
│ │ │ ───────────────► │ │
│ │ │ (GitOps sync) │ │
│ │ │ │ Controller │
│ │ │ │ decrypts │
│ │ │ │ ──────────► │
│ │ │ │ Secret │
└─────────────────────────────────────────────────────────────┘
- A controller runs in your cluster, generating a public/private key pair
- Developers use
kubesealCLI to encrypt secrets with the cluster’s public key - The encrypted
SealedSecretresource is committed to Git - Argo CD or Flux syncs the SealedSecret to the cluster
- The Sealed Secrets controller decrypts it, creating a standard Kubernetes Secret
Installation
# Install the controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Install kubeseal CLI
brew install kubeseal # macOS
# or download from GitHub releases
Creating a Sealed Secret
# Create a regular secret (don't commit this!)
kubectl create secret generic db-creds --from-literal=username=admin --from-literal=password=supersecret --dry-run=client -o yaml > secret.yaml
# Seal it (this is safe to commit)
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# The output looks like:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-creds
namespace: default
spec:
encryptedData:
username: AgBy8hCi8... # encrypted
password: AgCtr9dk3... # encrypted
Pros and Cons
Advantages:
- Simple mental model: „encrypt, commit, done“
- No external dependencies at runtime
- Works offline—no network calls to external systems
- Secrets are genuinely in Git (encrypted), enabling full GitOps audit trail
- Lightweight controller with minimal resource usage
Disadvantages:
- Cluster-specific encryption: secrets must be re-sealed for each cluster
- Key rotation is manual and requires re-sealing all secrets
- No automatic secret rotation from external sources
- Single point of failure: lose the private key, lose all secrets
- Doesn’t integrate with existing enterprise secret stores (Vault, AWS Secrets Manager)
External Secrets Operator: References to External Stores
The External Secrets Operator (ESO) takes a different approach: instead of encrypting secrets, it stores references to secrets in Git. The actual secret values live in external secret management systems.
How It Works
┌─────────────────────────────────────────────────────────────┐
│ EXTERNAL SECRETS OPERATOR FLOW │
│ │
│ Git Repo Kubernetes Secret Store │
│ │ │ │ │
│ ExternalSecret │ │ │
│ (reference) │ │ │
│ │ ────────────────► │ │ │
│ │ (GitOps sync) │ ESO Controller │ │
│ │ │ ────────────────► │ │
│ │ │ (fetch secret) │ │
│ │ │ ◄──────────────── │ │
│ │ │ (secret value) │ │
│ │ │ │ │
│ │ │ Creates K8s │ │
│ │ │ Secret │ │
└─────────────────────────────────────────────────────────────┘
- You define an
ExternalSecretresource that references a secret in an external store - The ExternalSecret is committed to Git and synced to the cluster
- ESO’s controller fetches the actual secret value from the external store
- ESO creates a standard Kubernetes Secret with the fetched values
- ESO periodically refreshes the secret, enabling automatic rotation
Supported Providers (20+)
ESO supports a vast ecosystem of secret stores:
- HashiCorp Vault (KV, PKI, database secrets engines)
- AWS Secrets Manager and Parameter Store
- Azure Key Vault
- Google Cloud Secret Manager
- 1Password, Doppler, Infisical
- CyberArk, Akeyless
- And many more…
Installation
# Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
Configuration Example: AWS Secrets Manager
# 1. Create a SecretStore (cluster-wide) or ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: eu-central-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
---
# 2. Create an ExternalSecret that references AWS
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h # Auto-refresh every hour
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-credentials # Name of the K8s Secret to create
data:
- secretKey: username
remoteRef:
key: production/database
property: username
- secretKey: password
remoteRef:
key: production/database
property: password
Pros and Cons
Advantages:
- Integrates with enterprise secret management (Vault, cloud providers)
- Automatic secret rotation—just update the source, ESO syncs
- Centralized secret management across multiple clusters
- No secrets in Git at all—not even encrypted
- Supports 20+ providers out of the box
- CNCF project with active community
Disadvantages:
- Runtime dependency on external secret store
- More complex setup (authentication to external providers)
- If the secret store is down, new secrets can’t be created
- Audit trail split between Git (references) and secret store (values)
- Higher resource usage than Sealed Secrets
SOPS: A Third Approach
SOPS (Secrets OPerationS) by Mozilla deserves mention as a popular alternative. Like Sealed Secrets, it encrypts secrets for storage in Git—but with key differences:
- Encrypts only the values in YAML/JSON, leaving keys readable
- Supports multiple key management systems (AWS KMS, GCP KMS, Azure Key Vault, PGP, age)
- Not Kubernetes-specific—works with any configuration files
- Integrates with Argo CD and Flux via plugins
# SOPS-encrypted secret (keys visible, values encrypted)
apiVersion: v1
kind: Secret
metadata:
name: db-creds
stringData:
username: ENC[AES256_GCM,data:admin,iv:...,tag:...]
password: ENC[AES256_GCM,data:supersecret,iv:...,tag:...]
sops:
kms:
- arn: arn:aws:kms:eu-central-1:123456789:key/abc-123
Decision Framework: Which Should You Use?
| Factor | Sealed Secrets | External Secrets Operator | SOPS |
|---|---|---|---|
| Existing Vault/Cloud KMS | ❌ Not integrated | ✅ Native support | ⚠️ For encryption only |
| Multi-cluster | ❌ Re-seal per cluster | ✅ Centralized store | ⚠️ Shared keys needed |
| Secret rotation | ❌ Manual | ✅ Automatic | ❌ Manual |
| Offline/air-gapped | ✅ Works offline | ❌ Needs connectivity | ✅ Works offline |
| Complexity | Low | Medium-High | Medium |
| Secrets in Git | Encrypted | References only | Encrypted |
| Enterprise compliance | ⚠️ Limited audit | ✅ Full audit trail | ⚠️ Depends on KMS |
Use Sealed Secrets When:
- You’re a small team without enterprise secret management
- You have a single cluster or few clusters
- You need simplicity over features
- Air-gapped or offline environments
Use External Secrets Operator When:
- You already use Vault, AWS Secrets Manager, or similar
- You need automatic secret rotation
- You manage multiple clusters
- Compliance requires centralized secret management
- You want zero secrets in Git (even encrypted)
Use SOPS When:
- You need to encrypt non-Kubernetes configs too
- You want cloud KMS without full ESO complexity
- You prefer visible structure with encrypted values
GitOps Integration: Argo CD and Flux
Argo CD with Sealed Secrets
Sealed Secrets work natively with Argo CD—just commit SealedSecrets to your repo:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
source:
repoURL: https://github.com/myorg/my-app
path: k8s/
# SealedSecrets in k8s/ are synced and decrypted automatically
Argo CD with External Secrets Operator
ESO also works seamlessly—ExternalSecrets are synced, and ESO creates the actual Secrets:
# In your Git repo
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: app-secrets
dataFrom:
- extract:
key: secret/data/my-app
Flux with SOPS
Flux has native SOPS support via the Kustomization resource:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
spec:
decryption:
provider: sops
secretRef:
name: sops-age # Key stored as K8s secret
Best Practices for 2026
- Never commit plaintext secrets. This seems obvious, but git history is forever. Use pre-commit hooks to catch accidents.
- Rotate secrets regularly. ESO makes this easy; Sealed Secrets requires re-sealing. Automate either way.
- Use namespaced secrets. Don’t create cluster-wide secrets unless absolutely necessary. Principle of least privilege applies.
- Monitor secret access. Enable audit logging in your secret store. Know who accessed what, when.
- Plan for key rotation. Sealed Secrets keys, SOPS keys, ESO service account credentials—all need rotation procedures.
- Test secret recovery. Can you recover if you lose access to your secret store? Document and test disaster recovery.
- Consider secret sprawl. As you scale, centralized management (ESO + Vault) becomes more valuable than per-cluster approaches.
Conclusion
GitOps and secrets management are fundamentally at tension—Git wants everything versioned and public within the org; secrets want to be hidden and ephemeral. Both Sealed Secrets and External Secrets Operator resolve this tension, but in different ways.
Sealed Secrets embraces encryption: secrets live in Git, but only the cluster can read them. External Secrets Operator embraces indirection: Git contains references, and runtime systems fetch the actual values.
For most organizations in 2026, External Secrets Operator is the strategic choice. It integrates with enterprise secret management, enables automatic rotation, and scales across clusters. But Sealed Secrets remains valuable for simpler deployments, air-gapped environments, and teams just starting their GitOps journey.
The worst choice? No choice at all—plaintext secrets in Git, or manual secret creation that bypasses GitOps entirely. Pick an approach, implement it consistently, and your GitOps practice will be both secure and auditable.
