CKS — CIS Remediation Strategies
Exam Context
| Property | Value |
|---|---|
| Domain | Domain 1 — Cluster Setup |
| Weight | 10% |
| Objective | Use CIS benchmark to review the security configuration of Kubernetes components |
Running kube-bench is the easy part. The real skill — and what the CKS exam tests — is fixing the failures. This topic provides a systematic, priority-based approach to remediating the most common kube-bench FAIL results across control plane and worker nodes.
Remediation Workflow
Every CIS benchmark fix follows the same five-step pattern:
1. IDENTIFY → Run kube-bench, note FAIL results
2. PRIORITIZE → Rank failures by security impact
3. FIX → Modify configuration (static Pod manifest or kubelet config)
4. VERIFY → Re-run the specific check
5. DOCUMENT → Record what was changed and why
CKS exam shortcut: On the exam, skip step 5. Focus on steps 1-4 as fast as possible. Fix the highest-impact failures first — authentication and authorization issues before file permissions.
Priority-Based Remediation
Not all FAIL results are equal. Use this priority matrix to decide what to fix first.
Priority Matrix
| Priority | Category | Risk | Examples | Fix Time |
|---|---|---|---|---|
| Critical | Authentication/Authorization bypass | Unauthenticated cluster access | anonymous-auth=true, authorization-mode=AlwaysAllow |
1-2 min |
| High | Missing audit trail | No forensic capability | audit-log-path not set |
3-5 min |
| High | TLS/encryption gaps | Data in transit exposed | etcd peer-auto-tls=true, missing TLS certs |
2-3 min |
| Medium | Information exposure | Internal data leaked | profiling=true, read-only-port=10255 |
1-2 min |
| Medium | Missing security controls | Weakened defense | protect-kernel-defaults=false, missing admission plugins |
1-2 min |
| Low | File permissions | Local privilege escalation | Manifest files with 644 instead of 600 | 1 min |
Top 10 FAIL Fixes: Step by Step
Fix 1: API Server — Disable Anonymous Authentication
Check: 1.2.1 — --anonymous-auth=false
Priority: Critical
File: /etc/kubernetes/manifests/kube-apiserver.yaml
# Check current state
grep 'anonymous-auth' /etc/kubernetes/manifests/kube-apiserver.yaml
If missing or set to true, add/modify:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --anonymous-auth=false
# ... keep all other existing flags
Verify:
# Wait for API server to restart (30-60 seconds)
kubectl get pods -n kube-system -l component=kube-apiserver -w
# Check the specific test
sudo kube-bench run --check 1.2.1
Fix 2: API Server — Set Authorization Mode
Check: 1.2.6 — --authorization-mode not AlwaysAllow and 1.2.7/1.2.8 — includes Node,RBAC
Priority: Critical
File: /etc/kubernetes/manifests/kube-apiserver.yaml
- --authorization-mode=Node,RBAC
What NOT to do:
# WRONG — AlwaysAllow bypasses all RBAC
- --authorization-mode=AlwaysAllow
# WRONG — Missing Node authorization
- --authorization-mode=RBAC
Verify:
sudo kube-bench run --check 1.2.6,1.2.7,1.2.8
Fix 3: API Server — Enable Audit Logging
Check: 1.2.19-1.2.22 — audit-log-path, maxage, maxbackup, maxsize
Priority: High
This fix requires multiple changes:
Step 1: Create audit directory and policy:
# Create directories
sudo mkdir -p /var/log/kubernetes/audit
sudo mkdir -p /etc/kubernetes/audit
Step 2: Create an audit policy file:
# /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log Secret access at Metadata level (don't log Secret content)
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
# Log configmap changes
- level: Metadata
resources:
- group: ""
resources: ["configmaps"]
# Log Pod creation/deletion at RequestResponse level
- level: RequestResponse
resources:
- group: ""
resources: ["pods"]
verbs: ["create", "delete", "patch"]
# Log RBAC changes
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "clusterroles", "rolebindings", "clusterrolebindings"]
# Log authentication events
- level: Metadata
resources:
- group: "authentication.k8s.io"
# Default: log at Metadata level
- level: Metadata
stages:
- ResponseComplete
sudo cp audit-policy.yaml /etc/kubernetes/audit/audit-policy.yaml
Step 3: Add audit flags and volumes to API server manifest:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
Add to the command section:
- --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
Add to volumeMounts:
volumeMounts:
- name: audit-log
mountPath: /var/log/kubernetes/audit
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
Add to volumes:
volumes:
- name: audit-log
hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
Verify:
# Wait for API server restart
kubectl get pods -n kube-system -l component=kube-apiserver -w
# Check audit log exists
sudo ls -la /var/log/kubernetes/audit/audit.log
# Verify checks
sudo kube-bench run --check 1.2.19,1.2.20,1.2.21,1.2.22
Fix 4: API Server — Disable Profiling
Check: 1.2.16 — --profiling=false
Priority: Medium
File: /etc/kubernetes/manifests/kube-apiserver.yaml
- --profiling=false
Fix 5: API Server — Enable Admission Plugins
Check: 1.2.17 — NodeRestriction admission plugin
Priority: Medium
File: /etc/kubernetes/manifests/kube-apiserver.yaml
- --enable-admission-plugins=NodeRestriction,PodSecurity
Fix 6: Controller Manager — Disable Profiling and Set Bind Address
Checks: 1.3.2 — --profiling=false and 1.3.7 — --bind-address=127.0.0.1
Priority: Medium
File: /etc/kubernetes/manifests/kube-controller-manager.yaml
sudo vi /etc/kubernetes/manifests/kube-controller-manager.yaml
spec:
containers:
- command:
- kube-controller-manager
- --profiling=false
- --bind-address=127.0.0.1
- --use-service-account-credentials=true
- --service-account-private-key-file=/etc/kubernetes/pki/sa.key
- --root-ca-file=/etc/kubernetes/pki/ca.crt
# ... other existing flags
Verify:
sudo kube-bench run --group 1.3
Fix 7: Scheduler — Disable Profiling and Set Bind Address
Checks: 1.4.1 — --profiling=false and 1.4.2 — --bind-address=127.0.0.1
Priority: Medium
File: /etc/kubernetes/manifests/kube-scheduler.yaml
spec:
containers:
- command:
- kube-scheduler
- --profiling=false
- --bind-address=127.0.0.1
# ... other existing flags
Verify:
sudo kube-bench run --group 1.4
Fix 8: Kubelet — Disable Anonymous Auth and Read-Only Port
Checks: 4.2.1 — anonymous-auth=false and 4.2.4 — readOnlyPort=0
Priority: Critical (anonymous auth) + Medium (read-only port)
File: /var/lib/kubelet/config.yaml
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
readOnlyPort: 0
Restart and verify:
sudo systemctl restart kubelet
sudo kube-bench run --check 4.2.1,4.2.4
Fix 9: Kubelet — Protect Kernel Defaults
Check: 4.2.6 — protectKernelDefaults=true
Priority: Medium
First, set required kernel parameters:
sudo sysctl -w vm.overcommit_memory=1
sudo sysctl -w vm.panic_on_oom=0
sudo sysctl -w kernel.panic=10
sudo sysctl -w kernel.panic_on_oops=1
# Make persistent
cat <<EOF | sudo tee /etc/sysctl.d/90-kubelet.conf
vm.overcommit_memory=1
vm.panic_on_oom=0
kernel.panic=10
kernel.panic_on_oops=1
EOF
sudo sysctl --system
Then enable in kubelet config:
protectKernelDefaults: true
sudo systemctl restart kubelet
sudo kube-bench run --check 4.2.6
Fix 10: File Permissions
Checks: 1.1.x (control plane) and 4.1.x (worker node)
Priority: Low
Control plane file permissions:
# API server manifest
sudo chmod 600 /etc/kubernetes/manifests/kube-apiserver.yaml
sudo chown root:root /etc/kubernetes/manifests/kube-apiserver.yaml
# Controller manager manifest
sudo chmod 600 /etc/kubernetes/manifests/kube-controller-manager.yaml
sudo chown root:root /etc/kubernetes/manifests/kube-controller-manager.yaml
# Scheduler manifest
sudo chmod 600 /etc/kubernetes/manifests/kube-scheduler.yaml
sudo chown root:root /etc/kubernetes/manifests/kube-scheduler.yaml
# etcd manifest
sudo chmod 600 /etc/kubernetes/manifests/etcd.yaml
sudo chown root:root /etc/kubernetes/manifests/etcd.yaml
# Admin kubeconfig
sudo chmod 600 /etc/kubernetes/admin.conf
sudo chown root:root /etc/kubernetes/admin.conf
# Scheduler kubeconfig
sudo chmod 600 /etc/kubernetes/scheduler.conf
sudo chown root:root /etc/kubernetes/scheduler.conf
# Controller manager kubeconfig
sudo chmod 600 /etc/kubernetes/controller-manager.conf
sudo chown root:root /etc/kubernetes/controller-manager.conf
# PKI files
sudo chmod 600 /etc/kubernetes/pki/*.key
sudo chown root:root /etc/kubernetes/pki/*.key
Worker node file permissions:
# kubelet kubeconfig
sudo chmod 600 /etc/kubernetes/kubelet.conf
sudo chown root:root /etc/kubernetes/kubelet.conf
# kubelet config
sudo chmod 600 /var/lib/kubelet/config.yaml
sudo chown root:root /var/lib/kubelet/config.yaml
# kubelet service file
KUBELET_SVC=$(systemctl show -p FragmentPath kubelet | cut -d= -f2)
sudo chmod 600 "$KUBELET_SVC"
sudo chown root:root "$KUBELET_SVC"
Verify:
# Control plane
sudo kube-bench run --group 1.1
# Worker
sudo kube-bench run --group 4.1
etcd Encryption and Peer TLS
Enabling etcd Encryption at Rest
etcd stores Secrets in plaintext by default. Encryption at rest protects Secret data if etcd storage is compromised.
Step 1: Create an encryption configuration:
# /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: $(head -c 32 /dev/urandom | base64)
- identity: {}
Generate the key:
sudo mkdir -p /etc/kubernetes/enc
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
cat <<EOF | sudo tee /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: ${ENCRYPTION_KEY}
- identity: {}
EOF
sudo chmod 600 /etc/kubernetes/enc/encryption-config.yaml
Step 2: Add to API server manifest:
- --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
Add the volume mount:
volumeMounts:
- name: enc-config
mountPath: /etc/kubernetes/enc
readOnly: true
volumes:
- name: enc-config
hostPath:
path: /etc/kubernetes/enc
type: DirectoryOrCreate
Step 3: Re-encrypt existing Secrets:
# After API server restarts, re-create all secrets to encrypt them
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
Step 4: Verify encryption:
# Read a Secret directly from etcd
sudo 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/my-secret | hexdump -C
# If encrypted, you'll see 'k8s:enc:aescbc:v1:key1' prefix instead of plaintext
Peer TLS Configuration
Ensure etcd peer communication uses proper TLS:
# In /etc/kubernetes/manifests/etcd.yaml
- --peer-auto-tls=false
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
- --peer-client-cert-auth=true
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
Remediation Tracking Table
Use this table to track your remediation progress:
| Check ID | Description | Status | Priority | Fix Applied | Verified |
|---|---|---|---|---|---|
| 1.2.1 | API server anonymous auth | FAIL → PASS | Critical | --anonymous-auth=false |
kube-bench --check 1.2.1 |
| 1.2.6 | Authorization mode | FAIL → PASS | Critical | --authorization-mode=Node,RBAC |
kube-bench --check 1.2.6 |
| 1.2.16 | API server profiling | FAIL → PASS | Medium | --profiling=false |
kube-bench --check 1.2.16 |
| 1.2.19 | Audit log path | FAIL → PASS | High | --audit-log-path=... |
kube-bench --check 1.2.19 |
| 1.2.20 | Audit log maxage | FAIL → PASS | High | --audit-log-maxage=30 |
kube-bench --check 1.2.20 |
| 1.3.2 | CM profiling | FAIL → PASS | Medium | --profiling=false |
kube-bench --check 1.3.2 |
| 1.3.7 | CM bind address | FAIL → PASS | Medium | --bind-address=127.0.0.1 |
kube-bench --check 1.3.7 |
| 1.4.1 | Scheduler profiling | FAIL → PASS | Medium | --profiling=false |
kube-bench --check 1.4.1 |
| 4.2.1 | Kubelet anonymous auth | FAIL → PASS | Critical | anonymous.enabled: false |
kube-bench --check 4.2.1 |
| 4.2.4 | Kubelet read-only port | FAIL → PASS | Medium | readOnlyPort: 0 |
kube-bench --check 4.2.4 |
Verification After Remediation
After applying all fixes, run a complete re-scan to verify:
# Full master scan
sudo kube-bench run --targets master
# Full etcd scan
sudo kube-bench run --targets etcd
# Full node scan (on worker)
sudo kube-bench run --targets node
Expected improvement:
Before: 38 PASS | 9 FAIL | 2 WARN
After: 47 PASS | 0 FAIL | 2 WARN
The WARN results typically require manual verification or organizational decisions (like EventRateLimit) — they’re acceptable to leave as WARN with documented justification.
Continuous Compliance
For production, automate kube-bench scans:
# Create a CronJob to run kube-bench weekly
apiVersion: batch/v1
kind: CronJob
metadata:
name: kube-bench-weekly
namespace: security
spec:
schedule: "0 2 * * 0" # Every Sunday at 2 AM
jobTemplate:
spec:
template:
spec:
hostPID: true
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: kube-bench
image: aquasec/kube-bench:v0.8.0
command: ["kube-bench", "run", "--targets", "master", "--json"]
volumeMounts:
- name: etc-kubernetes
mountPath: /etc/kubernetes
readOnly: true
- name: var-lib-kubelet
mountPath: /var/lib/kubelet
readOnly: true
restartPolicy: Never
volumes:
- name: etc-kubernetes
hostPath:
path: /etc/kubernetes
- name: var-lib-kubelet
hostPath:
path: /var/lib/kubelet
backoffLimit: 0
Practice Questions
Q1: You run kube-bench and see 12 FAIL results. In what order should you address them?
Show Answer
Use priority-based remediation:
-
Critical — Authentication/authorization bypass: Fix
--anonymous-auth,--authorization-mode, kubelet anonymous auth first. These allow unauthenticated cluster access. -
High — Missing audit trail and TLS gaps: Fix
--audit-log-path, etcd--peer-auto-tls, missing TLS certificates. These affect incident response capability and data protection. -
Medium — Information exposure and missing controls: Fix
--profiling=false,--read-only-port=0,--protect-kernel-defaults=true. These leak internal data or weaken defenses. -
Low — File permissions: Fix
chmod 600andchown root:rootissues. These require local access to exploit and are lowest risk.
Q2: After adding --audit-log-path to the API server, the kube-apiserver Pod fails to start. What’s the most likely cause?
Show Answer
The most likely causes are:
-
Missing volume mount — The audit log directory must be mounted into the static Pod. You need both a
volumeMountand avolumeentry for/var/log/kubernetes/audit. -
Missing audit policy file — The
--audit-log-pathflag requires--audit-policy-fileto also be set. Without an audit policy, the API server won’t start. -
Directory doesn’t exist — The host path must exist or use
type: DirectoryOrCreate.
Fix:
# Ensure all three flags are present
- --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30
# Ensure volumes are mounted
volumeMounts:
- name: audit-log
mountPath: /var/log/kubernetes/audit
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
volumes:
- name: audit-log
hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
Q3: Write the complete fix for kubelet to pass checks 4.2.1, 4.2.2, and 4.2.4.
Show Answer
Edit /var/lib/kubelet/config.yaml:
# Fix 4.2.1: Disable anonymous authentication
authentication:
anonymous:
enabled: false
webhook:
enabled: true
cacheTTL: 0s
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
# Fix 4.2.2: Use Webhook authorization
authorization:
mode: Webhook
webhook:
cacheAuthorizedTTL: 0s
cacheUnauthorizedTTL: 0s
# Fix 4.2.4: Disable read-only port
readOnlyPort: 0
Then:
sudo systemctl restart kubelet
sudo systemctl status kubelet
sudo kube-bench run --check 4.2.1,4.2.2,4.2.4
All three should show [PASS].
Q4: Explain how to enable etcd encryption at rest. What three changes are needed?
Show Answer
Three changes are required:
1. Create an EncryptionConfiguration file:
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
cat <<EOF | sudo tee /etc/kubernetes/enc/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: ${ENCRYPTION_KEY}
- identity: {}
EOF
2. Add the flag and volume to the API server manifest:
- --encryption-provider-config=/etc/kubernetes/enc/encryption-config.yaml
Plus the necessary volumeMount and volume for /etc/kubernetes/enc.
3. Re-encrypt existing Secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
This ensures Secrets created before encryption was enabled are also encrypted.
Q5: A kube-bench re-scan after remediation shows 0 FAIL but 3 WARN results. Is this acceptable for production?
Show Answer
It depends on the specific WARN results. WARN means kube-bench couldn’t automatically verify the check — it may pass or fail depending on context.
Common WARN results and their handling:
| WARN Check | Typical Action |
|---|---|
| EventRateLimit admission plugin | Accept if you have alternative rate limiting (e.g., API Priority and Fairness) |
| AlwaysPullImages plugin | Accept if your registry is private and secured |
| TLS cipher suites | Fix by configuring tlsCipherSuites in kubelet config |
For production:
- Review each WARN to determine if it’s a real risk
- Document justifications for accepted WARNs
- Fix where practical — configure cipher suites, add rate limiting
- 0 FAIL + documented WARNs is an acceptable security posture for most compliance frameworks
Next: Put it all together in the Hands-On Lab →
| ← Scanning Worker Nodes | Hands-On Lab → |