CIS Kubernetes Benchmark controls, packaged as a SEC/Locke tarball for direct
import into RaaS. This repo is a focused extraction of the Kubernetes-specific
pieces of saltstack-lock — it does not use the locke.py --build pipeline;
scripts/create_k8s_cis_tarball.py is a standalone packager.
scripts/create_k8s_cis_tarball.py # the tarball builder
benchmarks/CIS_Kubernetes_v2.0.1.meta # top-level benchmark descriptor
salt/locke/system/CIS_Kubernetes_v2_0_1/
<control_id>.meta # one per control — audit/remediation text (source of truth)
<control_id>.sls # one per control — Salt state implementing the audit
<control_id>_bad.sls # optional — noncompliant test fixture (a few controls have one)
helm/kube-bench-job/ # reference Helm chart for deploying kube-bench (see helm/kube-bench-job/README.md)
_build/, _dist/ # generated by the script, gitignored — do not edit or commit
Every control's .sls calls salt['kube_bench_cache.status_for_check'](test_number=...)
to read aggregated kube-bench results. That module lives outside this repo,
in the saltext-kubernetes Salt extension
(saltext/kubernetes/modules/kube_bench_cache.py) — it must be installed on
any minion that actually runs these controls. This repo only needs .sls
source text at build time; it does not execute the states, so the tarball
builder itself has no dependency on that extension.
The builder needs two Python packages: msgpack and pyyaml.
cd salt-k8s-compliance
python3 -m venv .venv
source .venv/bin/activate
pip install msgpack pyyaml(or uv venv && uv pip install msgpack pyyaml, or uv run --with msgpack --with pyyaml ...
per-invocation if you have PyPI network access — see below). .venv/ is
already gitignored.
source .venv/bin/activate # if not already active
python3 scripts/create_k8s_cis_tarball.py <target_version>Example:
python3 scripts/create_k8s_cis_tarball.py 2_0_2
# -> _dist/k8s_cis_v2_0_2_format.tar.gz<target_version>is required (e.g.2_0_2or2.0.2). It's used to label the output benchmark and is stamped into every rewritten state ID andbenchmark_id.--src-versiondefaults to2.0.1(the current baseline in this repo) — only pass it if you've added a differently-versioned control directory.--repo-rootdefaults to..--keep-buildskips wiping_build//_dist/first (they're wiped by default to avoid stale files leaking into the tarball).
Why bump the version on every build: RaaS rejects re-importing a benchmark
with the same name ("cannot override existing custom benchmark"). Each new
target version forces fresh rows instead of colliding with a previous import —
bump it every time you re-upload (2_0_1 → 2_0_2 → 2_0_3 ...).
Upload with:
kubectl cp _dist/k8s_cis_v<target>_format.tar.gz salt-raas/<pod>:/opt/saltstack/raas/
kubectl exec -it salt-raas/<pod> -n salt-raas -- bash
cd /opt/saltstack/raas
salt-ssc compliance import k8s_cis_v<target>_format.tar.gz
salt-ssc compliance list | grep -i kubernetes # verifyCustomers who don't want to build the tarball themselves can grab a prebuilt one from this repo's
GitHub Releases page — each release is tagged with the benchmark version (e.g. v2.0.2) and
has the corresponding k8s_cis_v<version>_format.tar.gz attached as a downloadable asset.
Via the gh CLI:
gh release download v2.0.2 --repo <org>/salt-k8s-compliance --pattern '*.tar.gz'Or via the web UI: open the Releases page, pick a version, and download the .tar.gz asset
directly.
Once downloaded, skip straight to the upload steps above (kubectl cp / salt-ssc compliance import) — no
Python setup or build step required.
Each control is a <control_id>.meta + <control_id>.sls pair, where
control_id mirrors the CIS section number with dots replaced by underscores
(e.g. CIS 1.2.31 → 1_2_31).
This is the source of truth — audit/remediation text, applicability, and the
benchmark linkage. Follow the shape of an existing file, e.g.
1_2_1.meta:
version: 1
display_name: |
1.2.31 <CIS control title>
global_description: |
<one-line description>
osfinger:
- Kubernetes
refs:
- CIS: 1.2.31
benchmark_id:
CIS_Kubernetes_v2.0.1: # must match benchmarks/CIS_Kubernetes_v2.0.1.meta's name: exactly
type: cis
control_id: 1.2.31
desc: |
<same as display_name>
profile:
master: level1
information: |
<why this matters>
audit: |
<the exact audit mechanism — kube-bench test_number, or a command>
remediation: |
<how to fix it>Almost every control just aggregates the matching kube-bench check via
kube_bench_cache.status_for_check. Copy the pattern from
1_1_1.sls:
{%- set UNIQUE_SLS_STR = UNIQUE_SLS_STR | default('local') %}
{%- set unique_id = UNIQUE_SLS_STR %}
{%- set kube_bench_report_path = kube_bench_report_path | default('/var/log/kube-bench.json') %}
{%- set result = salt['kube_bench_cache.status_for_check'](
test_number='1.2.31',
namespace=pillar.get('kube_bench', {}).get('namespace', 'kube-system'),
label=pillar.get('kube_bench', {}).get('label', 'app=kube-bench'),
cache_path=kube_bench_report_path,
ttl_seconds=pillar.get('kube_bench', {}).get('ttl_seconds', 900),
kubeconfig=pillar.get('kube_bench', {}).get('kubeconfig') or None,
auth_mode=pillar.get('kube_bench', {}).get('auth_mode') or None,
collection_strategy=pillar.get('kube_bench', {}).get('collection_strategy') or None,
cronjob_name=pillar.get('kube_bench', {}).get('cronjob_name') or None,
job_timeout=pillar.get('kube_bench', {}).get('job_timeout') or None
) %}
{%- set is_compliant = (result.status == 'PASS') %}
{%- set is_warn = (result.status == 'WARN') %}
{%- set diagnostic = result.status ~ ': 1.2.31 <CIS control title>\n' ~ result.comment %}
CIS_Kubernetes_v2_0_1-1_2_31_<short_slug>_{{ unique_id }}:
locke.notify:
- text: {{ diagnostic | tojson }}
- result: {{ (is_compliant or is_warn) | tojson }}
- status: {{ 'compliant' if is_compliant else ('notapplicable' if is_warn else 'noncompliant') }}Only the state ID (must be CIS_Kubernetes_v2_0_1-<control_id>_..._{{ unique_id }},
matching the filename stem) and test_number/title text change per control.
test_number must be the exact ID kube-bench itself reports for that check —
verify it against a real kube-bench --json run or docs/kube-bench-output.json
in saltstack-lock if you're unsure it lines up with the CIS number.
A handful of controls implement their own audit logic directly in Jinja
instead of calling status_for_check (e.g. controls that check something
kube-bench doesn't cover) — check neighboring .sls files in the same CIS
section before assuming every control fits the aggregation pattern above.
Only a few controls have one (1_1_1, 1_1_2, 1_2_1, 4_2_1, 4_2_10).
It's a test fixture that puts the system in a known noncompliant state —
since kube-bench results aren't something this repo can safely fake by
mutating a live cluster, existing examples just emit a locke.notify with
status: noncompliant matching the audit's failure shape. See
1_1_1_bad.sls.
Add one only if you have a real way to exercise the noncompliant path.
Add <major>.<minor>[.<patch>] (dotted form, e.g. 1.2.31) to the ids:
list in benchmarks/CIS_Kubernetes_v2.0.1.meta.
The build script does not validate this against the control directory —
it will happily process every .meta/.sls pair it finds regardless of
whether the control ID is listed here, so a missed ids: entry produces no
error or warning. It matters downstream: RaaS associates a check to its
benchmark by matching the check's benchmark_id: against the benchmark's own
identity, and ids: is part of how the benchmark advertises its control set.
Don't rely on the build output to catch a missing entry — check the ids:
list by hand.
python3 scripts/create_k8s_cis_tarball.py <target_version>Check the console output for ✓ Processing control: <control_id> and confirm
no ⚠ Warning: <control_id>.sls not found lines appear.