By default, Kubernetes stores all data in etcd as plain text (base64 for Secrets is just encoding, not security). Encryption at rest ensures that even if an attacker gains access to etcd's data files, they cannot read sensitive information. This is a CKS exam staple and a production security requirement.

1. The Threat Model

What encryption at rest protects against:

ThreatProtected?How
Attacker reads etcd data files on disk✅ YesData encrypted before writing to etcd
Attacker steals etcd backup files✅ YesBackup contains encrypted data
Attacker compromises etcd network✅ PartiallyData in etcd is encrypted (but etcd-to-API TLS also needed)
Attacker has kubectl get secrets RBAC❌ NoAPI server decrypts before returning — RBAC is the control here
Attacker compromises the API server❌ NoAPI server holds the decryption keys (or KMS access)
Encryption at rest is defense in depth. It protects the data layer (etcd) but does NOT replace RBAC, network security, or audit logging. An attacker with API access still sees plaintext Secrets. Encryption at rest specifically protects against physical access to etcd storage.

2. EncryptionConfiguration

The API server reads an EncryptionConfiguration file that defines which resources to encrypt and which encryption provider to use.

# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets                  # Encrypt Secrets
      - configmaps               # Can also encrypt ConfigMaps
    providers:                   # Tried in order for WRITES; all tried for READS
      - aescbc:                  # ← Primary: used for all new writes
          keys:
            - name: key-2024-01
              secret: dGhpcy1pcy1hLTMyLWJ5dGUta2V5LTEyMzQ1Ng==  # 32-byte base64
      - identity: {}             # ← Fallback: can read unencrypted (old) data

Provider Ordering Rules

  • First provider = used for writing (encrypting new/updated resources)
  • All providers = tried in order for reading (decrypting existing resources)
  • identity: {} = reads unencrypted data (needed during migration from plaintext to encrypted)
WRITE PATH: API Server Encrypt (aescbc) etcd (cipher) READ PATH: etcd (cipher) Try each provider until one decrypts Plaintext

3. Encryption Providers

ProviderAlgorithmKey LocationStrengthNotes
identityNoneN/A❌ No encryptionDefault; used as fallback reader
secretboxXSalsa20 + Poly1305Config file✅ StrongRecommended for local key
aescbcAES-256-CBC + HMACConfig file✅ StrongMost commonly used, well-understood
aesgcmAES-256-GCMConfig file⚠️ Key must be rotated frequentlyNonce reuse risk after ~200k writes per key
kms v1Envelope (external)External KMS✅✅ BestDeprecated in 1.28
kms v2Envelope (external)External KMS✅✅ BestStable in 1.29+; uses gRPC

Local Key Providers (aescbc, secretbox)

# Generate a 32-byte key:
head -c 32 /dev/urandom | base64
# Output: dGhpcy1pcy1hLTMyLWJ5dGUta2V5LTEyMzQ1Ng==

# Use in config:
providers:
  - secretbox:
      keys:
        - name: key-2024-01
          secret: dGhpcy1pcy1hLTMyLWJ5dGUta2V5LTEyMzQ1Ng==
Local key providers store the encryption key on the same disk as the API server. If an attacker compromises the control plane node, they get both the encrypted data (etcd) and the key to decrypt it. For true security separation, use KMS envelope encryption.

KMS v2 Envelope Encryption

Envelope encryption adds a layer of indirection:

Secret data (plaintext) DEK encrypts Data Encryption Key (generated per Secret) KEK encrypts DEK Key Encryption Key (lives in KMS only) Stored in etcd: encrypted(Secret) + encrypted(DEK) KEK never leaves KMS ✓ To decrypt: API server sends encrypted DEK to KMS → KMS returns plaintext DEK → decrypt Secret KEK (master key) NEVER touches the K8s cluster
# KMS v2 provider configuration:
providers:
  - kms:
      apiVersion: v2
      name: my-kms-provider
      endpoint: unix:///var/run/kms-plugin/socket.sock
      timeout: 3s
CloudKMS PluginKEK Location
AWSaws-encryption-providerAWS KMS key in your account
GCPBuilt into GKECloud KMS key
Azureazure-kms-providerAzure Key Vault
Self-managedHashicorp Vault pluginVault transit engine
Envelope encryption is the gold standard because: (1) The master key (KEK) never exists on the K8s node — it's in hardware-backed KMS. (2) Each Secret gets its own DEK — compromising one DEK exposes only one Secret. (3) Key rotation only needs to re-encrypt DEKs (fast), not all data.

4. Step-by-Step: Enable Encryption at Rest

Step 1: Generate Key and Create Config

# Generate a 32-byte encryption key:
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

# Create the config file:
cat > /etc/kubernetes/enc/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key-$(date +%Y%m%d)
              secret: ${ENCRYPTION_KEY}
      - identity: {}
EOF

# Set restrictive permissions:
chmod 600 /etc/kubernetes/enc/encryption-config.yaml

Step 2: Configure the API Server

# Edit /etc/kubernetes/manifests/kube-apiserver.yaml:
spec:
  containers:
    - command:
        - kube-apiserver
        - --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
        # ... other flags
      volumeMounts:
        - name: enc
          mountPath: /etc/kubernetes/enc
          readOnly: true
  volumes:
    - name: enc
      hostPath:
        path: /etc/kubernetes/enc
        type: DirectoryOrCreate

The API server static Pod restarts automatically when its manifest changes.

Step 3: Re-encrypt Existing Secrets

# Wait for API server to restart, then:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -

# This reads each Secret (decrypts with old provider or identity)
# and writes it back (encrypts with new primary provider)

Step 4: Verify

# Read directly from etcd (bypass API server):
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-creds \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key | hexdump -C

# Should see: k8s:enc:aescbc:v1:key-20240115:... (encrypted bytes)
# NOT: {"apiVersion":"v1","data":... (plaintext JSON)
CKS exam steps: (1) Create encryption config file with aescbc provider, (2) Add --encryption-provider-config to API server manifest, (3) Add volume/volumeMount for the config, (4) Wait for API server restart, (5) Re-encrypt existing Secrets, (6) Verify with etcdctl. Practice this flow until you can do it in under 5 minutes.

5. Key Rotation

Encryption keys should be rotated periodically. The process for local keys:

Rotation Steps

  1. Add new key as first in the list (it becomes the write key)
  2. Keep old key(s) below it (still used for reading old data)
  3. Restart API server
  4. Re-encrypt all Secrets (they get re-encrypted with new key)
  5. Remove old key from config (no data is encrypted with it anymore)
  6. Restart API server again
# After rotation, the config looks like:
providers:
  - aescbc:
      keys:
        - name: key-2024-06    # ← NEW key (writes)
          secret: bmV3LWtleS0yMDI0LTA2LXJvdGF0aW9u...
        - name: key-2024-01    # ← OLD key (reads only, until re-encryption done)
          secret: dGhpcy1pcy1hLTMyLWJ5dGUta2V5LTEyMzQ1Ng==
  - identity: {}
For KMS v2 envelope encryption, key rotation is much simpler: rotate the KEK in your cloud KMS (AWS/GCP/Azure console). New DEKs are encrypted with the new KEK automatically. Old DEKs can still be decrypted because KMS retains old key versions. No need to re-encrypt all data.

Removing identity (Disabling Unencrypted Reads)

Once all Secrets are encrypted and verified, you can remove identity: {} from the providers list. This ensures the API server cannot read unencrypted data — any plaintext Secret left behind will cause a read error (which alerts you to the problem).

Only remove identity: {} after confirming ALL Secrets are encrypted. If any remain unencrypted, they become unreadable. Verify first: etcdctl get /registry/secrets/ --prefix --keys-only and spot-check several.

Summary

ConceptKey Point
Default stateSecrets stored as plaintext (base64) in etcd — no encryption
EncryptionConfigurationDefines which resources to encrypt and which provider to use
Provider orderFirst = writes; all = tried for reads
aescbc / secretboxLocal key on disk — protects against etcd compromise, not API server compromise
KMS v2Envelope encryption — KEK never leaves external KMS. Gold standard.
identity: {}Read-only fallback for unencrypted data — needed during migration
Re-encryptionMust manually re-write existing Secrets after enabling encryption
Key rotationAdd new key first → re-encrypt all → remove old key
Verificationetcdctl get + hexdump — should show encrypted prefix, not JSON

📝 Quiz: Encryption at Rest

Q1: You enable encryption at rest with aescbc. An attacker with kubectl get secrets RBAC access tries to read a Secret. Can they see the plaintext?

Yes. Encryption at rest protects data in etcd (on disk). The API server decrypts Secrets before returning them to authorized clients. RBAC controls who can read Secrets through the API — encryption at rest is a different layer of defense (protects against etcd compromise, not API access).

Q2: You have two providers: [aescbc, identity]. Which is used for writing new Secrets? Which for reading?

Writing: aescbc (first provider). All new Secrets are encrypted with aescbc.
Reading: Both are tried in order. If data is encrypted (aescbc prefix), the aescbc provider decrypts it. If data is plaintext (old Secrets not yet re-encrypted), the identity provider reads it as-is.

Q3: You enabled encryption, restarted the API server, but forgot to re-encrypt existing Secrets. Are they encrypted now?

No. Existing Secrets remain unencrypted in etcd. Encryption only applies to new writes. You must force a re-write: kubectl get secrets --all-namespaces -o json | kubectl replace -f -. This reads each Secret (through the API) and writes it back (now encrypted with the new provider).

Q4: Why is KMS envelope encryption more secure than aescbc with a local key?

With aescbc, the encryption key sits on the API server's filesystem — compromising that node gives you both encrypted data AND the key. With KMS envelope encryption, the master key (KEK) never leaves the external KMS. Even if the node is fully compromised, the attacker has encrypted DEKs they cannot decrypt without KMS access (which requires separate IAM credentials).

Q5: You need to rotate your aescbc encryption key. What's the correct order of operations?

(1) Add new key as the first item in the keys list (becomes write key). Keep old key below. (2) Restart API server. (3) Re-encrypt all Secrets: kubectl get secrets -A -o json | kubectl replace -f -. (4) Remove old key from config. (5) Restart API server. Never remove the old key before re-encryption completes — data encrypted with it would become unreadable.

Q6: How do you verify that a Secret is actually encrypted in etcd (not just base64)?

Read directly from etcd bypassing the API server:
etcdctl get /registry/secrets/default/my-secret --endpoints=... --cacert=... --cert=... --key=... | hexdump -C
Encrypted data starts with k8s:enc:aescbc:v1:key-name: followed by binary. Unencrypted data shows readable JSON with {"apiVersion":"v1","data":...