CoursesCompliance as codeCIS Benchmarks as controls

CIS Benchmarks as controls

Automated hardening assessment, continuously.

Advanced30 min · lesson 3 of 15

Picture the moment an auditor points at your cluster and says, "Prove the control plane is hardened." You could hand over a wiki page describing how you configured it last year, or you could run one command that inspects the live API server, controller manager, kubelet, and etcd against a published standard and prints a pass/fail line for every setting. CIS Benchmarks are what make the second option possible, and they are the most directly automatable framework in this course: not prose about intent, but a numbered list of concrete settings you can check, score, and gate a pipeline on.

Consensus hardening baselines, defined

A CIS Benchmark is a consensus-developed hardening guide published by the Center for Internet Security for one specific target: Ubuntu 22.04, Amazon Web Services, Kubernetes, Docker, PostgreSQL, and dozens more. Each benchmark is a numbered list of recommendations, and each recommendation carries a rationale, an audit procedure (how to check it), and a remediation (how to fix it). Two dimensions matter in practice. Recommendations split into Level 1 (sensible baseline hardening with minimal operational impact) and Level 2 (defense-in-depth that may break workloads), and each item is either scored (objectively pass/fail) or not-scored / manual (needs human judgement). Because the audit procedures are concrete, "ensure the --anonymous-auth argument is set to false", "ensure CloudTrail is enabled in all regions", they map one-to-one onto automated checks. That is the whole trick of compliance as code: a control stops being a sentence and becomes an executable assertion with a verdict. Benchmarks are also versioned (CIS AWS Foundations v3.0, CIS Kubernetes 1.9), so pin the version your auditor expects rather than whatever the tool defaults to.

Assessing Kubernetes with kube-bench

kube-bench is the open-source scanner from Aqua Security that implements the CIS Kubernetes Benchmark. It reports [PASS], [FAIL], [WARN], or [INFO] for each item, where [WARN] means the check is manual or could not be verified automatically and [INFO] is purely informational. Install the binary on a control-plane node and point it at both the master and node profiles in one run.

control-plane node — install & run
# On a control-plane node: fetch and install the latest kube-bench binary
VER=$(curl -s https://api.github.com/repos/aquasecurity/kube-bench/releases/latest \
| grep -oP '"tag_name":\s*"v\K[^"]+')
curl -sL "https://github.com/aquasecurity/kube-bench/releases/download/v${VER}/kube-bench_${VER}_linux_amd64.tar.gz" | tar -xz
sudo install kube-bench /usr/local/bin/
sudo mkdir -p /etc/kube-bench && sudo cp -r cfg /etc/kube-bench/
# Assess the control plane AND this node against the CIS Kubernetes Benchmark.
# kube-bench auto-detects the benchmark version from the running kube-apiserver.
sudo kube-bench run --targets master,node

A trimmed run makes the shape obvious: grouped section headers, one verdict per item, remediation text for anything that failed, and a per-target summary.

kube-bench — stdout (trimmed)
[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 (Automated)
[INFO] 1.2 API Server
[PASS] 1.2.19 Ensure that the --audit-log-maxbackup argument is set to 10 or as appropriate (Automated)
[WARN] 1.2.1 Ensure that the --anonymous-auth argument is set to false (Manual)
[INFO] 1.3 Controller Manager
[FAIL] 1.3.2 Ensure that the --profiling argument is set to false (Automated)
== Remediation master ==
1.3.2 Edit the Controller Manager pod specification file
/etc/kubernetes/manifests/kube-controller-manager.yaml on the control-plane
node and set the below parameter:
--profiling=false
== Summary master ==
41 checks PASS
9 checks FAIL
11 checks WARN
0 checks INFO

Read a [FAIL] as an actionable ticket. Item 1.3.2 names the exact file and flag to change, and kube-bench prints that remediation verbatim so an engineer never has to guess. The summary line is your score, 41 pass against 9 fail, the single number a dashboard and an auditor both understand. Under the hood kube-bench does not call the Kubernetes API at all: it reads the command-line flags of the running processes from /proc, checks file ownership and permissions on the manifests and kubeconfigs, and compares them to the benchmark's expected values. That is why it must run on the node itself (or as a privileged Job with hostPID), and why it physically cannot inspect a control plane you do not operate.

Turning a scan into a gate

A scanner that only prints is a report; a scanner that fails the build is a control. Here is the trap most teams hit first: kube-bench returns exit code 0 even when checks fail, so a naive pipeline stays green while the cluster drifts. Force a non-zero exit with --exit-code, or emit JSON and gate on the fail count yourself.

ci runner — gate on the result
# kube-bench returns 0 even when checks FAIL, so a naive job stays green.
# Option A: force a non-zero exit code when any check fails.
kube-bench run --targets master,node --exit-code 1
# Option B: emit JSON and gate on the fail count in your pipeline.
kube-bench run --targets master,node --json > kb.json
fails=$(jq '.Totals.total_fail' kb.json)
echo "failed CIS checks: $fails"
[ "$fails" -gt 0 ] && { echo "CIS gate: BLOCK merge"; exit 1; }
ci runner — stdout
failed CIS checks: 10
CIS gate: BLOCK merge
$ echo $?
1

Cloud posture with Prowler

For cloud accounts the equivalent is Prowler, an open-source scanner that assesses AWS, Azure, GCP, and Kubernetes and ships the CIS mappings built in. One flag runs the entire CIS AWS Foundations Benchmark v3.0 and labels every finding with its CIS control number.

workstation / ci runner — Prowler
# Install Prowler (open-source, Python) and run the full CIS AWS v3.0 assessment.
pipx install prowler
prowler aws --compliance cis_3.0_aws
prowler — stdout (trimmed)
FAIL cloudtrail_multi_region_enabled [us-east-1]
No multi-region CloudTrail trail capturing management events was found.
CIS 3.0 AWS -> 3.1 Ensure CloudTrail is enabled in all regions
Remediation: aws cloudtrail update-trail --name my-trail \
--is-multi-region-trail --enable-log-file-validation
FAIL iam_rotate_access_key_90_days [global]
CIS 3.0 AWS -> 1.14 Ensure access keys are rotated every 90 days or less
Compliance Status of CIS 3.0 AWS:
Requirements passed: 60 / 62 (96.8%)
Failing requirements: 3.1, 1.14
Detailed report written to: output/prowler-output-123456789012-cis_3.0_aws.csv
$ echo $?
3 # Prowler exits 3 when any check fails -> it gates CI without extra wiring

Because a single Prowler run evaluates hundreds of checks and each maps to one or more CIS controls, one scan produces evidence for dozens of controls at once, the leverage that makes benchmark automation worth wiring in. Note the exit codes differ by tool: Prowler returns 3 on any failure, so it gates CI out of the box, while kube-bench returns 0 and must be forced. Steampipe's CIS compliance mods are a SQL-based alternative that query the same posture and emit the same pass/fail rows if you prefer to store results in a database.

Run these in two places. In CI you assess images and IaC before they ship (shift-left), catching a misconfiguration before it ever exists. Against live accounts and clusters you assess continuously (runtime posture management), because a setting that passed at deploy can regress an hour later when someone toggles it in the console. The benchmark becomes a living control set that re-proves itself on every run and flags drift the moment it appears, which is exactly the continuous compliance the rest of this course builds toward.

What a compliance pipeline does with each verdict
CIS check verdict (per item)
emitted by kube-bench / Prowler
PASS
Record as evidence
timestamped result feeds the audit trail; build proceeds
FAIL (scored)
Break the build
non-zero exit; open a remediation ticket from the printed fix
WARN / Manual
Triage
human confirms; file a waiver with an expiry, never a silent skip
INFO
Log only
informational, no gate
The verdict, not the scanner, is the control. Exit-code gating is what turns a printed FAIL into an enforced one.
Managed control planes turn master checks into false positives
On EKS, GKE, and AKS you do not run or see the API server, controller manager, or etcd, the provider does, so --targets master will FAIL or come back not-applicable for items you cannot possibly fix. Scope managed clusters to --targets node plus the provider-specific benchmark (CIS EKS/GKE/AKS), and mark inapplicable control-plane items as N/A with a documented justification. Letting unfixable master FAILs block delivery trains teams to ignore the scanner, and an auditor will reject evidence that quietly mixes not-applicable items into your fail count.
Quick check
01Your CI job runs kube-bench run --targets master,node, the log clearly shows 9 [FAIL] items, yet the pipeline reports success. What is the most likely cause?
Correct — A printed FAIL is only a report until you force a non-zero exit or parse Totals.total_fail.
Incorrect — No. A FAIL is a real, actionable finding with a remediation; it simply does not set the process exit code on its own.
Incorrect — No. kube-bench only assesses and reports; it never changes your configuration.
Incorrect — No. master results are included; the issue is exit-code handling, not the chosen target.
02Why must kube-bench run on the node it is assessing (or as a privileged Job with hostPID) instead of from your laptop with kubectl access?
Correct — Its evidence comes from local process flags and on-disk file permissions, so it has to run where those live.
Incorrect — No. kube-bench does not call the Kubernetes API at all, so API access is not what it is missing.
Incorrect — No. kube-bench only assesses and reports; it prints the remediation verbatim but never changes your configuration.
Incorrect — No. CIS also publishes provider-specific benchmarks (EKS/GKE/AKS); the constraint is where the flags and files can be read.
03On a managed EKS cluster your job runs kube-bench run --targets master,node and the master section returns FAIL for API server and etcd items you have no way to change. What do you do next?
Correct — That keeps the gate meaningful and hands the auditor an explicit, justified N/A instead of a permanent failure.
Incorrect — No. You do not run or see the managed control plane, so those items never clear, and unfixable FAILs blocking delivery train the team to ignore the scanner.
Incorrect — No. An auditor will reject evidence that quietly mixes not-applicable items into the fail count; the exclusion has to be documented as N/A.
Incorrect — No. On EKS, GKE, and AKS the provider runs the API server, controller manager, and etcd; you cannot reach those processes.

CIS Benchmarks give you hundreds of pre-written checks for free, but they only cover what the consensus anticipated. The moment you need an organization-specific rule, "every S3 bucket must carry a data-classification tag", "no container may run as root in the payments namespace", you have to write the check yourself. That is exactly what the next lesson does with OPA and Rego: the general-purpose policy engine that lets you express your own controls in the same executable, gate-able form you just saw a benchmark produce.

Try this

Work through “Cloud posture with Prowler” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: managed control planes turn master checks into false positives. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related