CKS — Secrets Management Lab
Secrets Management — Hands-On Lab
CKS Exam Domain: Minimize Microservice Vulnerabilities (20%)
This lab walks you through the complete lifecycle of secrets management in Kubernetes — from creating secrets to encrypting them at rest, restricting access with RBAC, and auditing permissions. Each exercise mirrors the type of tasks you may face on the CKS exam.
Prerequisites
- A Kubernetes cluster with control plane access (kubeadm preferred — you need access to API server manifests and etcd)
kubectlconfigured with cluster-admin accessetcdctlinstalled on the control plane node- SSH access to the control plane node (for modifying static Pod manifests)
Lab 1: Create Secrets Using kubectl
Task
Create Kubernetes Secrets using different methods — from literals, from files, and verify their contents.
Steps
# Create a namespace for the lab
kubectl create namespace secrets-lab
# 1. Create a secret from literal values
kubectl create secret generic db-creds \
--from-literal=username=admin \
--from-literal=password=CKS-Lab-P@ss123 \
-n secrets-lab
# 2. Create a secret from a file
echo -n "my-api-key-value-12345" > /tmp/api-key.txt
kubectl create secret generic api-key \
--from-file=key=/tmp/api-key.txt \
-n secrets-lab
# 3. Create a TLS secret (generate self-signed cert first)
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/tls.key -out /tmp/tls.crt \
-subj "/CN=lab.example.com"
kubectl create secret tls lab-tls \
--cert=/tmp/tls.crt \
--key=/tmp/tls.key \
-n secrets-lab
# 4. Verify secrets were created
kubectl get secrets -n secrets-lab
Expected Output
NAME TYPE DATA AGE
api-key Opaque 1 5s
db-creds Opaque 2 10s
lab-tls kubernetes.io/tls 2 3s
Verify Secret Contents
# Decode the db-creds secret
kubectl get secret db-creds -n secrets-lab -o jsonpath='{.data.username}' | base64 -d
# admin
kubectl get secret db-creds -n secrets-lab -o jsonpath='{.data.password}' | base64 -d
# CKS-Lab-P@ss123
# Decode the api-key secret
kubectl get secret api-key -n secrets-lab -o jsonpath='{.data.key}' | base64 -d
# my-api-key-value-12345
# View all keys in a secret
kubectl get secret db-creds -n secrets-lab -o json | jq -r '.data | keys[]'
# password
# username
Lab 2: Verify Secrets Are Base64 in etcd (Before Encryption)
Task
Prove that Secrets are stored as base64-encoded plaintext in etcd before encryption at rest is enabled.
Steps
# SSH to the control plane node (if not already there)
# Read the db-creds secret directly from etcd
ETCDCTL_API=3 etcdctl get /registry/secrets/secrets-lab/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
Expected Output
You should see the Secret data in readable form within the output. Look for strings like admin and CKS-Lab-P@ss123 in the raw output.
# Use hexdump for clearer output
ETCDCTL_API=3 etcdctl get /registry/secrets/secrets-lab/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 | head -40
# You will see ASCII representations of the secret values
# This proves secrets are NOT encrypted in etcd by default
What This Proves
- Secrets in etcd are base64-encoded, not encrypted
- Anyone with etcd credentials can read every secret in the cluster
- etcd backups contain all secrets in plaintext
- Encryption at rest is essential for production security
Lab 3: Create EncryptionConfiguration for aescbc
Task
Create an EncryptionConfiguration resource that encrypts Secrets in etcd using the aescbc provider.
Steps
# SSH to the control plane node
# Step 1: Generate a 32-byte encryption key
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo "Generated key: ${ENCRYPTION_KEY}"
# Step 2: Create the directory
sudo mkdir -p /etc/kubernetes/enc
# Step 3: Create the EncryptionConfiguration
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 4: Set restrictive permissions
sudo chmod 600 /etc/kubernetes/enc/enc.yaml
sudo chown root:root /etc/kubernetes/enc/enc.yaml
# Step 5: Verify the file
sudo cat /etc/kubernetes/enc/enc.yaml
Expected Output
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <your-generated-key>
- identity: {}
Verification Checklist
- File exists at
/etc/kubernetes/enc/enc.yaml - Permissions are
600(readable only by root) aescbcis the first provider (beforeidentity)identity: {}is present as fallback for reading old unencrypted secrets- Key is exactly 32 bytes base64-encoded
Lab 4: Configure API Server with –encryption-provider-config
Task
Modify the kube-apiserver static Pod manifest to use the encryption configuration.
Steps
# Step 1: Back up the current API server manifest
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml \
/etc/kubernetes/manifests/kube-apiserver.yaml.bak
# Step 2: Edit the manifest
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Changes to Make
Add the flag to the command section:
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc/enc.yaml # ADD THIS LINE
# ... keep all existing flags ...
Add the volume mount to the container:
volumeMounts:
- name: enc-config # ADD THIS BLOCK
mountPath: /etc/kubernetes/enc
readOnly: true
# ... keep all existing volume mounts ...
Add the volume to the Pod spec:
volumes:
- name: enc-config # ADD THIS BLOCK
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
# ... keep all existing volumes ...
Wait for Restart
# The kubelet will detect the change and restart the API server
# Wait for it to come back (may take 30-60 seconds):
# Method 1: Watch kubectl
watch -n 2 "kubectl get nodes 2>&1"
# Method 2: Watch the container
sudo crictl ps | grep kube-apiserver
# Method 3: Check logs if it doesn't come back
sudo crictl logs $(sudo crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}') 2>&1 | tail -20
Troubleshooting
If the API server doesn’t come back:
# Check for common errors:
# 1. File not found — verify the path
sudo ls -la /etc/kubernetes/enc/enc.yaml
# 2. YAML syntax error — validate
python3 -c "import yaml; yaml.safe_load(open('/etc/kubernetes/enc/enc.yaml'))"
# 3. Key length error — verify key is 32 bytes
KEY=$(sudo grep 'secret:' /etc/kubernetes/enc/enc.yaml | awk '{print $2}')
echo -n "$KEY" | base64 -d | wc -c
# Must be exactly 32
# 4. Restore backup if needed
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
/etc/kubernetes/manifests/kube-apiserver.yaml
Lab 5: Verify Encryption in etcd (After Encryption)
Task
Create a new Secret after encryption is enabled and verify it’s encrypted in etcd.
Steps
# Step 1: Create a new test secret
kubectl create secret generic encrypted-test \
--from-literal=testkey=testvalue123 \
-n secrets-lab
# Step 2: Read it from etcd
ETCDCTL_API=3 etcdctl get /registry/secrets/secrets-lab/encrypted-test \
--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
Expected Output
You should see the prefix k8s:enc:aescbc:v1:key1 followed by binary (encrypted) data. The actual secret value testvalue123 should NOT be visible.
00000000 ... k 8 s : e n c : a e s c b c : v 1 : k e y 1 ...
00000010 ... <binary encrypted data> ...
Compare: Old Secret (Before Encryption)
# The db-creds secret was created BEFORE encryption was enabled
# It should still be in plaintext:
ETCDCTL_API=3 etcdctl get /registry/secrets/secrets-lab/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 should still see "admin" and "CKS-Lab-P@ss123" in plaintext
# This proves that enabling encryption does NOT retroactively encrypt existing secrets
Verify via kubectl (Should Still Work)
# kubectl reads through the API server, which handles decryption transparently:
kubectl get secret encrypted-test -n secrets-lab -o jsonpath='{.data.testkey}' | base64 -d
# testvalue123
Lab 6: Encrypt All Existing Secrets
Task
Re-encrypt all pre-existing Secrets so they are stored encrypted in etcd.
Steps
# Step 1: Re-encrypt all secrets in all namespaces
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Expected Output
You’ll see each Secret being replaced:
secret/db-creds replaced
secret/api-key replaced
secret/lab-tls replaced
secret/encrypted-test replaced
...
Verify the Old Secret Is Now Encrypted
# Check db-creds which was created BEFORE encryption:
ETCDCTL_API=3 etcdctl get /registry/secrets/secrets-lab/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
# NOW you should see k8s:enc:aescbc:v1:key1 prefix
# The plaintext values should NO LONGER be visible
Verify All Secrets Still Work
# Confirm secrets are still readable through the API:
kubectl get secret db-creds -n secrets-lab -o jsonpath='{.data.password}' | base64 -d
# CKS-Lab-P@ss123
kubectl get secret api-key -n secrets-lab -o jsonpath='{.data.key}' | base64 -d
# my-api-key-value-12345
Lab 7: Create RBAC to Restrict Secret Access
Task
Create RBAC rules that allow a ServiceAccount to read only a specific Secret.
Steps
# Step 1: Create a ServiceAccount
kubectl create serviceaccount app-sa -n secrets-lab
# Step 2: Create a Role that only allows reading db-creds
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: db-creds-reader
namespace: secrets-lab
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-creds"]
verbs: ["get"]
EOF
# Step 3: Create a RoleBinding
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-sa-read-db-creds
namespace: secrets-lab
subjects:
- kind: ServiceAccount
name: app-sa
namespace: secrets-lab
roleRef:
kind: Role
name: db-creds-reader
apiGroup: rbac.authorization.k8s.io
EOF
Verify Permissions
# Can get db-creds? (should be YES)
kubectl auth can-i get secrets/db-creds \
--as=system:serviceaccount:secrets-lab:app-sa \
-n secrets-lab
# yes
# Can get api-key? (should be NO)
kubectl auth can-i get secrets/api-key \
--as=system:serviceaccount:secrets-lab:app-sa \
-n secrets-lab
# no
# Can list all secrets? (should be NO)
kubectl auth can-i list secrets \
--as=system:serviceaccount:secrets-lab:app-sa \
-n secrets-lab
# no
# Can create secrets? (should be NO)
kubectl auth can-i create secrets \
--as=system:serviceaccount:secrets-lab:app-sa \
-n secrets-lab
# no
Test Access from a Pod
# Create a Pod using the app-sa ServiceAccount
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: rbac-test
namespace: secrets-lab
spec:
serviceAccountName: app-sa
automountServiceAccountToken: true
containers:
- name: test
image: bitnami/kubectl:latest
command: ["sleep", "3600"]
EOF
# Wait for Pod to be running
kubectl wait --for=condition=ready pod/rbac-test -n secrets-lab --timeout=60s
# Test from inside the Pod:
# Should SUCCEED:
kubectl exec -it rbac-test -n secrets-lab -- \
kubectl get secret db-creds -n secrets-lab -o jsonpath='{.data.password}'
# Q0tTLUxhYi1QQHNzMTIz (base64)
# Should FAIL (Forbidden):
kubectl exec -it rbac-test -n secrets-lab -- \
kubectl get secret api-key -n secrets-lab 2>&1
# Error from server (Forbidden): secrets "api-key" is forbidden
# Should FAIL (Forbidden):
kubectl exec -it rbac-test -n secrets-lab -- \
kubectl get secrets -n secrets-lab 2>&1
# Error from server (Forbidden): secrets is forbidden
Lab 8: Audit Who Can Access Secrets
Task
Perform a comprehensive audit of secret access permissions in the cluster.
Steps
# Step 1: Audit ServiceAccounts in secrets-lab namespace
echo "=== Secret Access Audit: secrets-lab ==="
for sa in $(kubectl get sa -n secrets-lab -o jsonpath='{.items[*].metadata.name}'); do
echo ""
echo "ServiceAccount: secrets-lab/$sa"
for verb in get list watch create update delete; do
result=$(kubectl auth can-i $verb secrets \
--as=system:serviceaccount:secrets-lab:$sa \
-n secrets-lab 2>/dev/null)
if [ "$result" = "yes" ]; then
echo " ✗ CAN $verb secrets"
fi
done
done
Expected Output
=== Secret Access Audit: secrets-lab ===
ServiceAccount: secrets-lab/default
(no secret permissions — good!)
ServiceAccount: secrets-lab/app-sa
✗ CAN get secrets ← Expected (scoped to db-creds only)
Step 2: Cluster-Wide Secret Access Audit
# Find all ClusterRoleBindings that grant secret access:
echo "=== Cluster-Wide Secret Access ==="
for crb in $(kubectl get clusterrolebindings -o jsonpath='{.items[*].metadata.name}'); do
role=$(kubectl get clusterrolebinding "$crb" -o jsonpath='{.roleRef.name}')
# Check if the role has secret access
has_secrets=$(kubectl get clusterrole "$role" -o json 2>/dev/null | \
jq -r '.rules[]? | select(.resources[]? == "secrets" or .resources[]? == "*") | .verbs[]' 2>/dev/null)
if [ -n "$has_secrets" ]; then
subjects=$(kubectl get clusterrolebinding "$crb" \
-o jsonpath='{range .subjects[*]}{.kind}/{.name} {end}')
echo ""
echo "ClusterRoleBinding: $crb"
echo " Role: $role"
echo " Subjects: $subjects"
echo " Secret verbs: $(echo $has_secrets | tr '\n' ' ')"
fi
done
Step 3: Check Specific Users/Groups
# Check if a specific user can access secrets:
kubectl auth can-i get secrets --as=developer -n production 2>/dev/null
kubectl auth can-i list secrets --as=developer --all-namespaces 2>/dev/null
# Full permission listing for a user:
kubectl auth can-i --list --as=developer -n secrets-lab 2>/dev/null | grep -i secret
Cleanup
# Remove lab resources
kubectl delete namespace secrets-lab
# Clean up temporary files
rm -f /tmp/api-key.txt /tmp/tls.key /tmp/tls.crt
# OPTIONAL: Remove encryption at rest (only if you want to revert)
# Step 1: Move identity to first position in /etc/kubernetes/enc/enc.yaml
# Step 2: Restart API server
# Step 3: Re-write all secrets: kubectl get secrets -A -o json | kubectl replace -f -
# Step 4: Remove --encryption-provider-config from API server manifest
# Step 5: Delete /etc/kubernetes/enc/
# Restore API server backup if needed:
# sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
# /etc/kubernetes/manifests/kube-apiserver.yaml
Practice Questions
Question 1
You need to enable encryption at rest for Secrets using aescbc. List the complete steps in order.
Show Answer
- Generate a 32-byte encryption key:
head -c 32 /dev/urandom | base64 - Create the EncryptionConfiguration file:
```bash
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:
- identity: {} EOF sudo chmod 600 /etc/kubernetes/enc/enc.yaml ```
- resources:
- Configure the API server — edit
/etc/kubernetes/manifests/kube-apiserver.yaml:- Add flag:
--encryption-provider-config=/etc/kubernetes/enc/enc.yaml - Add volume mount for
/etc/kubernetes/enc - Add hostPath volume for
/etc/kubernetes/enc
- Add flag:
- Wait for API server restart (kubelet auto-restarts):
kubectl get nodes # Verify connectivity - Verify new secrets are encrypted:
kubectl create secret generic test --from-literal=k=v etcdctl get /registry/secrets/default/test ... | hexdump -C # Look for k8s:enc:aescbc:v1:key1 - Encrypt all existing secrets:
kubectl get secrets -A -o json | kubectl replace -f -
Question 2
Write the kubectl auth can-i commands to verify that ServiceAccount app-sa in namespace myns can ONLY read the db-password Secret and nothing else.
Show Answer
# Should return "yes" — allowed to get the specific secret:
kubectl auth can-i get secrets/db-password \
--as=system:serviceaccount:myns:app-sa -n myns
# yes
# Should return "no" — cannot get other secrets:
kubectl auth can-i get secrets/other-secret \
--as=system:serviceaccount:myns:app-sa -n myns
# no
# Should return "no" — cannot list all secrets:
kubectl auth can-i list secrets \
--as=system:serviceaccount:myns:app-sa -n myns
# no
# Should return "no" — cannot watch secrets:
kubectl auth can-i watch secrets \
--as=system:serviceaccount:myns:app-sa -n myns
# no
# Should return "no" — cannot create secrets:
kubectl auth can-i create secrets \
--as=system:serviceaccount:myns:app-sa -n myns
# no
# Should return "no" — cannot delete secrets:
kubectl auth can-i delete secrets \
--as=system:serviceaccount:myns:app-sa -n myns
# no
# Should return "no" — cannot access secrets in other namespaces:
kubectl auth can-i get secrets \
--as=system:serviceaccount:myns:app-sa -n default
# no
Question 3
After enabling encryption at rest, you notice that old Secrets are still in plaintext when read from etcd. What command fixes this?
Show Answer
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
This command:
- Reads every Secret through the API server (which decrypts transparently if needed)
- Writes each Secret back via
kubectl replace(which encrypts using the current first provider in the EncryptionConfiguration)
After running this, verify with etcdctl:
ETCDCTL_API=3 etcdctl get /registry/secrets/<namespace>/<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
Why this is needed: Enabling encryption only affects new writes. Existing data stays in its original format until it’s re-written.
Question 4
Your API server won’t start after adding the --encryption-provider-config flag. List the troubleshooting steps.
Show Answer
# 1. Check API server container logs:
sudo crictl logs $(sudo crictl ps -a | grep kube-apiserver | head -1 | awk '{print $1}') 2>&1 | tail -30
# 2. Common issues and fixes:
# "no such file or directory":
# → Volume mount is wrong or file path doesn't match
sudo ls -la /etc/kubernetes/enc/enc.yaml
# "invalid EncryptionConfiguration":
# → YAML syntax error
python3 -c "import yaml; yaml.safe_load(open('/etc/kubernetes/enc/enc.yaml'))"
# "secret is not of the expected length":
# → The encryption key is not exactly 32 bytes
KEY=$(sudo grep 'secret:' /etc/kubernetes/enc/enc.yaml | awk '{print $2}')
echo -n "$KEY" | base64 -d | wc -c
# Must output: 32
# "unknown apiVersion":
# → Wrong apiVersion in the config
# Must be: apiserver.config.k8s.io/v1
# 3. If stuck, restore the backup:
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml.bak \
/etc/kubernetes/manifests/kube-apiserver.yaml
# API server will restart with the original config
Question 5
Create a complete RBAC setup (Role + RoleBinding) that allows ServiceAccount monitoring in namespace observability to only read the Secret named grafana-creds. Write the full YAML.
Show Answer
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: grafana-creds-reader
namespace: observability
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["grafana-creds"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: monitoring-read-grafana-creds
namespace: observability
subjects:
- kind: ServiceAccount
name: monitoring
namespace: observability
roleRef:
kind: Role
name: grafana-creds-reader
apiGroup: rbac.authorization.k8s.io
Apply and verify:
kubectl apply -f rbac.yaml
# Verify:
kubectl auth can-i get secrets/grafana-creds \
--as=system:serviceaccount:observability:monitoring \
-n observability
# yes
kubectl auth can-i list secrets \
--as=system:serviceaccount:observability:monitoring \
-n observability
# no
kubectl auth can-i get secrets/other-secret \
--as=system:serviceaccount:observability:monitoring \
-n observability
# no
Key design decisions:
- Role (not ClusterRole) — scoped to
observabilitynamespace only getonly — nolistorwatch(which would expose all secrets)resourceNames— restricts tografana-credsonly- ServiceAccount subject — specific SA, not a group or user
| Previous | Up | Next |
|---|---|---|
| Secret Access Control | Secrets Management | Runtime Security |