On This Page
  1. Why Encrypt at Rest
  2. How etcd Encryption Works
  3. Encryption Providers
  4. KMS v2 Envelope Encryption
  5. EncryptionConfiguration
  6. Enabling Encryption
  7. Key Rotation
  8. Verifying Encryption
  9. etcd Disk-Level Encryption
  10. Node Storage Security
  11. Managed Kubernetes Encryption
  12. Metrics, Alerts & Runbooks
  13. Best Practices
Coverage Checklist

Why Encrypt at Rest

By default, all Kubernetes objects — including Secrets — are stored in etcd as plaintext (base64-encoded, which is encoding not encryption). Anyone with read access to the etcd data directory or etcd API can read every Secret in the cluster.

etcd Access = Full Cluster Compromise

etcd stores the entire cluster state. Direct access to the etcd data directory bypasses all Kubernetes AuthN, AuthZ, and admission controls. An attacker who can read etcd data can extract every Secret, ServiceAccount token, TLS certificate, and kubeconfig in the cluster without triggering a single audit log entry.

Attack Vectors That Encryption at Rest Mitigates

etcd Backup Theft

etcd snapshots are commonly stored in object storage. Without encryption at rest, a stolen backup exposes the entire cluster state including all Secrets.

Disk Forensics

Physical access to a decommissioned control plane node allows recovery of etcd data files. Encryption at rest makes this data unreadable without the key.

Cloud Provider Access

Cloud storage snapshots of the etcd EBS volume or GCP persistent disk could be accessed by a cloud provider employee or via a compromised cloud account.

Partial Mitigation Only

Encryption at rest does not protect against a live etcd API compromise (attacker has credentials to talk to etcd directly) — etcd always decrypts before returning data.

How etcd Encryption Works

Kubernetes API server encrypts resources before writing them to etcd, and decrypts them after reading. etcd itself has no knowledge of the encryption — it stores opaque encrypted bytes.

── Write path ────────────────────────────────────────────────────────

kubectl create secret


kube-apiserver
│ receives plaintext Secret object

├── Serialize to JSON/protobuf

├── Encrypt with configured provider
(secretbox / aesgcm / KMS envelope encryption)

└──▶ etcd
stores: k8s:enc:secretbox:v1:key1:<ciphertext>

── Read path ─────────────────────────────────────────────────────────

etcd
returns: k8s:enc:secretbox:v1:key1:<ciphertext>


kube-apiserver
├── Parse prefix to identify provider + key name
├── Decrypt ciphertext
└──▶ returns plaintext Secret to caller
Encryption Prefix Format

Encrypted values in etcd are prefixed with k8s:enc:<provider>:v1:<keyname>:. This prefix tells the API server which provider and key to use for decryption. The identity provider stores values without any prefix — they appear as raw protobuf.

What Gets Encrypted

The EncryptionConfiguration specifies which resource types to encrypt. You can encrypt any API resource, but these are the most security-relevant:

ResourceWhy EncryptPriority
secretsContains passwords, tokens, TLS keysCritical
configmapsMay contain environment-specific config with sensitive valuesHigh
serviceaccountsSA metadata (not tokens — those are JWTs and separate)Medium
podsPod specs can contain env var secrets, volume refsMedium
Custom resourcesCRDs that store credentials or sensitive configCase-by-case

Encryption Providers

ProviderAlgorithmKey ManagementPerformanceRecommended
identity None (plaintext) N/A Best Never for sensitive resources
secretbox XSalsa20 + Poly1305 Local key in config file Excellent Good for non-KMS setups
aesgcm AES-GCM (128 or 256-bit) Local key in config file Good (hardware AES-NI) Acceptable; rotate keys frequently
aescbc AES-CBC (256-bit) Local key in config file Good Legacy; use secretbox or aesgcm
kms (v1) Envelope: AES-CBC DEK + KMS KEK External KMS (AWS/GCP/Azure/Vault) Moderate (KMS call per write) Superseded by v2
kms (v2) Envelope: AES-GCM DEK + KMS KEK, DEK cached External KMS (AWS/GCP/Azure/Vault) Excellent (DEK cache) Recommended for production
Provider Order Matters: First = Write, All = Read

The first provider in the list is used to encrypt new writes. All listed providers are tried in order for decryption. During key rotation, you add the new key as first (for new writes) while keeping the old key second (to decrypt existing data). Once all data is re-encrypted, remove the old key.

identity Provider as Read Fallback

Including identity in the providers list allows reading unencrypted data that was written before encryption was enabled. Once all existing resources have been re-encrypted, remove identity from the list. Leaving it in permanently means unencrypted values can always be written if a misconfiguration occurs.

KMS v2 Envelope Encryption

KMS v2 (GA in Kubernetes 1.29) uses envelope encryption — a two-tier key hierarchy that keeps the KMS workload manageable while maintaining strong security properties.

── KMS v2 Envelope Encryption ────────────────────────────────────────

KMS (AWS/GCP/Azure/Vault)
│ holds: Key Encryption Key (KEK) ← never leaves KMS

├── On startup: generate Data Encryption Key (DEK)
│ DEK encrypted by KEK → EncryptedDEK stored in API server memory

├── On Secret write:
plaintext → AES-GCM encrypt with DEK → ciphertext
ciphertext + EncryptedDEK stored in etcd
(no KMS call needed — DEK is cached)

├── On Secret read:
read ciphertext + EncryptedDEK from etcd
if DEK cached: decrypt directly
if DEK not cached: call KMS to decrypt EncryptedDEK first

└── On API server restart:
DEK cache is lost → first read calls KMS to re-derive DEK

── vs KMS v1 ─────────────────────────────────────────────────────────
KMS v1: KMS call on EVERY write (severe latency at scale)
KMS v2: KMS call only on DEK generation (startup) + DEK expiry (every 1h default)

KMS Plugin Architecture

The API server communicates with the KMS via a local gRPC socket. The KMS plugin (a separate process) translates between the Kubernetes KMS gRPC protocol and the cloud KMS API.

# kube-apiserver communicates via Unix socket to KMS plugin
# KMS plugin process runs on the control plane node

──── kube-apiserver ────────────────────────────────────────────────────
#  │
#  │  gRPC over Unix socket: /var/run/kms-plugin/socket.sock
#  │  Protocol: k8s.io/kms/v2beta1
#  ▼
──── KMS plugin process ────────────────────────────────────────────────
#  │
#  │  Cloud SDK / Vault API
#  ▼
──── External KMS ──────────────────────────────────────────────────────
#  AWS KMS / GCP Cloud KMS / Azure Key Vault / HashiCorp Vault

KMS Plugin Examples

PlatformPluginKey Type
AWS EKS / self-managed on EC2aws-encryption-providerAWS KMS CMK (symmetric)
GKE / GCEkmsplugin (built into GKE) or cloud-kms-pluginGCP Cloud KMS CryptoKey
AKS / Azureazure-kms-pluginAzure Key Vault key
Anyvault-k8s-kms-pluginHashiCorp Vault Transit key
Anyk8s-kms-plugin (generic)Configurable backend

EncryptionConfiguration

Full Configuration with All Providers

# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration

resources:
# ── Secrets: highest priority, KMS v2 ────────────────────────────────
- resources:
  - secrets
  providers:
  # First provider = used for new writes
  - kms:
      apiVersion: v2            # KMS v2 (GA in 1.29)
      name: aws-kms-key-1
      endpoint: unix:///var/run/kms-plugin/socket.sock
      cachesize: 1000           # Max cached DEKs
      timeout: 3s
  # identity as last fallback allows reading pre-encryption data
  # REMOVE after all secrets re-encrypted
  - identity: {}

# ── ConfigMaps: secretbox (local key) ────────────────────────────────
- resources:
  - configmaps
  providers:
  - secretbox:
      keys:
      - name: key2
        secret: c2VjcmV0Ym94a2V5Zm9yY29uZmlnbWFwc3h5ejEyMzQ1Njc=
        # 32-byte base64-encoded random key
        # Generate: head -c 32 /dev/urandom | base64
  - identity: {}

# ── Everything else: identity (unencrypted) ──────────────────────────
# Add other resource types above as needed

secretbox Provider (Local Key)

# Generate a 32-byte base64-encoded key for secretbox
head -c 32 /dev/urandom | base64

# secretbox provider configuration
resources:
- resources:
  - secrets
  providers:
  - secretbox:
      keys:
      - name: key1          # Arbitrary name; appears in etcd prefix
        secret: <base64-32-bytes>
  - identity: {}           # Read fallback for pre-encryption data

aesgcm Provider

# aesgcm uses 16-byte (AES-128) or 32-byte (AES-256) keys
# Generate AES-256 key:
head -c 32 /dev/urandom | base64

providers:
- aesgcm:
    keys:
    - name: key1
      secret: <base64-32-bytes>
- identity: {}
aesgcm: Rotate Keys Every 200,000 Writes

AES-GCM uses a random 96-bit nonce. The probability of nonce collision becomes significant at ~2^32 writes with the same key (birthday bound). For AES-GCM, key rotation should happen after ~200,000 writes to a single key. secretbox uses a 192-bit nonce and does not have this constraint at practical scale.

KMS v2 Provider (Full)

providers:
- kms:
    apiVersion: v2
    name: my-kms-key            # Logical name (appears in etcd prefix)
    endpoint: unix:///var/run/kmsplugin/socket.sock
    cachesize: 1000             # Number of DEKs to cache (default 1000)
    timeout: 3s                 # gRPC call timeout (default 3s)
- identity: {}                  # Read fallback; remove after re-encryption

Enabling Encryption

Enabling encryption for an existing cluster requires a careful sequence to avoid downtime and data loss. The API server must be restarted, which is momentarily disruptive in single-API-server setups.

1

Create EncryptionConfiguration file on all control plane nodes

Write the config to /etc/kubernetes/encryption-config.yaml. Include identity as a read fallback provider. Restrict file permissions: chmod 600, owned by root.

2

Add --encryption-provider-config flag to API server

For kubeadm clusters: edit /etc/kubernetes/manifests/kube-apiserver.yaml. The kubelet will detect the change and restart the API server pod. Add the volume mount for the config file.

3

Verify API server restarts successfully

Watch kubectl get pods -n kube-system. Check API server logs for encryption provider initialization. Verify the cluster is healthy before proceeding.

4

Re-encrypt all existing resources

New writes go through the encryption provider, but existing data in etcd is still plaintext. Force re-encryption by reading and writing back all resources.

5

Remove identity fallback provider

Once all resources are re-encrypted, remove identity from the providers list and restart the API server. Now any plaintext read from etcd will fail — which is intentional.

# Step 2: kubeadm API server manifest additions
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
  containers:
  - command:
    - kube-apiserver
    # ... existing flags ...
    - --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
    volumeMounts:
    - name: encryption-config
      mountPath: /etc/kubernetes/encryption-config.yaml
      readOnly: true
  volumes:
  - name: encryption-config
    hostPath:
      path: /etc/kubernetes/encryption-config.yaml
      type: File
# Step 4: Re-encrypt all existing Secrets
# This reads each secret and writes it back — triggers encryption on write

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

# Re-encrypt ConfigMaps if configured
kubectl get configmaps --all-namespaces -o json | \
  kubectl replace -f -

# For large clusters, process namespace by namespace to avoid API server overload
for ns in $(kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'); do
  echo "Processing namespace: $ns"
  kubectl get secrets -n "$ns" -o json | kubectl replace -f - || true
  sleep 1
done

Key Rotation

Key rotation for local providers (secretbox, aesgcm) requires a precise sequence. KMS v2 key rotation is handled by the KMS provider — you rotate the KEK in AWS/GCP/Azure and the plugin transparently re-wraps DEKs.

Local Key Rotation (secretbox / aesgcm)

1

Generate new key

head -c 32 /dev/urandom | base64 — store securely.

2

Add new key as FIRST in providers list, old key second

New writes use the new key. Old key remains for decrypting existing data. Restart API server.

3

Re-encrypt all resources

Run kubectl get secrets --all-namespaces -o json | kubectl replace -f -. This re-encrypts all existing data with the new key.

4

Remove old key from providers list

After all data is re-encrypted with the new key, remove the old key entry. Restart API server. Securely delete the old key material.

# Phase 1: Add new key as first (new writes use new key)
providers:
- secretbox:
    keys:
    - name: key2            # NEW — first = used for writes
      secret: <new-key-base64>
    - name: key1            # OLD — kept for reading existing data
      secret: <old-key-base64>

# Phase 2: After re-encryption, remove old key
providers:
- secretbox:
    keys:
    - name: key2            # Only new key remains
      secret: <new-key-base64>

KMS v2 Key Rotation

# AWS KMS: rotate the CMK (creates new key material, old still valid for decrypt)
aws kms enable-key-rotation --key-id <key-id>

# GCP Cloud KMS: create a new key version
gcloud kms keys versions create \
  --key my-k8s-key \
  --keyring my-keyring \
  --location global

# Set new version as primary
gcloud kms keys versions get-public-key 1 \
  --key my-k8s-key --keyring my-keyring --location global

# After KMS rotation: force API server to re-derive DEK from new KEK
# Restart API server OR wait for DEK expiry (1h default)

# Re-encrypt all secrets so they use new DEK wrapped by new KEK
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

Verifying Encryption

After enabling encryption, verify that etcd actually stores encrypted data — not just that the API server accepted the configuration.

# Create a test secret
kubectl create secret generic encryption-test \
  --from-literal=test-key=test-value \
  -n default

# Read directly from etcd using etcdctl
# (must run on a control plane node with etcd certs)
ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/encryption-test

# Expected output if encrypted (secretbox):
# /registry/secrets/default/encryption-test
# k8s:enc:secretbox:v1:key1:<binary ciphertext>

# If you see plaintext JSON/protobuf: encryption is NOT working
# k8s:enc: prefix MUST be present
# Alternative: pipe through hexdump to inspect binary prefix
ETCDCTL_API=3 etcdctl \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  get /registry/secrets/default/encryption-test | hexdump -C | head -5

# Look for the "k8s:enc:" ASCII prefix in the hex output
# 6b 38 73 3a 65 6e 63 3a  = "k8s:enc:"
# Check API server logs for encryption provider initialization
kubectl logs -n kube-system kube-apiserver-<node-name> | \
  grep -i "encrypt\|kms\|provider"

# Expected log line on successful KMS v2 init:
# "KMS plugin initialized" or "encryption provider" messages

# Verify encryption config is loaded
kubectl get --raw /healthz/etcd

# Check KMS plugin health
kubectl get --raw /healthz/kms-providers

etcd Disk-Level Encryption

Kubernetes-level encryption (via EncryptionConfiguration) and disk-level encryption (LUKS/dm-crypt) are complementary, not mutually exclusive. Disk encryption protects against physical disk theft; Kubernetes encryption protects against etcd API compromise and backup theft.

etcd TLS Configuration

Regardless of encryption at rest, etcd communications must be TLS-encrypted in transit. This is separate from encryption at rest but equally important.

# etcd flags for mutual TLS (peer and client)

# Client-to-server TLS (kube-apiserver → etcd)
--cert-file=/etc/kubernetes/pki/etcd/server.crt
--key-file=/etc/kubernetes/pki/etcd/server.key
--trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
--client-cert-auth=true

# Peer TLS (etcd member ↔ etcd member)
--peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
--peer-key-file=/etc/kubernetes/pki/etcd/peer.key
--peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
--peer-client-cert-auth=true

LUKS Full Disk Encryption for etcd Data Directory

# Create LUKS encrypted volume for etcd data
# (Run before etcd is initialized)

# 1. Create LUKS container on device /dev/sdb
cryptsetup luksFormat --type luks2 \
  --cipher aes-xts-plain64 \
  --key-size 512 \
  --hash sha256 \
  --pbkdf argon2id \
  /dev/sdb

# 2. Open the LUKS volume
cryptsetup luksOpen /dev/sdb etcd-data

# 3. Format the plaintext device
mkfs.ext4 /dev/mapper/etcd-data

# 4. Mount as etcd data directory
mount /dev/mapper/etcd-data /var/lib/etcd

# 5. Add to /etc/crypttab for auto-open on boot
# etcd-data  /dev/sdb  /etc/etcd-luks.key  luks

# 6. Key stored in HSM or cloud KMS — NOT on the same disk
Cloud-Managed Disk Encryption

Managed Kubernetes services (EKS, GKE, AKS) automatically encrypt etcd data at the infrastructure level using the cloud provider's disk encryption. This provides disk-level encryption but may not provide Kubernetes-level secret encryption unless you additionally configure EncryptionConfiguration with a KMS provider.

Node Storage Security

Encryption at rest for etcd is only one layer. Data also lands on node storage through container filesystems, emptyDir volumes, and node-local resources.

emptyDir and tmpfs

# emptyDir defaults to node disk — data survives pod restarts on same node
# Use medium: Memory (tmpfs) for sensitive temporary data
spec:
  containers:
  - name: app
    volumeMounts:
    - name: scratch
      mountPath: /tmp/secrets
  volumes:
  - name: scratch
    emptyDir:
      medium: Memory   # tmpfs — data in RAM, never written to disk
      sizeLimit: 64Mi

Secret Volume Default Mode

# Secret volumes are projected to tmpfs on the node by default (since K8s 1.3)
# They do NOT write to the node's disk filesystem
# However, container images and writable layers ARE on disk

volumes:
- name: db-creds
  secret:
    secretName: db-credentials
    defaultMode: 0400   # Read-only for owner only (pod UID)

Container Image Layer Encryption (OCI)

OCI image encryption (ocicrypt) allows container image layers to be encrypted. This is distinct from Kubernetes-level encryption and is relatively uncommon — it protects image content at rest in the registry and on node disk.

Storage LocationDefault StateEncryption Mechanism
etcd SecretsPlaintext (base64)EncryptionConfiguration + KMS
etcd data directoryDepends on cloud/disk configLUKS / cloud disk encryption
Secret volume mounttmpfs (in-memory)N/A — RAM only, cleared on unmount
emptyDir volumeNode disk (unless medium:Memory)Node disk encryption or tmpfs
Container image layersPlaintext on nodeOCI image encryption (ocicrypt)
PersistentVolumeDepends on StorageClassStorage provider encryption (EBS, GCP PD)

Managed Kubernetes Encryption

Amazon EKS

# Enable Secrets encryption with AWS KMS CMK at cluster creation
aws eks create-cluster \
  --name my-cluster \
  --kubernetes-version 1.29 \
  --resources-vpc-config subnetIds=subnet-xxx,securityGroupIds=sg-xxx \
  --encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:us-east-1:123456789012:key/xxx"}}]'

# Enable on existing cluster
aws eks associate-encryption-config \
  --cluster-name my-cluster \
  --encryption-config '[{"resources":["secrets"],"provider":{"keyArn":"arn:aws:kms:..."}}]'

# Verify encryption config
aws eks describe-cluster --name my-cluster \
  --query 'cluster.encryptionConfig'

GKE (Google Kubernetes Engine)

# GKE uses Google-managed encryption by default (AES-256)
# For CMEK (Customer-Managed Encryption Keys):
gcloud container clusters create my-cluster \
  --database-encryption-key projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key

# GKE Application-layer secrets encryption (using Cloud KMS)
gcloud container clusters update my-cluster \
  --database-encryption-key projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key

AKS (Azure Kubernetes Service)

# AKS encrypts etcd at rest with Microsoft-managed keys by default
# For customer-managed keys (CMK):
az aks create \
  --resource-group myRG \
  --name myAKS \
  --enable-encryption-at-host

# Azure Disk Encryption for node OS/data disks
az aks update \
  --resource-group myRG \
  --name myAKS \
  --enable-disk-encryption

Metrics, Alerts & Runbooks

Key Metrics

MetricSourceDescription
apiserver_envelope_encryption_dek_cache_fill_percentkube-apiserverDEK cache fill ratio (KMS v2)
apiserver_storage_transformation_operations_totalkube-apiserverEncrypt/decrypt operations by provider and resource
apiserver_storage_transformation_duration_secondskube-apiserverLatency of encryption/decryption operations
etcd_disk_wal_fsync_duration_secondsetcdetcd write latency (elevated if storage is slow)
kms_plugin_rpc_duration_secondsKMS pluginLatency of KMS gRPC calls (high = KMS issues)

Alerts

# Alert: KMS plugin unhealthy
- alert: KMSPluginUnhealthy
  expr: up{job="kube-apiserver"} == 1 and absent(kms_plugin_rpc_duration_seconds)
  for: 5m
  severity: critical
  annotations:
    summary: "KMS plugin not reachable — new Secret writes will fail"

# Alert: High KMS call latency
- alert: KMSHighLatency
  expr: histogram_quantile(0.99, kms_plugin_rpc_duration_seconds_bucket) > 1
  for: 5m
  annotations:
    summary: "KMS gRPC p99 latency exceeds 1s — API server write latency elevated"

# Alert: Encryption transformation errors
- alert: EncryptionTransformationErrors
  expr: increase(apiserver_storage_transformation_operations_total{status="failed"}[5m]) > 0
  severity: critical
  annotations:
    summary: "Encryption/decryption failures — data may be inaccessible"

# Alert: DEK cache full
- alert: DEKCacheNearFull
  expr: apiserver_envelope_encryption_dek_cache_fill_percent > 90
  annotations:
    summary: "DEK cache over 90% full — consider increasing cachesize"

Runbooks

KMS Plugin Unreachable

1. Check plugin process: systemctl status kms-plugin
2. Check Unix socket exists: ls -la /var/run/kmsplugin/socket.sock
3. Check cloud KMS connectivity from control plane node
4. KMS v2: existing secrets still readable from DEK cache; new writes fail

Encryption Not Working

1. Check etcdctl value prefix: must start with k8s:enc:
2. Verify --encryption-provider-config flag is set on API server
3. Check API server logs for provider init errors
4. Verify config file exists and has correct permissions (600)

API Server Won't Start After Config Change

1. Check API server logs in /var/log/pods/kube-system_kube-apiserver*/
2. Verify EncryptionConfiguration YAML is valid
3. If old key was removed prematurely: restore key, restart, re-encrypt, remove key
4. Never remove a key while there is data still encrypted with it

Key Rotation Emergency

1. If key is compromised: rotate to new key immediately (add as first provider)
2. Re-encrypt all resources urgently
3. Remove compromised key from config
4. Audit all secret reads in logs for the compromise window

Verify Encrypted etcd Backup

1. Restore backup to test cluster
2. Attempt to read secrets WITHOUT the encryption key
3. Reads should fail with garbled data
4. Confirms backup is truly encrypted at rest

Best Practices

1

Use KMS v2 with a cloud-managed key for production

Local key providers (secretbox, aesgcm) store the encryption key in a config file on the control plane node. If the node is compromised, the key and data are both exposed. KMS v2 with AWS KMS/GCP Cloud KMS/Azure Key Vault separates key management from data storage.

2

Encrypt at minimum: secrets and configmaps

Secrets are the obvious target. ConfigMaps are often overlooked but frequently contain connection strings, environment-specific config, or feature flags with security implications.

3

Remove identity provider after re-encryption

Leaving identity in the providers list permanently creates a window where plaintext data could be written (e.g., if the primary provider fails and falls through). Remove it once all existing data is encrypted.

4

Never remove a key before re-encrypting all data

Removing a key while data still encrypted with it exists in etcd will make that data permanently unreadable. Always: add new key first → re-encrypt all → remove old key.

5

Restrict access to the EncryptionConfiguration file

The config file contains encryption keys (for local providers) or KMS key references. Set chmod 600 owned by root. Never commit to version control. For KMS v2, the file only contains a socket path — less sensitive, but still should be protected.

6

Test restoration from encrypted etcd backups

Regularly practice restoring etcd backups to confirm the encryption/decryption pipeline works end-to-end. An encrypted backup you cannot decrypt is worse than no backup — you will discover this at the worst possible moment.

7

Monitor KMS plugin health continuously

If the KMS plugin becomes unreachable, new Secret writes will fail (with KMS v2, reads continue from DEK cache). Alert on KMS plugin health endpoint and RPC latency. Have a runbook for KMS plugin restart.

8

Combine encryption at rest with RBAC and audit logging

Encryption at rest protects against offline access. RBAC (see RBAC) restricts live API access. Audit logging (see Audit Logging) provides forensic visibility. All three are required for a complete secrets security posture.