CKS — Encryption at Rest
Encryption at Rest
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
THIS IS A MUST-KNOW CKS TOPIC. Encryption at rest is one of the most frequently tested topics on the CKS exam. You must be able to create an
EncryptionConfiguration, configure the API server, and verify that Secrets are encrypted in etcd.
By default, Kubernetes stores Secrets in etcd as base64-encoded plaintext. Encryption at rest ensures that even if an attacker gains access to etcd data or backups, they cannot read Secret values without the encryption key.
1. The Problem — Secrets in etcd Without Encryption
# Without encryption at rest, Secrets are readable in etcd:
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
# You will see the base64-encoded values in PLAINTEXT in the hex dump
# Example: "password123" is visible as ASCII text
This means:
- etcd backups contain all secrets in plaintext
- etcd disk contains all secrets in plaintext
- Anyone with etcd TLS credentials can dump every credential in your cluster
2. EncryptionConfiguration Resource
Kubernetes uses the EncryptionConfiguration resource to define how Secrets (and other resources) are encrypted before being written to etcd.
Complete EncryptionConfiguration YAML
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <BASE64_ENCODED_32_BYTE_KEY>
- identity: {}
Key Points
resourcesspecifies which Kubernetes resources to encrypt (most commonlysecrets)providersare tried in order — the first provider is used for writing (encryption)- All providers are tried for reading (decryption) — this allows key rotation
identity: {}at the end means “read unencrypted data” — needed to read Secrets created before encryption was enabled- Order matters: If
identityis first, nothing gets encrypted
Full Example with Multiple Resources
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- aescbc:
keys:
- name: key1
secret: dGhpcyBpcyBhIDMyIGJ5dGUga2V5IGZvciBhZXNjYmM=
- identity: {}
3. Encryption Providers
Kubernetes supports multiple encryption providers. Choose based on your security requirements and environment.
| Provider | Encryption | Strength | Key Management | CKS Exam |
|---|---|---|---|---|
identity |
None | No encryption — plaintext | N/A | Default (insecure) |
aescbc |
AES-CBC with PKCS#7 padding | Strong | Static key in config | Recommended for exam |
aesgcm |
AES-GCM | Strong + authenticated | Static key, must rotate frequently | Not recommended (nonce reuse risk) |
secretbox |
XSalsa20 + Poly1305 | Strong + authenticated | Static key in config | Good alternative |
kms v1 |
Envelope encryption | Strongest | External KMS (Vault, AWS KMS, etc.) | Production-grade |
kms v2 |
Envelope encryption (improved) | Strongest | External KMS with DEK reuse | Production-grade |
Why aescbc for the Exam
- Simplest working encryption provider
- No external dependencies (no KMS server required)
- All configuration is self-contained in a single file
- Well-documented in Kubernetes docs
- Most likely what the exam environment supports
Exam Tip: Unless the question specifically asks for KMS, use
aescbc. It’s fast to configure and easy to verify.
Provider Comparison — Encryption Flow
identity (no encryption):
Secret → base64 → etcd (plaintext)
aescbc:
Secret → AES-CBC encrypt with static key → etcd (encrypted blob)
kms v2 (envelope encryption):
Secret → Encrypt with DEK → Store encrypted DEK + encrypted secret in etcd
└── DEK encrypted by remote KMS (Vault, AWS KMS, etc.)
4. Generating the Encryption Key
The encryption key must be 32 bytes for aescbc, base64-encoded:
# Generate a 32-byte random key and base64-encode it:
head -c 32 /dev/urandom | base64
# Example output: dGhpcyBpcyBhIDMyIGJ5dGUga2V5IGZvciBhZXNjYmM=
Important: This key is the only thing protecting your Secrets. If it’s compromised, all encrypted Secrets can be decrypted. Store it securely and restrict file permissions.
5. Step-by-Step: Enable Encryption at Rest
Step 1: Generate the Encryption Key
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY
Step 2: Create the EncryptionConfiguration File
sudo mkdir -p /etc/kubernetes/enc
cat <<EOF | sudo tee /etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: ${ENCRYPTION_KEY}
- identity: {}
EOF
Step 3: Set Restrictive Permissions
sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo chown root:root /etc/kubernetes/enc/enc.yaml
Step 4: Configure the API Server
Edit the kube-apiserver static Pod manifest:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add the flag to the command:
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml # ← ADD THIS
# ... other flags ...
Add the volume and volume mount:
spec:
containers:
- name: kube-apiserver
volumeMounts:
- name: enc-config
mountPath: /etc/kubernetes/enc
readOnly: true
# ... other volume mounts ...
volumes:
- name: enc-config
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
# ... other volumes ...
Step 5: Wait for API Server Restart
The kubelet detects the manifest change and automatically restarts the API server Pod:
# Watch for the API server to come back:
watch -n 1 "kubectl get pods -n kube-system | grep kube-apiserver"
# Or check container status:
sudo crictl ps | grep kube-apiserver
# Give it 30-60 seconds. If it doesn't come back, check logs:
sudo crictl logs $(sudo crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}')
Step 6: Verify — Create a New Secret and Check etcd
# Create a test secret:
kubectl create secret generic test-encryption \
--from-literal=mykey=mydata
# Read it from etcd:
ETCDCTL_API=3 etcdctl get /registry/secrets/default/test-encryption \
--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
Before encryption: You’ll see mydata or its base64 encoding in ASCII in the hex dump.
After encryption: You’ll see an encrypted blob. The data starts with the prefix:
k8s:enc:aescbc:v1:key1:<encrypted binary data>
6. Encrypting Existing Secrets
Enabling encryption at rest only affects newly created or updated Secrets. Existing Secrets remain unencrypted in etcd until they are re-written.
Encrypt All Existing Secrets
# This reads every Secret and writes it back, triggering encryption:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Verify Encryption for a Specific Secret
# Check an existing secret in etcd:
ETCDCTL_API=3 etcdctl get /registry/secrets/kube-system/default-token-xxxxx \
--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
# Look for the k8s:enc:aescbc:v1:key1 prefix
# If you see plaintext base64, the secret hasn't been re-encrypted yet
7. Verifying Encryption in etcd — Detailed
This is a critical exam skill — knowing how to verify that encryption is actually working.
Before Encryption (identity provider)
ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \
--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
# Output will contain readable content like:
# k8s
#
# v1Secret...
# password
# s3cur3P@ss ← Secret value visible!
After Encryption (aescbc provider)
ETCDCTL_API=3 etcdctl get /registry/secrets/default/my-secret \
--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
# Output will show:
# 00000000 ... k8s:enc:aescbc:v1:key1 ...
# followed by binary encrypted data
# The actual secret value is NOT visible
Key Prefixes by Provider
| Provider | etcd Value Prefix |
|---|---|
identity |
k8s\n\x00 (no prefix — plaintext) |
aescbc |
k8s:enc:aescbc:v1:<key-name>: |
aesgcm |
k8s:enc:aesgcm:v1:<key-name>: |
secretbox |
k8s:enc:secretbox:v1:<key-name>: |
kms v1 |
k8s:enc:kms:v1:<config-name>: |
kms v2 |
k8s:enc:kms:v2:<config-name>: |
8. Key Rotation
To rotate encryption keys without downtime:
Step 1: Add the New Key (First Position) and Keep the Old Key
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key2 # ← New key (used for writing)
secret: <NEW_KEY_BASE64>
- name: key1 # ← Old key (still used for reading)
secret: <OLD_KEY_BASE64>
- identity: {}
Step 2: Restart API Server
# The kubelet auto-restarts after manifest change
# Wait for the API server to come back:
kubectl get nodes # Simple connectivity test
Step 3: Re-encrypt All Secrets
# Re-encrypt with the new key:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Step 4: Remove the Old Key
Once all Secrets are re-encrypted, remove the old key from the config:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key2
secret: <NEW_KEY_BASE64>
- identity: {}
Step 5: Restart API Server Again
# Confirm all secrets are still readable:
kubectl get secrets --all-namespaces
9. Decrypting — Reverting to No Encryption
To disable encryption and revert to plaintext storage:
Step 1: Move identity to First Position
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- identity: {} # ← Now first — new writes are plaintext
- aescbc:
keys:
- name: key1
secret: <KEY_BASE64> # ← Still needed to read encrypted secrets
Step 2: Restart API Server and Re-write All Secrets
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Step 3: Remove the EncryptionConfiguration Entirely
Remove the --encryption-provider-config flag from the API server manifest and delete the config file.
10. Troubleshooting
API Server Won’t Start After Adding Encryption Config
# Check API server container logs:
sudo crictl logs $(sudo crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}')
# Common errors:
# - "no such file or directory" → Volume mount path is wrong
# - "invalid EncryptionConfiguration" → YAML syntax error
# - "secret is not of the expected length" → Key is not 32 bytes (base64 of 32 bytes)
Secrets Cannot Be Read After Enabling Encryption
This happens when identity: {} is missing from the providers list and there are pre-existing unencrypted Secrets:
# WRONG — cannot read old unencrypted secrets:
providers:
- aescbc:
keys:
- name: key1
secret: <KEY>
# CORRECT — can read both encrypted and unencrypted:
providers:
- aescbc:
keys:
- name: key1
secret: <KEY>
- identity: {} # ← Needed for reading old unencrypted data
Verifying the Config Is Loaded
# Check if the API server flag is present:
ps aux | grep kube-apiserver | grep encryption-provider-config
# Check if the config file is mounted:
sudo crictl inspect $(sudo crictl ps | grep kube-apiserver | awk '{print $1}') | \
jq '.info.config.mounts[] | select(.destination | contains("enc"))'
Summary
| Step | Action | Command/File |
|---|---|---|
| 1 | Generate key | head -c 32 /dev/urandom \| base64 |
| 2 | Create config | /etc/kubernetes/enc/enc.yaml |
| 3 | Configure API server | --encryption-provider-config flag + volume mount |
| 4 | Restart API server | Edit static Pod manifest, kubelet auto-restarts |
| 5 | Verify new secrets | etcdctl get ... \| hexdump -C — look for k8s:enc:aescbc |
| 6 | Encrypt existing | kubectl get secrets --all-namespaces -o json \| kubectl replace -f - |
| 7 | Key rotation | Add new key first, re-encrypt, remove old key |
Practice Questions
Question 1
Write a complete EncryptionConfiguration that encrypts Secrets using aescbc with a key named prod-key-1.
Show Answer
# First, generate a 32-byte key:
head -c 32 /dev/urandom | base64
# Example: czNjdXIzUEBzc3dvcmQxMjM0NTY3ODkwMTIzNDU2Nzg=
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: prod-key-1
secret: czNjdXIzUEBzc3dvcmQxMjM0NTY3ODkwMTIzNDU2Nzg=
- identity: {}
Key points:
apiVersion: apiserver.config.k8s.io/v1(must be exact)aescbcis listed beforeidentity(otherwise nothing gets encrypted)identity: {}at the end allows reading pre-existing unencrypted Secrets- The key must be exactly 32 bytes, base64-encoded
Question 2
After enabling encryption at rest, you create a new Secret. How do you verify it’s actually encrypted in etcd?
Show Answer
Use etcdctl to read the Secret directly from etcd and inspect the raw data:
# Create a test secret:
kubectl create secret generic verify-enc --from-literal=test=hello
# Read from etcd:
ETCDCTL_API=3 etcdctl get /registry/secrets/default/verify-enc \
--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
Look for the prefix k8s:enc:aescbc:v1:<key-name> in the output. If encryption is working:
- The data will be binary (not human-readable)
- The prefix
k8s:enc:aescbc:v1:key1will be visible
If encryption is NOT working:
- You’ll see the Secret data in plaintext or base64
# Cleanup:
kubectl delete secret verify-enc
Question 3
You enabled encryption at rest, but Secrets created before the change are still unencrypted in etcd. How do you fix this?
Show Answer
Enabling encryption only affects new writes. Existing Secrets must be re-written to trigger encryption:
# Re-encrypt all existing Secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
This command:
- Reads every Secret via the API server (which decrypts them transparently)
- Writes them back via
kubectl replace(which re-encrypts with the current provider)
After this, verify a pre-existing Secret:
ETCDCTL_API=3 etcdctl get /registry/secrets/kube-system/<secret-name> \
--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 now show k8s:enc:aescbc:v1:key1 prefix
Question 4
What happens if you put identity: {} as the first provider in your EncryptionConfiguration?
Show Answer
If identity: {} is the first provider, no encryption occurs. The first provider in the list is used for writing, and identity means “no encryption.”
# WRONG — this does NOT encrypt anything:
providers:
- identity: {} # ← First = used for writing = NO encryption
- aescbc:
keys:
- name: key1
secret: <KEY>
# CORRECT — aescbc first = encryption active:
providers:
- aescbc:
keys:
- name: key1
secret: <KEY>
- identity: {} # ← Only used as fallback for reading old data
The identity provider is useful as the last provider to allow reading Secrets that were stored before encryption was enabled. But it must never be the first provider if you want encryption.
Question 5
You need to rotate the encryption key from key1 to key2 without downtime. Describe the steps.
Show Answer
Step 1: Add the new key before the old key in the EncryptionConfiguration:
providers:
- aescbc:
keys:
- name: key2 # New key — used for writing
secret: <NEW_KEY>
- name: key1 # Old key — used for reading
secret: <OLD_KEY>
- identity: {}
Step 2: Restart the API server (edit static Pod manifest, kubelet auto-restarts):
# Wait for API server:
kubectl get nodes
Step 3: Re-encrypt all Secrets with the new key:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Step 4: Remove the old key from the configuration:
providers:
- aescbc:
keys:
- name: key2
secret: <NEW_KEY>
- identity: {}
Step 5: Restart the API server again. All Secrets are now encrypted with key2 only.
This process ensures zero downtime because both keys are available for reading during the transition.
| Previous | Up | Next |
|---|---|---|
| Kubernetes Secrets Deep Dive | Secrets Management | External Secret Stores |