Verified8 commandsAI-assisted

kube-bench

.md

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#

Diagram

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 version

Important

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#

ConceptWhat it means
Benchmark versionA specific, versioned CIS Kubernetes Benchmark release (cis-1.24, eks-1.5.0, gke-1.6.0) — checks differ meaningfully between them
TargetWhich cluster role's checks to run — master, etcd, node, policies, controlplane
Scored vs. unscored checkA "scored" check counts toward the benchmark's compliance percentage; "unscored" checks are informational/manual-review only
Remediation textEvery 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 typeControl-plane checks (master/etcd)Node/policy checks
Self-managed (kubeadm, on-prem, kind)Yours — you run the API server/etcd yourselfYours
EKSAWS's — you have no filesystem access to fix these, and most don't applyYours — this is where real findings belong
GKEGoogle's — same reasoningYours
AKSMicrosoft's — same reasoningYours

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 costExample checksTypical approach
Low — a file permission or flag changechmod/chown on a manifest file, a missing --anonymous-auth=false kubelet flagFix immediately, low blast radius
Medium — a config change needing a rolling restartEnabling audit logging, changing an admission-controller flagSchedule a maintenance window, test in staging first
High — an architectural/RBAC decisionRestricting a wildcard ClusterRoleBinding used by several teamsNeeds 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 --benchmark in 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: true
kube-bench run --config-dir ./cfg --config ./cfg/config.yaml    # point at a custom/modified check set

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