CKS — Scanning Worker Nodes

9 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

Worker nodes run your actual workloads. A misconfigured kubelet can expose container exec access, leak Pod metadata, or allow unauthorized reads of Secrets. This topic covers every major kube-bench check for worker nodes and the exact remediation steps.

Running the Worker Node Scan

SSH into a worker node and run:

# Full worker node scan
sudo kube-bench run --targets node

Or as a Kubernetes Job:

kubectl apply -f kube-bench-node-job.yaml
kubectl wait --for=condition=complete job/kube-bench-node --timeout=120s
kubectl logs job/kube-bench-node

The scan covers two main sections:

  1. Section 4.1 — Worker Node Configuration Files
  2. Section 4.2 — Kubelet

Section 4.1: Worker Node Configuration Files

These checks verify that kubelet-related files have restrictive permissions and proper ownership. File permission issues are among the most common FAIL results on worker nodes.

kubelet Service File

[FAIL] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restrictive
[FAIL] 4.1.2 Ensure that the kubelet service file ownership is set to root:root

What it checks: The systemd service file for kubelet.

Audit:

# Find the kubelet service file
systemctl show -p FragmentPath kubelet

# Common location
stat -c '%a %U:%G' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

Expected output (secure):

600 root:root

Remediation:

# Fix permissions
sudo chmod 600 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

# Fix ownership
sudo chown root:root /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

# Verify
stat -c '%a %U:%G' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

kubelet.conf (kubeconfig)

[FAIL] 4.1.5 Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 or more restrictive
[FAIL] 4.1.6 Ensure that the --kubeconfig kubelet.conf file ownership is set to root:root

Audit:

stat -c '%a %U:%G' /etc/kubernetes/kubelet.conf

Remediation:

sudo chmod 600 /etc/kubernetes/kubelet.conf
sudo chown root:root /etc/kubernetes/kubelet.conf

kubelet config.yaml

[FAIL] 4.1.9 If the kubelet config.yaml configuration file is being used, validate permissions set to 600 or more restrictive
[FAIL] 4.1.10 If the kubelet config.yaml configuration file is being used, ensure ownership is set to root:root

Audit:

stat -c '%a %U:%G' /var/lib/kubelet/config.yaml

Remediation:

sudo chmod 600 /var/lib/kubelet/config.yaml
sudo chown root:root /var/lib/kubelet/config.yaml

Complete File Permission Remediation Script

For convenience, here’s a script that fixes all common file permission issues on a worker node:

#!/bin/bash
# fix-worker-file-permissions.sh
# Fixes CIS benchmark Section 4.1 file permission issues

echo "Fixing kubelet service file permissions..."
KUBELET_SVC=$(systemctl show -p FragmentPath kubelet | cut -d= -f2)
if [ -f "$KUBELET_SVC" ]; then
  sudo chmod 600 "$KUBELET_SVC"
  sudo chown root:root "$KUBELET_SVC"
  echo "Fixed: $KUBELET_SVC"
fi

# Fix kubeadm drop-in
KUBEADM_DROPIN="/etc/systemd/system/kubelet.service.d/10-kubeadm.conf"
if [ -f "$KUBEADM_DROPIN" ]; then
  sudo chmod 600 "$KUBEADM_DROPIN"
  sudo chown root:root "$KUBEADM_DROPIN"
  echo "Fixed: $KUBEADM_DROPIN"
fi

echo "Fixing kubelet.conf permissions..."
if [ -f /etc/kubernetes/kubelet.conf ]; then
  sudo chmod 600 /etc/kubernetes/kubelet.conf
  sudo chown root:root /etc/kubernetes/kubelet.conf
  echo "Fixed: /etc/kubernetes/kubelet.conf"
fi

echo "Fixing kubelet config.yaml permissions..."
if [ -f /var/lib/kubelet/config.yaml ]; then
  sudo chmod 600 /var/lib/kubelet/config.yaml
  sudo chown root:root /var/lib/kubelet/config.yaml
  echo "Fixed: /var/lib/kubelet/config.yaml"
fi

echo "Fixing PKI directory permissions..."
sudo chmod -R 600 /etc/kubernetes/pki/*.crt 2>/dev/null
sudo chmod -R 600 /etc/kubernetes/pki/*.key 2>/dev/null
sudo chown -R root:root /etc/kubernetes/pki/ 2>/dev/null
echo "Fixed: /etc/kubernetes/pki/"

echo "All file permission fixes applied."
echo "Run 'sudo kube-bench run --group 4.1' to verify."

Section 4.2: Kubelet Configuration

Kubelet configuration checks are the most security-critical worker node checks. The kubelet is the agent that runs containers — misconfigured kubelet settings directly expose workloads.

Anonymous Authentication

[FAIL] 4.2.1 Ensure that the --anonymous-auth argument is set to false

What it checks: Whether the kubelet API accepts unauthenticated requests.

Why it matters: The kubelet API (port 10250) provides endpoints for:

  • Reading Pod status and logs
  • Executing commands inside containers (/exec)
  • Port-forwarding to containers

With anonymous auth enabled, anyone who can reach port 10250 can do all of the above without authentication.

Audit:

# Check running process
ps -ef | grep kubelet | grep -o '\-\-anonymous-auth=[a-z]*'

# Check config file
grep -A3 'authentication:' /var/lib/kubelet/config.yaml

Remediation in /var/lib/kubelet/config.yaml:

authentication:
  anonymous:
    enabled: false
  webhook:
    enabled: true
    cacheTTL: 0s
  x509:
    clientCAFile: /etc/kubernetes/pki/ca.crt

Restart kubelet:

sudo systemctl restart kubelet
sudo systemctl status kubelet

Verify:

sudo kube-bench run --check 4.2.1

Webhook Authentication

[FAIL] 4.2.2 Ensure that the --authorization-mode argument is not set to AlwaysAllow

What it checks: Whether the kubelet authorizes API requests through the API server (Webhook mode) rather than allowing everything.

Why it matters: With AlwaysAllow, any authenticated request to the kubelet is approved regardless of RBAC. Webhook mode delegates authorization to the API server, enforcing the same RBAC rules.

Remediation in /var/lib/kubelet/config.yaml:

authorization:
  mode: Webhook
  webhook:
    cacheAuthorizedTTL: 0s
    cacheUnauthorizedTTL: 0s

Read-Only Port

[FAIL] 4.2.4 Ensure that the --read-only-port argument is set to 0

What it checks: Whether the kubelet exposes a read-only API on port 10255.

Why it matters: Port 10255 provides unauthenticated, unencrypted access to kubelet metrics and Pod information. This is a significant information leak:

# What an attacker can do with the read-only port
curl http://<node-ip>:10255/pods          # List all Pods with full spec (env vars, volumes)
curl http://<node-ip>:10255/spec          # Node hardware spec
curl http://<node-ip>:10255/metrics       # Node and container metrics

Audit:

# Check if port 10255 is listening
ss -tulnp | grep 10255

# Check config
grep 'readOnlyPort' /var/lib/kubelet/config.yaml

Remediation in /var/lib/kubelet/config.yaml:

readOnlyPort: 0

Then restart kubelet:

sudo systemctl restart kubelet

# Verify port is no longer listening
ss -tulnp | grep 10255
# Should return empty

Protect Kernel Defaults

[FAIL] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to true

What it checks: Whether the kubelet enforces kernel parameter requirements rather than silently modifying them.

Why it matters: Some workloads require specific sysctl settings. Without --protect-kernel-defaults=true, the kubelet may adjust kernel parameters to meet container requirements — silently weakening the node’s security posture. With this flag, the kubelet refuses to start if the required kernel parameters are not already set correctly.

Audit:

grep 'protectKernelDefaults' /var/lib/kubelet/config.yaml

Remediation in /var/lib/kubelet/config.yaml:

protectKernelDefaults: true

Important: Before enabling this, ensure your kernel parameters are already set correctly. Otherwise kubelet will fail to start.

# Check current kernel parameters
sysctl vm.overcommit_memory
sysctl vm.panic_on_oom
sysctl kernel.panic
sysctl kernel.panic_on_oops

# Set them if needed
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
echo "vm.overcommit_memory=1" | sudo tee -a /etc/sysctl.d/90-kubelet.conf
echo "vm.panic_on_oom=0" | sudo tee -a /etc/sysctl.d/90-kubelet.conf
echo "kernel.panic=10" | sudo tee -a /etc/sysctl.d/90-kubelet.conf
echo "kernel.panic_on_oops=1" | sudo tee -a /etc/sysctl.d/90-kubelet.conf
sudo sysctl --system

Then restart kubelet:

sudo systemctl restart kubelet

TLS Configuration

[FAIL] 4.2.10 Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate

What it checks: Whether the kubelet HTTPS endpoint uses proper TLS certificates.

Remediation in /var/lib/kubelet/config.yaml:

tlsCertFile: /var/lib/kubelet/pki/kubelet.crt
tlsPrivateKeyFile: /var/lib/kubelet/pki/kubelet.key

Certificate Rotation

[PASS] 4.2.11 Ensure that the --rotate-certificates argument is not set to false
[PASS] 4.2.12 Verify that the RotateKubeletServerCertificate argument is set to true

What it checks: Whether kubelet automatically rotates its client and server certificates before they expire.

Audit:

grep -E 'rotateCertificates|serverTLSBootstrap' /var/lib/kubelet/config.yaml

Expected configuration:

rotateCertificates: true
serverTLSBootstrap: true

kubeadm enables certificate rotation by default.

Strong Cryptographic Ciphers

[WARN] 4.2.13 Ensure that the Kubelet only makes use of Strong Cryptographic Ciphers

Remediation in /var/lib/kubelet/config.yaml:

tlsCipherSuites:
  - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
  - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
  - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
  - TLS_RSA_WITH_AES_256_GCM_SHA384
  - TLS_RSA_WITH_AES_128_GCM_SHA256

Complete kubelet config.yaml (Hardened)

Here is a complete hardened kubelet configuration file that passes all CIS Level 1 and most Level 2 checks:

# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration

# Authentication
authentication:
  anonymous:
    enabled: false            # 4.2.1: Disable anonymous auth
  webhook:
    enabled: true             # Use API server for auth
    cacheTTL: 0s
  x509:
    clientCAFile: /etc/kubernetes/pki/ca.crt

# Authorization
authorization:
  mode: Webhook               # 4.2.2: Webhook authorization
  webhook:
    cacheAuthorizedTTL: 0s
    cacheUnauthorizedTTL: 0s

# TLS
tlsCertFile: /var/lib/kubelet/pki/kubelet.crt    # 4.2.10
tlsPrivateKeyFile: /var/lib/kubelet/pki/kubelet.key
tlsCipherSuites:              # 4.2.13: Strong ciphers
  - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
  - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
  - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
  - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
  - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
  - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384

# Certificate rotation
rotateCertificates: true       # 4.2.11
serverTLSBootstrap: true       # 4.2.12

# Security
readOnlyPort: 0               # 4.2.4: Disable read-only port
protectKernelDefaults: true    # 4.2.6: Protect kernel defaults
makeIPTablesUtilChains: true   # 4.2.7

# Cluster
clusterDomain: cluster.local
clusterDNS:
  - 10.96.0.10

# Resource management
cgroupDriver: systemd

File Permission Summary Table

File/Directory Expected Permission Expected Owner Check IDs
kubelet service file 600 root:root 4.1.1, 4.1.2
/etc/kubernetes/kubelet.conf 600 root:root 4.1.5, 4.1.6
/var/lib/kubelet/config.yaml 600 root:root 4.1.9, 4.1.10
/etc/kubernetes/pki/*.crt 600 or 644 root:root Varies
/etc/kubernetes/pki/*.key 600 root:root Varies
/var/lib/kubelet/ 750 root:root General best practice

Complete Sample Worker Node Output

[INFO] 4 Worker Node Security Configuration
[INFO] 4.1 Worker Node Configuration Files
[PASS] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restrictive
[PASS] 4.1.2 Ensure that the kubelet service file ownership is set to root:root
[PASS] 4.1.5 Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 or more restrictive
[PASS] 4.1.6 Ensure that the --kubeconfig kubelet.conf file ownership is set to root:root
[PASS] 4.1.9 If the kubelet config.yaml configuration file is being used, validate permissions set to 600 or more restrictive
[PASS] 4.1.10 If the kubelet config.yaml configuration file is being used, ensure ownership is set to root:root

[INFO] 4.2 Kubelet
[PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false
[PASS] 4.2.2 Ensure that the --authorization-mode argument is not set to AlwaysAllow
[PASS] 4.2.3 Ensure that the --client-ca-file argument is set as appropriate
[PASS] 4.2.4 Ensure that the --read-only-port argument is set to 0
[PASS] 4.2.6 Ensure that the --protect-kernel-defaults argument is set to true
[PASS] 4.2.7 Ensure that the --make-iptables-util-chains argument is set to true
[PASS] 4.2.10 Ensure that the --tls-cert-file and --tls-private-key-file arguments are set as appropriate
[PASS] 4.2.11 Ensure that the --rotate-certificates argument is not set to false
[PASS] 4.2.12 Verify that the RotateKubeletServerCertificate argument is set to true
[WARN] 4.2.13 Ensure that the Kubelet only makes use of Strong Cryptographic Ciphers

== Summary node ==
15 checks PASS
0 checks FAIL
1 checks WARN
0 checks INFO

Practice Questions

Q1: What command scans only worker node components with kube-bench?

Show Answer
sudo kube-bench run --targets node

This runs all checks in Section 4.1 (Worker Node Configuration Files) and Section 4.2 (Kubelet). It must be run on the worker node itself (or as a Kubernetes Job with nodeSelector targeting worker nodes).


Q2: Why is readOnlyPort: 0 critical in the kubelet configuration?

Show Answer

The kubelet read-only port (10255) provides unauthenticated, unencrypted HTTP access to:

  • /pods — Full Pod specifications including environment variables, volume mounts, and Secrets references
  • /spec — Node hardware specifications
  • /metrics — Node and container metrics

Setting readOnlyPort: 0 disables this port entirely. Any client needing kubelet data should use the authenticated HTTPS port (10250) instead.


Q3: Describe the complete remediation for kubelet anonymous authentication (check 4.2.1).

Show Answer
  1. Edit the kubelet configuration file:

    sudo vi /var/lib/kubelet/config.yaml
    
  2. Set the authentication section:

    authentication:
      anonymous:
        enabled: false
      webhook:
        enabled: true
      x509:
        clientCAFile: /etc/kubernetes/pki/ca.crt
    
  3. Restart kubelet:

    sudo systemctl restart kubelet
    
  4. Verify kubelet is running:

    sudo systemctl status kubelet
    
  5. Re-run the check:

    sudo kube-bench run --check 4.2.1
    

    Expected: [PASS] 4.2.1


Q4: What is the risk of not setting protectKernelDefaults: true, and what must you do before enabling it?

Show Answer

Risk: Without protectKernelDefaults: true, the kubelet may silently modify kernel parameters (sysctl values) to meet container requirements. This can weaken the node’s security posture without the administrator’s knowledge — for example, changing vm.overcommit_memory or kernel.panic settings.

Before enabling: Ensure the required kernel parameters are already correctly set:

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 them persistent in /etc/sysctl.d/. If these aren’t set correctly before enabling the flag, kubelet will fail to start.


Q5: Which file permission checks does kube-bench perform on worker nodes, and what is the expected permission for each?

Show Answer
Check File Expected Permission Expected Owner
4.1.1 kubelet service file 600 root:root
4.1.2 kubelet service file root:root ownership root:root
4.1.5 /etc/kubernetes/kubelet.conf 600 root:root
4.1.6 /etc/kubernetes/kubelet.conf root:root ownership root:root
4.1.9 /var/lib/kubelet/config.yaml 600 root:root
4.1.10 /var/lib/kubelet/config.yaml root:root ownership root:root

Fix all with:

sudo chmod 600 <file>
sudo chown root:root <file>

Next: Learn how to fix failures systematically with Remediation Strategies →


← Scanning Control Plane Remediation Strategies →

Updated: