Encryption at Rest
etcd encryption at rest for Kubernetes Secrets and other resources: provider hierarchy, KMS v2 envelope encryption, key rotation procedures, verification, etcd disk encryption, and node-level storage security.
Coverage Checklist
- etcd stores plaintext by default — the fundamental problem
- EncryptionConfiguration resource and API server flag
- Providers: identity / secretbox / aesgcm / aescbc / kms v1 / kms v2
- Provider selection order (first = write, all = read)
- KMS v2 envelope encryption: DEK/KEK model
- KMS v2 vs v1 improvements: DEK caching, performance
- KMS plugin: gRPC socket, Health check
- AWS KMS / GCP CMEK / Azure Key Vault integration
- Full EncryptionConfiguration YAML for all providers
- Enabling encryption: step-by-step procedure
- Re-encrypting existing resources: kubectl replace
- Key rotation: zero-downtime procedure
- Verifying: etcdctl get + base64 decode
- etcd TLS configuration
- etcd disk-level encryption: LUKS
- Node ephemeral storage: emptyDir, tmpfs
- Managed K8s: EKS / GKE / AKS encryption options
- 5 metrics, 4 alerts, 5 runbooks, 8 best practices
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 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.
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
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:
| Resource | Why Encrypt | Priority |
|---|---|---|
secrets | Contains passwords, tokens, TLS keys | Critical |
configmaps | May contain environment-specific config with sensitive values | High |
serviceaccounts | SA metadata (not tokens — those are JWTs and separate) | Medium |
pods | Pod specs can contain env var secrets, volume refs | Medium |
| Custom resources | CRDs that store credentials or sensitive config | Case-by-case |
Encryption Providers
| Provider | Algorithm | Key Management | Performance | Recommended |
|---|---|---|---|---|
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 |
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.
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 (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
| Platform | Plugin | Key Type |
|---|---|---|
| AWS EKS / self-managed on EC2 | aws-encryption-provider | AWS KMS CMK (symmetric) |
| GKE / GCE | kmsplugin (built into GKE) or cloud-kms-plugin | GCP Cloud KMS CryptoKey |
| AKS / Azure | azure-kms-plugin | Azure Key Vault key |
| Any | vault-k8s-kms-plugin | HashiCorp Vault Transit key |
| Any | k8s-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: {}
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.
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.
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.
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.
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.
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)
Generate new key
head -c 32 /dev/urandom | base64 — store securely.
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.
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.
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
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 Location | Default State | Encryption Mechanism |
|---|---|---|
| etcd Secrets | Plaintext (base64) | EncryptionConfiguration + KMS |
| etcd data directory | Depends on cloud/disk config | LUKS / cloud disk encryption |
| Secret volume mount | tmpfs (in-memory) | N/A — RAM only, cleared on unmount |
| emptyDir volume | Node disk (unless medium:Memory) | Node disk encryption or tmpfs |
| Container image layers | Plaintext on node | OCI image encryption (ocicrypt) |
| PersistentVolume | Depends on StorageClass | Storage 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
| Metric | Source | Description |
|---|---|---|
apiserver_envelope_encryption_dek_cache_fill_percent | kube-apiserver | DEK cache fill ratio (KMS v2) |
apiserver_storage_transformation_operations_total | kube-apiserver | Encrypt/decrypt operations by provider and resource |
apiserver_storage_transformation_duration_seconds | kube-apiserver | Latency of encryption/decryption operations |
etcd_disk_wal_fsync_duration_seconds | etcd | etcd write latency (elevated if storage is slow) |
kms_plugin_rpc_duration_seconds | KMS plugin | Latency 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
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.
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.
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.
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.
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.
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.
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.
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.