Skip to main content
πŸš€ Claude Code Bootcamp β€” May 30 5 hours from prompting to production. Build 10 real-world projects with AI-assisted development. Register Now
Kubernetes secrets management best practices 2026
Platform Engineering

Kubernetes Secrets Management Best Practices 2026: From etcd to External Vaults

Production-grade secrets management for Kubernetes: encryption at rest, external secret operators, sealed secrets, CSI drivers, rotation strategies, and zero-trust patterns.

LB
Luca Berton
Β· 2 min read

The Problem with Native Kubernetes Secrets

Kubernetes Secrets are base64-encoded, not encrypted. Anyone with RBAC access to read Secrets in a namespace can decode them instantly:

kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
# mysuperpassword123

By default, Secrets are stored unencrypted in etcd. This means:

  • etcd backups contain plaintext credentials
  • Any etcd compromise exposes everything
  • GitOps workflows cannot safely store Secret manifests in git

Level 1: Encryption at Rest

The minimum baseline β€” encrypt Secrets in etcd:

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}

Enable on the API server:

# kube-apiserver manifest
spec:
  containers:
    - command:
        - kube-apiserver
        - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml

Re-encrypt existing Secrets:

kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Limitation: The encryption key itself must be managed somewhere. If it is on the control plane disk, a node compromise still exposes everything.

Level 2: External Secrets Operator

Pull secrets from external vaults at runtime instead of storing them in Kubernetes:

helm install external-secrets external-secrets/external-secrets \
  -n external-secrets --create-namespace

AWS Secrets Manager Example

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/database
        property: username
    - secretKey: password
      remoteRef:
        key: prod/database
        property: password

HashiCorp Vault Example

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-store
spec:
  provider:
    vault:
      server: "https://vault.internal:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "my-app"
          serviceAccountRef:
            name: my-app-sa
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: api-keys
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: vault-store
    kind: SecretStore
  target:
    name: api-keys
  data:
    - secretKey: stripe-key
      remoteRef:
        key: secret/data/payments
        property: stripe_api_key

Level 3: Secrets Store CSI Driver

Mount secrets directly as files without creating Kubernetes Secret objects:

helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
  -n kube-system
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-secrets
spec:
  provider: vault
  parameters:
    vaultAddress: "https://vault.internal:8200"
    roleName: "my-app"
    objects: |
      - objectName: "db-password"
        secretPath: "secret/data/database"
        secretKey: "password"
---
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
    - name: app
      image: my-app:latest
      volumeMounts:
        - name: secrets
          mountPath: "/mnt/secrets"
          readOnly: true
  volumes:
    - name: secrets
      csi:
        driver: secrets-store.csi.x-k8s.io
        readOnly: true
        volumeAttributes:
          secretProviderClass: vault-secrets

The secret is never stored in etcd β€” it goes directly from Vault to the pod filesystem.

Level 4: Sealed Secrets for GitOps

When you need Secret manifests in git (ArgoCD, Flux):

# Install Sealed Secrets controller
helm install sealed-secrets sealed-secrets/sealed-secrets \
  -n kube-system

# Encrypt a secret
kubectl create secret generic db-creds \
  --from-literal=password=supersecret \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-db-creds.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-creds
spec:
  encryptedData:
    password: AgBY7... # Only the cluster can decrypt this

This is safe to commit to git. Only the cluster’s private key can unseal it.

Secret Rotation Strategy

Automatic Rotation with External Secrets

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: rotating-secret
spec:
  refreshInterval: 5m  # Check for updates every 5 minutes
  secretStoreRef:
    name: vault-store
    kind: SecretStore
  target:
    name: rotating-secret
    creationPolicy: Owner
    template:
      metadata:
        annotations:
          secret-rotated: "{{ .creationTimestamp }}"
  data:
    - secretKey: api-key
      remoteRef:
        key: secret/data/api
        property: key

Triggering Pod Restart on Secret Change

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  annotations:
    secret.reloader.stakater.com/reload: "db-credentials,api-keys"
spec:
  template:
    spec:
      containers:
        - name: app
          envFrom:
            - secretRef:
                name: db-credentials

Use Reloader to auto-restart pods when secrets change.

RBAC Hardening

Restrict who can read Secrets:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-secret-reader
  namespace: production
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    resourceNames: ["app-config"]  # Only specific secrets
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-reads-own-secret
  namespace: production
subjects:
  - kind: ServiceAccount
    name: my-app-sa
    namespace: production
roleRef:
  kind: Role
  name: app-secret-reader
  apiGroup: rbac.authorization.k8s.io

Audit secret access:

# Enable audit logging for secret reads
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]

Comparison: Which Approach When?

ApproachGitOps SafeNo etcd StorageAuto-RotationComplexity
Native + encryption at rest❌❌❌Low
Sealed Secretsβœ…βŒβŒMedium
External Secrets Operatorβœ…βŒ (creates K8s Secret)βœ…Medium
CSI DriverN/Aβœ…βœ…High
Vault Agent SidecarN/Aβœ…βœ…High

My Recommendation

For most production clusters in 2026:

  1. External Secrets Operator as the primary mechanism (works with any vault)
  2. Sealed Secrets for bootstrap secrets that must exist before the operator runs
  3. Encryption at rest as a defense-in-depth layer
  4. Reloader for automatic pod restarts on rotation
  5. RBAC + audit logging to track who accesses what

Start simple, add layers as your threat model requires.

Deepen Your Kubernetes Skills

If you found this article useful, check out my books for hands-on Kubernetes mastery:

Both books follow the same practical, example-driven approach you see in my articles.

Free 30-min AI & Cloud consultation

Book Now