πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Security advanced ⏱ 40 minutes K8s 1.28+

Automated Secret Rotation on Kubernetes

Implement zero-downtime secret rotation with External Secrets Operator, HashiCorp Vault dynamic secrets, and rolling restarts for enterprise compliance.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Use External Secrets Operator to sync secrets from Vault/AWS Secrets Manager with refreshInterval: 1h. Vault dynamic secrets auto-expire. Combine with Reloader or stakater to trigger rolling restarts when secrets change β€” zero-downtime rotation.

The Problem

Enterprise compliance (SOC2, PCI-DSS) requires regular secret rotation β€” typically every 90 days for service credentials and 24 hours for database passwords. Manual rotation is error-prone and causes outages when pods use stale credentials. You need automated rotation that updates secrets and restarts affected workloads without downtime.

flowchart LR
    VAULT["HashiCorp Vault<br/>(Secret Source)"] -->|"Sync every 1h"| ESO["External Secrets<br/>Operator"]
    ESO -->|"Update K8s Secret"| SECRET["Kubernetes Secret"]
    SECRET -->|"Detect change"| RELOADER["Stakater Reloader"]
    RELOADER -->|"Rolling restart"| DEPLOY["Deployment<br/>(picks up new secret)"]
    DEPLOY -->|"Zero downtime"| PODS["New Pods<br/>(fresh credentials)"]

The Solution

External Secrets Operator with Vault

# 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
# ClusterSecretStore pointing to Vault
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: vault-backend
spec:
  provider:
    vault:
      server: "https://vault.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "external-secrets"
          serviceAccountRef:
            name: external-secrets
            namespace: external-secrets
---
# ExternalSecret with automatic refresh
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: production
spec:
  refreshInterval: 1h  # Check for updates every hour
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: database-credentials
    creationPolicy: Owner
    template:
      type: Opaque
      data:
        DB_HOST: "{{ .host }}"
        DB_USER: "{{ .username }}"
        DB_PASS: "{{ .password }}"
        DB_CONNECTION_STRING: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:5432/appdb"
  data:
    - secretKey: host
      remoteRef:
        key: production/database
        property: host
    - secretKey: username
      remoteRef:
        key: production/database
        property: username
    - secretKey: password
      remoteRef:
        key: production/database
        property: password

Vault Dynamic Database Secrets

# Configure Vault database engine (one-time)
vault secrets enable database

vault write database/config/postgres \
  plugin_name=postgresql-database-plugin \
  connection_url="postgresql://{{username}}:{{password}}@postgres.example.com:5432/appdb" \
  allowed_roles="app-readonly,app-readwrite" \
  username="vault-admin" \
  password="vault-admin-password"

# Create role with 24h TTL (auto-rotation)
vault write database/roles/app-readwrite \
  db_name=postgres \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  default_ttl="24h" \
  max_ttl="48h"
# ExternalSecret using Vault dynamic secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-dynamic-creds
  namespace: production
spec:
  refreshInterval: 12h  # Refresh before 24h TTL expires
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-dynamic-creds
    creationPolicy: Owner
  dataFrom:
    - extract:
        key: database/creds/app-readwrite

Auto-Restart on Secret Change (Stakater Reloader)

# Install Reloader
helm repo add stakater https://stakater.github.io/stakater-charts
helm install reloader stakater/reloader -n kube-system
# Annotate deployment to auto-restart on secret change
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-gateway
  namespace: production
  annotations:
    reloader.stakater.com/auto: "true"  # Watch all referenced secrets/configmaps
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    spec:
      containers:
        - name: api
          image: registry.example.com/api-gateway:v2.5.0
          envFrom:
            - secretRef:
                name: database-credentials

Rotation Schedule

Secret TypeRotation PeriodMethodRestart Required
Database passwords24hVault dynamic secretsYes (Reloader)
API keys90 daysESO refresh from VaultYes (Reloader)
TLS certificates90 dayscert-manager auto-renewalNo (live reload)
OAuth client secrets90 daysESO refresh from VaultYes (Reloader)
Encryption keys365 daysManual rotation + envelope encryptionNo (dual-key period)

Common Issues

IssueCauseFix
Pods crash after rotationApplication doesn’t handle new credentialsUse connection pooling with reconnect logic
Vault lease expiredrefreshInterval > secret TTLSet refreshInterval to half the TTL
Reloader triggers too many restartsMultiple secrets changing at onceUse reloader.stakater.com/search: "true" with specific annotations
Old pods still using old credentialsRolling update not completeEnsure PDB allows at least 1 pod restart

Best Practices

  • Dynamic secrets over static β€” Vault dynamic secrets auto-expire; no rotation needed
  • Refresh before expiry β€” set refreshInterval to half the secret TTL
  • Rolling restarts β€” Reloader + RollingUpdate strategy ensures zero-downtime rotation
  • Dual-key period β€” for encryption keys, support both old and new keys during transition
  • Audit secret access β€” enable Vault audit log + K8s audit logging for compliance
  • Test rotation in staging β€” verify applications handle credential changes gracefully

Key Takeaways

  • External Secrets Operator syncs secrets from Vault/cloud providers with automatic refresh
  • Vault dynamic secrets provide time-limited credentials that auto-expire (no manual rotation)
  • Stakater Reloader triggers rolling restarts when referenced secrets change
  • Combine all three for fully automated, zero-downtime secret rotation
  • Set refresh intervals to half the secret TTL to ensure rotation happens before expiry
#secret-rotation #vault #external-secrets #compliance #zero-downtime
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens