CKS — CIS Remediation Strategies

10 minute read

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:

  1. Critical — Authentication/authorization bypass: Fix --anonymous-auth, --authorization-mode, kubelet anonymous auth first. These allow unauthenticated cluster access.

  2. 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.

  3. Medium — Information exposure and missing controls: Fix --profiling=false, --read-only-port=0, --protect-kernel-defaults=true. These leak internal data or weaken defenses.

  4. Low — File permissions: Fix chmod 600 and chown root:root issues. 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:

  1. Missing volume mount — The audit log directory must be mounted into the static Pod. You need both a volumeMount and a volume entry for /var/log/kubernetes/audit.

  2. Missing audit policy file — The --audit-log-path flag requires --audit-policy-file to also be set. Without an audit policy, the API server won’t start.

  3. 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 →

Updated: