kube-bench
Verified against Official docs — aquasecurity.github.io/kube-bench (flags-and-commands page) and · official docs
What it is and where it fits 🎯#
kube-bench audits a running Kubernetes cluster's actual configuration against the CIS Kubernetes Benchmark — a published, versioned checklist of concrete, testable hardening controls (file permissions on the API server's manifest, whether anonymous authentication is disabled, whether etcd's client communication is TLS-encrypted, and dozens more). It's the direct, hands-on tool for the shared responsibility model this series introduces in Fundamentals & Shift-Left — a managed Kubernetes service (EKS, GKE, AKS) secures the control plane for you, but node configuration, RBAC, and workload posture are still the cluster operator's job, and kube-bench is exactly how you get concrete, checkable evidence of which of those you've actually covered rather than assumed.
It complements, rather than overlaps, the other Kubernetes-security tools in this series: Trivy's
k8s/cluster-scanning mode and Falco look at running workloads and runtime behavior; OPA
Gatekeeper/Kyverno enforce policy at admission time, going forward; kube-bench instead answers "is
the cluster's own infrastructure — kubelet flags, API server flags, file permissions, etcd config —
already configured per a recognized hardening standard, right now." It's a point-in-time audit tool, not
a continuously-running policy gate.
How kube-bench decides which checks to run#
Auto-detection matters most on a managed service — a distribution-specific benchmark variant (EKS's own CIS benchmark, for instance) tests different things than the generic upstream benchmark, since AWS already manages the control plane and some upstream checks simply don't apply.
Installation#
# Binary release — pin an exact version rather than "latest" for reproducible CI results
curl -sL https://github.com/aquasecurity/kube-bench/releases/latest/download/kube-bench_linux_amd64.tar.gz \
| tar xz -C /usr/local/bin kube-bench
# Or run as a Kubernetes Job (the officially recommended in-cluster method — see below)
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kube-bench versionImportant
On a managed control plane (EKS/GKE/AKS), master/etcd checks will fail or not apply — that's
expected, not a bug. You typically don't have filesystem access to the control plane at all on a
managed service; scope --targets to node,policies there, and treat master/etcd hardening as the
cloud provider's documented responsibility instead (see the shared-responsibility framing above).
Core concepts#
| Concept | What it means |
|---|---|
| Benchmark version | A specific, versioned CIS Kubernetes Benchmark release (cis-1.24, eks-1.5.0, gke-1.6.0) — checks differ meaningfully between them |
| Target | Which cluster role's checks to run — master, etcd, node, policies, controlplane |
| Scored vs. unscored check | A "scored" check counts toward the benchmark's compliance percentage; "unscored" checks are informational/manual-review only |
| Remediation text | Every failed check ships with the exact command/config change the CIS benchmark itself recommends — kube-bench surfaces this directly in its output |
Running kube-bench against a cluster#
# Auto-detect version and run every applicable target
kube-bench run
# Scope explicitly — the common shape for a self-managed cluster
kube-bench run --targets=master,node,etcd,policies
# Pin a specific benchmark instead of trusting auto-detection (reproducible CI runs)
kube-bench run --benchmark cis-1.24
# EKS-specific benchmark, worker-node checks only (the part actually in your control on EKS)
kube-bench run --benchmark eks-1.5.0 --targets=node,policies
kube-bench run --check 1.1.1,1.1.2 # run only specific numbered checks
kube-bench run --skip 4.2.6 # exclude a specific check (e.g. a documented, accepted exception)Running inside a kind cluster (control plane runs in a container, not a normal node)#
kind create cluster --config kind-config.yaml
docker exec -it kind-control-plane sh -c \
"curl -sL https://github.com/aquasecurity/kube-bench/releases/latest/download/kube-bench_linux_amd64.tar.gz \
| tar xz -C /usr/local/bin && kube-bench run --targets=master,node,etcd,policies"kind's control-plane node is itself a Docker container rather than a real VM, so the standard "run kube-bench as a Job scheduled onto a master node" approach doesn't map cleanly — exec'ing directly inside the control-plane container is the practical workaround for local testing.
Output formats#
kube-bench run --json # machine-readable JSON to stdout
kube-bench run --junit # JUnit XML — plugs straight into CI test-result dashboards
kube-bench run --outputfile results.json --json # write JSON findings to a file instead of stdout
kube-bench run --include-test-output # show the actual raw command output behind each check, not just PASS/FAIL
kube-bench run --noremediations # suppress remediation text (shorter output for a quick pass/fail glance)
kube-bench run --asff # send findings directly to AWS Security Hub (ASFF format)Sample output (representative — exact check IDs/counts vary by benchmark version and cluster state)#
[INFO] 1 Control Plane Security Configuration
[INFO] 1.1 Control Plane Node Configuration Files
[PASS] 1.1.1 Ensure that the API server pod specification file permissions are set to 600 or more restrictive
[FAIL] 1.1.12 Ensure that the etcd data directory ownership is set to etcd:etcd
== Remediation ==
On the etcd server node, get the etcd data directory, passed as an argument --data-dir, from the
below command: ps -ef | grep etcd
Then, run the below command (based on the etcd data directory found above).
For example, chown etcd:etcd /var/lib/etcd
== Summary ==
25 checks PASS
3 checks FAIL
1 checks WARN
0 checks INFO
The == Remediation == block is genuinely the most useful part of a [FAIL] result — it's the exact CIS
Benchmark document's own recommended fix, not a generic "review this" note.
Who owns which findings — the shared responsibility model in practice#
The single most common confusion teams new to kube-bench run into is treating every [FAIL] as equally
"theirs" to fix. It isn't — which checks are even meaningful depends entirely on who manages the control
plane:
| Cluster type | Control-plane checks (master/etcd) | Node/policy checks |
|---|---|---|
Self-managed (kubeadm, on-prem, kind) | Yours — you run the API server/etcd yourself | Yours |
| EKS | AWS's — you have no filesystem access to fix these, and most don't apply | Yours — this is where real findings belong |
| GKE | Google's — same reasoning | Yours |
| AKS | Microsoft's — same reasoning | Yours |
This is the concrete version of the shared-responsibility framing this series introduces early on: a
[FAIL] on an EKS control-plane check isn't a gap in your team's work — it's either a stale check that
doesn't apply to a managed control plane at all, or evidence you ran the wrong benchmark variant. Always
confirm you're running the distribution-specific benchmark (--benchmark eks-1.5.0, not the generic
cis-1.24) before treating a control-plane finding as actionable.
Triaging failures by realistic remediation cost#
Not every [FAIL] deserves the same urgency — grouping by how disruptive the fix actually is produces a
much more realistic remediation plan than working strictly top-to-bottom by check number:
| Remediation cost | Example checks | Typical approach |
|---|---|---|
| Low — a file permission or flag change | chmod/chown on a manifest file, a missing --anonymous-auth=false kubelet flag | Fix immediately, low blast radius |
| Medium — a config change needing a rolling restart | Enabling audit logging, changing an admission-controller flag | Schedule a maintenance window, test in staging first |
| High — an architectural/RBAC decision | Restricting a wildcard ClusterRoleBinding used by several teams | Needs a design discussion — don't rush a fix that could break existing workflows |
Tip
Re-run kube-bench after every cluster upgrade, not just once at initial setup. A managed-service control-plane upgrade or a new default kubelet flag in a newer Kubernetes minor version can silently reintroduce a finding that was previously fixed — treat this the same way you'd treat re-running Trivy after a base-image bump, not a one-time checklist item.
Real-world scenario: baselining a freshly-created cluster#
Expect a real, substantial number of [FAIL] results even on a brand-new kind cluster — this is normal
and the whole point of the exercise is triage, not being surprised failures exist:
- Run
kube-bench run --targets=master,node,etcd,policies --json --outputfile baseline.json. - Group failures by severity/section rather than fixing top-to-bottom — file-permission fixes on the control plane are usually quick wins; RBAC/policy findings often need a real design decision.
- For every failure that's a deliberate, accepted risk (a dev-only cluster, a check that doesn't apply
to your distribution), document it explicitly and re-run with
--skip <check-id>rather than silently ignoring the same[FAIL]on every future run. - Re-run after remediation and diff the summary counts — this is your concrete "before vs. after" evidence for a compliance review (see Part 6 of this series).
Real-world scenario: gating cluster provisioning in CI#
# .github/workflows/cluster-hardening-check.yml
name: kube-bench CIS audit
on:
workflow_dispatch:
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Run kube-bench against staging cluster
run: |
kube-bench run --targets=node,policies --benchmark eks-1.5.0 --json --outputfile results.json
- name: Fail the pipeline on any FAIL result
run: |
FAILS=$(jq '[.Controls[].tests[].results[] | select(.status=="FAIL")] | length' results.json)
echo "Found $FAILS failing checks"
[ "$FAILS" -eq 0 ]kube-bench itself doesn't have a built-in "fail the pipeline on any FAIL" flag — the exit-code gating
has to be built from its JSON output, exactly as shown, which is why --json is worth reaching for even
for a quick manual run.
Automating remediation for repeat findings#
# A simple wrapper that re-runs after applying a known-good remediation set, to confirm the fix actually worked
kube-bench run --targets=node --json --outputfile before.json
ansible-playbook harden-kubelet.yml # apply the org's own standard kubelet-hardening playbook
kube-bench run --targets=node --json --outputfile after.json
diff <(jq '.Controls[].tests[].results[] | select(.status=="FAIL") | .test_number' before.json) \
<(jq '.Controls[].tests[].results[] | select(.status=="FAIL") | .test_number' after.json)kube-bench itself only ever audits — it never modifies anything on the host — so pairing it with the org's own configuration-management tooling (Ansible, a hardened AMI pipeline, a Kubernetes operator like kube-bench's companion project) for the actual fix, then re-running kube-bench to confirm the fix worked, is the standard pattern rather than expecting the tool to remediate on your behalf.
Common pitfalls#
- Running the generic upstream benchmark against a managed control plane and treating every master/etcd
[FAIL]as your team's problem. See the IMPORTANT box above — pin the distribution-specific benchmark (eks-1.5.0,gke-1.6.0) instead. - Not pinning
--benchmarkin CI. Auto-detection can pick a different benchmark version after a cluster upgrade, silently changing which checks run and making a "before vs. after" comparison invalid. - Treating every
[WARN]as ignorable. Unscored/manual-review checks often flag something genuinely worth a human decision (e.g., "review whether this setting is appropriate for your environment") — they aren't noise, just not auto-gradable. - Running it once and never again. kube-bench audits a point-in-time configuration; a cluster upgrade, a new node pool, or a changed kubelet flag can silently reintroduce a previously-fixed finding.
Config file format — customizing which checks run#
kube-bench ships its check definitions as YAML files bundled per benchmark version (cfg/cis-1.24/master.yaml,
etc.) — a real, complete excerpt:
# cfg/cis-1.24/master.yaml (abridged)
controls:
version: cis-1.24
id: 1
text: "Control Plane Security Configuration"
type: "master"
groups:
- id: 1.1
text: "Control Plane Node Configuration Files"
checks:
- id: 1.1.1
text: "Ensure that the API server pod specification file permissions are set to 600 or more restrictive"
audit: "stat -c permissions=%a /etc/kubernetes/manifests/kube-apiserver.yaml"
tests:
test_items:
- flag: "permissions"
compare: { op: bitmask, value: "600" }
set: true
remediation: "chmod 600 /etc/kubernetes/manifests/kube-apiserver.yaml"
scored: truekube-bench run --config-dir ./cfg --config ./cfg/config.yaml # point at a custom/modified check setEditing this directly is rare in day-to-day use, but understanding the shape matters for two real
situations: writing a custom check for an internal standard the CIS benchmark doesn't cover, and
understanding exactly what audit command a given [FAIL] actually ran, when the remediation text alone
isn't enough context to act on confidently.
Exit codes and when to reach for something else#
kube-bench's process exit code reflects whether it ran successfully, not whether checks passed — build
pass/fail gating from the JSON output's per-check status field, as shown in the CI recipe above. The
--exit-code flag can be set to force a specific non-zero code on any failing check, useful when the
calling pipeline framework only understands a raw process exit code and can't easily parse JSON. For
host/daemon-level Docker configuration auditing specifically (as opposed to Kubernetes cluster
configuration), reach for Docker Bench for Security instead (its own cheat sheet) — same CIS-benchmark
philosophy, different layer. For runtime behavioral detection on already-running workloads (a container
spawning an unexpected shell), reach for Falco, already covered in this series — kube-bench only looks
at configuration, never live process behavior.