Secure Kubernetes Secrets: Comparing Sealed Secrets, External Secrets Operator, and CSI Driver
This article explains why native Kubernetes Secrets are insufficiently protected, introduces three open‑source solutions—Sealed Secrets, External Secrets Operator, and Secrets Store CSI Driver—covers their architecture, installation steps, usage examples, advantages, drawbacks, and provides practical code snippets for managing secrets safely in Git‑backed clusters.
Kubernetes has become essential for modern software infrastructure, but its built‑in Secret objects store data only as base64‑encoded strings, which can be decoded by anyone with cluster or repository access. Proper secret management therefore requires encryption and careful RBAC configuration.
By default, Kubernetes Secrets are stored unencrypted in etcd; anyone with API or etcd access can read or modify them. – Kubernetes documentation
To address these risks, three open‑source approaches are examined: Sealed Secrets, External Secrets Operator (ESO), and Secrets Store CSI Driver.
Sealed Secrets
Sealed Secrets is a Bitnami‑maintained controller and CLI that encrypts secrets with asymmetric cryptography, allowing the encrypted SealedSecret CRD to be safely committed to Git. The controller decrypts the secret inside the cluster using a private key that never leaves the cluster.
Workflow:
Use the kubeseal CLI on a developer machine to encrypt a regular Secret, producing a SealedSecret CRD.
Deploy the CRD to the target cluster.
The Sealed Secrets controller uses the cluster’s private key to decrypt the sealed secret and creates a standard Kubernetes Secret.
Pros
Supports templating to add metadata (labels, annotations) to unencrypted secrets.
Encrypted secret CRD owns the underlying secret and updates it automatically.
Certificates rotate every 30 days by default and are configurable.
Encryption uses a unique key per cluster/namespace/secret combination; scopes can be strict, namespace‑wide, or cluster‑wide.
Can manage existing secrets in a cluster.
Has a VSCode extension for easier use.
Cons
If you have cluster or namespace access, you can still decode the secret after decryption.
Each cluster requires its own key pair, so secrets must be re‑encrypted per environment.
Installation
Download controller.yaml from the release page.
Run kubectl apply -f controller.yaml to install the controller in the kube-system namespace.
Install the CLI with brew install kubeseal or download it manually.
Usage Example
Create a regular secret and pipe it to kubeseal:
echo -n secretvalue | kubectl create secret generic mysecret \
--dry-run=client \
--from-file=foo=/dev/stdin -o yaml > my-secret.yamlSeal the secret:
kubeseal --format yaml < my-secret.yaml > my-sealed-secret.yamlApply the sealed secret to the cluster: kubectl apply -f my-sealed-secret.yaml Now the secret can be used like any other Kubernetes Secret.
External Secrets Operator (ESO)
ESO synchronizes Kubernetes Secrets with external secret stores such as HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, or Azure Key Vault. It defines four CRDs: ExternalSecret, ClusterExternalSecret, SecretStore, and ClusterSecretStore.
Workflow:
Create a SecretStore CRD that describes the connection to the external secret manager.
Store the actual secret in the external system.
Create an ExternalSecret CRD that references the SecretStore and specifies which external data to fetch.
Deploy the CRDs; the ESO controller pulls the data and creates a regular Kubernetes Secret.
Pros
Secrets remain in a secure external manager, not in code repositories.
Automatic synchronization and rotation support.
Works with many external providers.
Multiple secret stores can be used in a single cluster.
Provides Prometheus metrics for monitoring.
Cons
Initial setup is more involved.
If you have cluster access, you can still read the resulting Kubernetes Secret.
Security depends on the external manager’s access policies.
Installation (Helm)
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace
# Add --set installCRDs=true if you want Helm to install the CRDs.Example with HashiCorp Vault
Create a SecretStore that points to Vault:
# vault-backend.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: 'YOUR_VAULT_ADDRESS'
path: 'secret'
version: 'v2'
namespace: 'admin'
auth:
tokenSecretRef:
name: 'vault-token'
key: 'token'Create a token secret in Kubernetes:
kubectl create secret generic vault-token \
--dry-run=client \
--from-literal=token=YOUR_VAULT_TOKEN -o yaml | kubectl apply -f -Store a secret in Vault:
vault kv put secret/mysecret my-value=supersecretDefine an ExternalSecret that pulls the value:
# vault-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: vault-example
spec:
refreshInterval: '15s'
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: vault-example-sync
data:
- secretKey: secret-from-vault
remoteRef:
key: secret/mysecret
property: my-valueApply the resources:
kubectl apply -f vault-backend.yaml
kubectl apply -f vault-secret.yamlSecrets Store CSI Driver
The CSI driver enables pods to mount secrets directly from external providers as volumes, avoiding exposure as Kubernetes Secrets. It works with Vault, AWS, Azure, and GCP providers.
Workflow:
Create a SecretProviderClass CRD that describes which external secret to fetch.
Reference the SecretProviderClass in a pod’s volume spec.
The driver retrieves the secret from the external provider and mounts it into the pod as a tmpfs volume (or optionally creates a regular Secret).
Pros
Secrets stay in external managers, not in the repository.
Automatic synchronization and rotation.
Supports all major external providers.
Mounts secrets as files, preventing them from appearing as Kubernetes Secrets.
Cons
More complex to set up than ESO.
Consumes additional resources because a daemonset runs on every node.
Relies on the external provider’s access controls.
Google Secret Manager (GSM) Example
Prerequisites: a GKE cluster with Workload Identity enabled and the Secret Manager API activated.
# Create a workload‑identity service account
gcloud iam service-accounts create gke‑workload
# Bind the K8s service account to the GCP service account
gcloud iam service-accounts add-iam-policy-binding \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:$PROJECT_ID.svc.id.goog[default/mypodserviceaccount]" \
gke‑workload@$PROJECT_ID.iam.gserviceaccount.comCreate a secret in GSM and grant the service account access:
# Create secret
echo "mysupersecret" > secret.data
gcloud secrets create testsecret --replication-policy=automatic --data-file=secret.data
rm secret.data
# Grant access
gcloud secrets add-iam-policy-binding testsecret \
--member=serviceAccount:gke‑workload@$PROJECT_ID.iam.gserviceaccount.com \
--role=roles/secretmanager.secretAccessorDefine a SecretProviderClass that points to the GSM secret:
# secret-provider-class.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: app-secrets
spec:
provider: gcp
parameters:
secrets: |
- resourceName: "projects/$PROJECT_ID/secrets/testsecret/versions/latest"
path: "good1.txt"
- resourceName: "projects/$PROJECT_ID/secrets/testsecret/versions/latest"
path: "good2.txt"Create a pod that mounts the secret files:
# my-pod.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: mypodserviceaccount
namespace: default
annotations:
iam.gke.io/gcp-service-account: gke‑workload@$PROJECT_ID.iam.gserviceaccount.com
---
apiVersion: v1
kind: Pod
metadata:
name: mypod
namespace: default
spec:
serviceAccountName: mypodserviceaccount
containers:
- name: mypod
image: gcr.io/google.com/cloudsdktool/cloud-sdk:slim
stdin: true
stdinOnce: true
tty: true
volumeMounts:
- name: mysecret
mountPath: '/var/secrets'
volumes:
- name: mysecret
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: 'app-secrets'Apply the resources and verify the mounted files:
kubectl apply -f secret-provider-class.yaml
kubectl apply -f my-pod.yaml
kubectl exec -it mypod -- cat /var/secrets/good1.txtConclusion
Sealed Secretsis an excellent solution for small teams that need to store encrypted secrets in Git. Larger teams may prefer External Secrets Operator or Secrets Store CSI Driver for tighter integration with external secret managers. All three approaches should be combined with proper RBAC policies to prevent non‑admin access to secrets.
System Architect Go
Programming, architecture, application development, message queues, middleware, databases, containerization, big data, image processing, machine learning, AI, personal growth.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
