Objective
The CIS Kubernetes Benchmark defines security configuration standards for control plane, etcd, kubelet, and policies. kube-bench automates these checks and produces a structured report. This exercise runs kube-bench as a Kubernetes Job, parses the JSON output, and produces a categorised report with a compliance score — the foundation of a recurring security posture measurement workflow.
Prerequisites
- A running Kubernetes cluster (EKS, AKS, GKE, or kubeadm)
- kubectl with cluster-admin permissions
- Python 3.9+ (for the parsing script)
- jq installed (for quick ad-hoc queries)
Steps
Run kube-bench as a Kubernetes Job
The cleanest approach is running kube-bench as a Job on each node type (master/worker). The official Kubernetes Job manifests handle node targeting via tolerations and host path mounts.
# Run against worker nodes (node benchmark) kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml # Watch the job complete kubectl wait --for=condition=complete job/kube-bench --timeout=120s # Retrieve output kubectl logs job/kube-bench ## [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 ... ## [FAIL] 4.1.5 Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 ... ## ... ## == Summary == ## 38 checks PASS ## 12 checks FAIL ## 4 checks WARN ## 0 checks INFO kubectl delete job kube-bench
Export results as JSON
# Use the JSON output job variant kubectl apply -f - <<EOF apiVersion: batch/v1 kind: Job metadata: name: kube-bench-json spec: template: spec: hostPID: true containers: - name: kube-bench image: aquasec/kube-bench:latest command: ["kube-bench", "--json", "--outputfile", "/tmp/results.json"] volumeMounts: - name: tmp mountPath: /tmp restartPolicy: Never volumes: - name: tmp emptyDir: {} EOF kubectl wait --for=condition=complete job/kube-bench-json --timeout=120s # Copy the JSON out of the pod POD=$(kubectl get pods -l job-name=kube-bench-json -o jsonpath='{.items[0].metadata.name}') kubectl cp ${POD}:/tmp/results.json kube-bench-results.json # Preview structure with jq jq '.Controls[0] | {version, text, node_type}' kube-bench-results.json ## { "version": "1.8", "text": "Kubernetes Worker Node Security", "node_type": ["node"] } kubectl delete job kube-bench-json
Parse and categorise failures with Python
import json, sys from collections import defaultdict with open("kube-bench-results.json") as f: data = json.load(f) totals = defaultdict(int) failures_by_section = defaultdict(list) for control in data.get("Controls", []): section_name = control.get("text", "Unknown") for group in control.get("tests", []): for result in group.get("results", []): status = result.get("status", "UNKNOWN") totals[status] += 1 if status == "FAIL": failures_by_section[section_name].append({ "id": result.get("test_number"), "desc": result.get("test_desc"), "scored": result.get("scored", True), "remediation": result.get("remediation", "").strip()[:120], }) total_checks = sum(totals.values()) passed = totals.get("PASS", 0) score = (passed / total_checks * 100) if total_checks > 0 else 0 print(f"\n=== CIS Kubernetes Benchmark Report ===\n") print(f"Compliance Score : {score:.1f}% ({passed}/{total_checks} checks passing)") print(f"PASS: {totals['PASS']} FAIL: {totals['FAIL']} WARN: {totals['WARN']}\n") for section, failures in sorted(failures_by_section.items()): print(f"── {section} ({len(failures)} failures)") for f in failures: scored_tag = "[SCORED]" if f["scored"] else "[NOT SCORED]" print(f" {f['id']} {scored_tag} {f['desc']}") if f["remediation"]: print(f" Remediation: {f['remediation']}...") print()
python3 parse_kube_bench.py ## === CIS Kubernetes Benchmark Report === ## ## Compliance Score : 76.0% (38/50 checks passing) ## PASS: 38 FAIL: 12 WARN: 4 ## ## ── Kubernetes Worker Node Security (8 failures) ## 4.1.5 [SCORED] Ensure kubelet.conf permissions are 600 ... ## Remediation: Run: chmod 600 /etc/kubernetes/kubelet.conf... ## 4.2.1 [SCORED] Ensure anonymous-auth is false ... ## Remediation: Edit kubelet config: set authentication.anonymous.enabled: false...
Identify the top 5 highest-priority remediations
Prioritise scored failures that are exploitable at the network level. These five are the most common and highest-impact on a default cluster:
# 1. Anonymous authentication on kubelet (4.2.1) # Edit kubelet config: /var/lib/kubelet/config.yaml # authentication: # anonymous: # enabled: false # 2. Kubelet read-only port (4.2.4) # readOnlyPort: 0 # disable the unauthenticated port 10255 # 3. Audit logging not enabled (1.2.22 — if kubeadm) # Add to kube-apiserver: --audit-policy-file=/etc/kubernetes/audit-policy.yaml # 4. Etcd not encrypted at rest (1.2.33) # Add to kube-apiserver: --encryption-provider-config=/etc/kubernetes/enc.yaml # 5. AlwaysPullImages admission controller not enabled (1.2.11) # Add to kube-apiserver: --enable-admission-plugins=...,AlwaysPullImages # Restart kubelet after config changes sudo systemctl daemon-reload && sudo systemctl restart kubelet # Re-run kube-bench to measure improvement kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml kubectl wait --for=condition=complete job/kube-bench --timeout=120s kubectl logs job/kube-bench | tail -10
Success Criteria
Further Reading
- kube-bench: github.com/aquasecurity/kube-bench
- CIS Kubernetes Benchmark: cisecurity.org — free download with registration
- Kubescape: github.com/kubescape/kubescape — broader framework coverage (NSA/CISA, MITRE)
- AKS CIS benchmark guidance: learn.microsoft.com — Azure Policy for AKS CIS