# kube-bench Cheat Sheet

> **Tool:** kube-bench (Aqua Security)
> **Category:** Security & Compliance
> **Verified against:** Official docs — aquasecurity.github.io/kube-bench (flags-and-commands page) and
> the project README at github.com/aquasecurity/kube-bench — 2026-09-05 (not installable in this
> sandboxed environment; verified against current official docs rather than a local `--help` run)
> **Official docs:** https://aquasecurity.github.io/kube-bench/

## 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](/tutorials/devsecops/01-fundamentals-and-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

```mermaid
flowchart TD
    Start["kube-bench run"] --> Detect{"Auto-detect K8s<br/>version + node role"}
    Detect -->|"Version detected"| Pick["Pick matching CIS<br/>benchmark version<br/>(cis-1.24, eks-1.5.0, ...)"]
    Detect -->|"--benchmark flag given"| Pin["Use the EXPLICITLY<br/>pinned benchmark —<br/>skip auto-detection"]
    Pick --> Targets{"--targets flag"}
    Pin --> Targets
    Targets -->|"master"| M["Control-plane checks:<br/>API server, scheduler,<br/>controller-manager"]
    Targets -->|"etcd"| E["etcd configuration<br/>checks"]
    Targets -->|"node"| N["Kubelet + worker-node<br/>checks"]
    Targets -->|"policies"| P["RBAC / Pod Security<br/>policy checks"]
    M & E & N & P --> Report["PASS / FAIL / WARN<br/>per numbered CIS control"]

    classDef info fill:#e5f0fa,stroke:#1d6fb8,color:#10161c
    classDef crit fill:#fbe8e6,stroke:#b3261e,color:#10161c
    classDef ok fill:#e5f5ea,stroke:#1f8a4c,color:#10161c
    class Pick,Pin info
    class Report ok
```

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

```bash
# 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

| 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

```bash
# 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)

```bash
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

```bash
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

```yaml
# .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

```bash
# 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:

```yaml
# 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
```

```bash
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.
