Fix CIS benchmark findings with kube-bench (CKS)
Fix CIS benchmark findings with kube-bench (CKS) Lesson five of the CKS series: the CIS benchmark, with kube-bench. We will run it on a control plane and a worker, read what it tells us, and fix three specific findings
Fix CIS benchmark findings with kube-bench (CKS)
Lesson five of the CKS series: the CIS benchmark, with kube-bench. We will run it on a control plane and a worker, read what it tells us, and fix three specific findings, re-checking each one. And for the kubelet we will not just trust the checklist; we will see what the insecure setting actually allowed.
This is a CKS Cluster Setup walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).
The scenario
The setup. A two-node cluster, with kube-bench already installed on both nodes. On the control plane, fix check one point four point one, scheduler profiling. On the worker, fix four point one point nine, the permissions of the kubelet config file, and four point two point one, kubelet anonymous authentication. Use the CIS one point twelve benchmark.
- Two nodes, kube-bench installed on both (benchmark cis-1.12)
- Control plane: 1.4.1 kube-scheduler --profiling
- Worker: 4.1.9 kubelet config.yaml permissions
- Worker: 4.2.1 kubelet anonymous auth
How kube-bench works
kube-bench runs the CIS Kubernetes Benchmark on the node you are standing on. The --targets flag picks the section: master for control plane checks, node for kubelet checks, so you run it on each node separately. Every check has a number and prints PASS, FAIL or WARN, and every failure comes with a remediation that names the exact file to change. --check narrows the run to one or more numbers, which is how you confirm a fix. Control plane fixes land in static pod manifests; worker fixes land in the kubelet's files, followed by a kubelet restart.
Control plane findings
Start on the control plane. Run kube-bench against the master target and keep only the FAIL lines. Ten failures on a stock kubeadm cluster, which is normal. The task names one of them, one point four point one, the last line: the scheduler's profiling argument. Resist the urge to fix everything; the grader checks what was asked, and every extra change is a chance to break a component.
$ ssh cks-scenario5-control-plane
cks-scenario5-control-plane
$ kube-bench run --benchmark cis-1.12 --targets master | grep '^\[FAIL\]'
[FAIL] 1.1.12 Ensure that the etcd data directory ownership is set to etcd:etcd (Automated
[FAIL] 1.2.5 Ensure that the --kubelet-certificate-authority argument is set as appropriat
[FAIL] 1.2.15 Ensure that the --profiling argument is set to false (Automated)
[FAIL] 1.2.16 Ensure that the --audit-log-path argument is set (Automated)
[FAIL] 1.2.17 Ensure that the --audit-log-maxage argument is set to 30 or as appropriate (
[FAIL] 1.2.18 Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriat
[FAIL] 1.2.19 Ensure that the --audit-log-maxsize argument is set to 100 or as appropriate
[FAIL] 1.2.30 Ensure that the --service-account-extend-token-expiration parameter is set t
[FAIL] 1.3.2 Ensure that the --profiling argument is set to false (Automated)
[FAIL] 1.4.1 Ensure that the --profiling argument is set to false (Automated)
Read the remediation
Narrow it down with --check one point four point one. Now we get the remediation: edit the scheduler pod specification, kube-scheduler dot yaml in /etc/kubernetes/manifests, and set --profiling=false. Profiling exposes a debug endpoint with performance data about the component, which you do not want available in production.
$ kube-bench run --benchmark cis-1.12 --targets master --check 1.4.1
[FAIL] 1.4.1 Ensure that the --profiling argument is set to false (Automated)
== Remediations master ==
1.4.1 Edit the Scheduler pod specification file /etc/kubernetes/manifests/kube-scheduler.yaml file
on the control plane node and set the below parameter.
--profiling=false
Disable scheduler profiling
Open the scheduler manifest in vi, go to the end of the command list and add one line: --profiling=false, lined up with the other flags. Save with colon w q. The kubelet notices the file changed and restarts the scheduler on its own.
$ sed -n '/- kube-scheduler/,/image:/p' /etc/kubernetes/manifests/kube-scheduler.yaml
- kube-scheduler
- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
- --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
- --bind-address=127.0.0.1
- --kubeconfig=/etc/kubernetes/scheduler.conf
- --leader-elect=true
image: registry.k8s.io/kube-scheduler:v1.37.0
$ sed -n '/- kube-scheduler/,/image:/p' /etc/kubernetes/manifests/kube-scheduler.yaml
- kube-scheduler
- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
- --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
- --bind-address=127.0.0.1
- --kubeconfig=/etc/kubernetes/scheduler.conf
- --leader-elect=true
- --profiling=false
image: registry.k8s.io/kube-scheduler:v1.37.0
1.4.1 passes
Give the scheduler a few seconds to come back, then rerun just that check. One point four point one, PASS. That is the pattern for every finding: read, fix, recheck.
$ kube-bench run --benchmark cis-1.12 --targets master --check 1.4.1 | grep '^\['
[PASS] 1.4.1 Ensure that the --profiling argument is set to false (Automated)
Worker findings
Now the worker. Same command, target node. Three failures: four point one point one, which this newer benchmark flags and the task does not mention, so we leave it; and our two, four point one point nine and four point two point one. The config file is mode six six six, readable and writable by every user on the node. And here is what anonymous auth means in practice: a request to the kubelet API with no credentials at all is accepted as the user system:anonymous. It only gets Forbidden because authorization happens to deny that user. One RBAC mistake away from reading every pod on this node.
$ ssh cks-scenario5-worker
cks-scenario5-worker
$ kube-bench run --benchmark cis-1.12 --targets node | grep '^\[FAIL\]'
[FAIL] 4.1.1 Ensure that the kubelet service file permissions are set to 600 or more restr
[FAIL] 4.1.9 If the kubelet config.yaml configuration file is being used validate permissi
[FAIL] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Automated)
$ stat -c '%a %U:%G %n' /var/lib/kubelet/config.yaml
666 root:root /var/lib/kubelet/config.yaml
$ curl -sk https://localhost:10250/pods; echo
Forbidden (user=system:anonymous, verb=get, resource=nodes, subresource(s)=[pods proxy])
Fix the file permissions
Four point one point nine is one command. chmod six hundred: read and write for root, nothing for anyone else. stat confirms it. This benchmark asks for six hundred; older ones accepted six four four, so always read the remediation for the version you are running.
$ chmod 600 /var/lib/kubelet/config.yaml && stat -c '%a %U:%G %n' /var/lib/kubelet/config.yaml
600 root:root /var/lib/kubelet/config.yaml
Turn off anonymous auth
Four point two point one lives in that same file. Open /var/lib/kubelet/config.yaml, and under authentication, anonymous, change enabled from true to false. Careful: there is a second enabled line just below it, for webhook authentication, and that one must stay true. Save, then restart the kubelet with systemctl, because unlike static pods the kubelet does not reload its own config.
$ grep -n '' /var/lib/kubelet/config.yaml | head -n 9
1:apiVersion: kubelet.config.k8s.io/v1beta1
2:authentication:
3: anonymous:
4: enabled: true
5: webhook:
6: cacheTTL: 0s
7: enabled: true
8: x509:
9: clientCAFile: /etc/kubernetes/pki/ca.crt
Verify the worker
The same anonymous request now gets Unauthorized. It is rejected at authentication and never reaches the authorization check, which is what defense in depth looks like. And kube-bench agrees: four point one point nine and four point two point one, both PASS.
$ curl -sk https://localhost:10250/pods; echo
Unauthorized
$ kube-bench run --benchmark cis-1.12 --targets node --check 4.1.9,4.2.1 | grep '^\['
[PASS] 4.1.9 If the kubelet config.yaml configuration file is being used validate permissi
[PASS] 4.2.1 Ensure that the --anonymous-auth argument is set to false (Automated)
Exam tips
Tips for the exam. Run kube-bench on the node the check belongs to: master checks on the control plane, node checks on workers. Use --check to get just the remediation you need, instead of scrolling hundreds of lines. Fix only the numbered checks in the task. Back up a manifest outside the manifests folder before editing it. Restart the kubelet after changing its config file; static pods restart themselves. And rerun the check every time, because PASS is the only proof.
- master checks on the control plane, node checks on workers
- --check for just the remediation you need
- Fix only the numbered checks the task names
- Kubelet config changed? systemctl restart kubelet
- Rerun the check: PASS is the only proof
Recap
- 1.4.1: --profiling=false in kube-scheduler.yaml
- 4.1.9: chmod 600 /var/lib/kubelet/config.yaml
- 4.2.1: anonymous.enabled false + restart kubelet
- Recheck each to PASS; subscribe + dev.to writeup
Reproduce this yourself
The entire scenario is scripted on a throwaway kind cluster: https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios
git clone https://github.com/The-Cyber-Sidekick/TCS_CKS_2026_Exam_Scenarios.git
cd TCS_CKS_2026_Exam_Scenarios/scenario5-kube-bench-cis
./setup.sh # creates the cluster AND arms the scenario
# solve it by hand, or:
./solution.sh # apply the answer key and verify
If this helped, subscribe to The Cyber SideKick on YouTube for more CKS drills, and grab the newsletter at https://thecybersidekick.beehiiv.com.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.