Cloud Native 20 min read

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.

System Architect Go
System Architect Go
System Architect Go
Secure Kubernetes Secrets: Comparing Sealed Secrets, External Secrets Operator, and CSI Driver

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.yaml

Seal the secret:

kubeseal --format yaml < my-secret.yaml > my-sealed-secret.yaml

Apply 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=supersecret

Define 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-value

Apply the resources:

kubectl apply -f vault-backend.yaml
kubectl apply -f vault-secret.yaml

Secrets 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.com

Create 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.secretAccessor

Define 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.txt

Conclusion

Sealed Secrets

is 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.

cloud nativeKubernetesSecrets ManagementSealed SecretsCSI DriverExternal Secrets Operator
System Architect Go
Written by

System Architect Go

Programming, architecture, application development, message queues, middleware, databases, containerization, big data, image processing, machine learning, AI, personal growth.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.