Verify & Validate
Don’t trust that it’s secure — prove it. The same tests a professional auditor would run, in plain steps.
A fortress you haven’t tested is just a hope with good intentions. This chapter is a checklist of concrete tests that prove each layer is doing its job. Run them right after building, and again after any big change. If a test fails, the chapter it maps to tells you how to fix it.
Most of these are read-only and safe to run anytime. The handful that deliberately try to break in (marked 🔴 attack test) are safe too — they’re designed to be blocked, and a block is a pass.
The fast acceptance test
If you only do one thing, run the cluster benchmark and the deliberate-bad-pod test:
# 1. CIS benchmark — the authoritative cluster hardening score
sudo k3s kubectl apply -f scripts/config/k3s/kube-bench-job.yaml
sudo k3s kubectl logs -f job/kube-bench | tail -40
# 2. A rule-breaking pod MUST be rejected by admission control:
kubectl run evil --image=nginx --privileged --restart=Never
# PASS = "Error from server ... admission webhook ... denied the request"
Pass criteria: kube-bench shows 0 FAIL on scored controls (a few WARN/INFO are normal and explained in its output), and the privileged pod is denied. If both hold, your core hardening is real, not theoretical.
Layer-by-layer verification
L0 — Host
sudo nft list ruleset | grep -E 'policy drop|dport' # firewall default-deny + only allowed ports
sudo aa-status | head -3 # AppArmor: profiles in enforce mode
sudo systemctl is-active auditd # auditing on
sudo sysctl kernel.kptr_restrict kernel.unprivileged_bpf_disabled # hardening applied
lsblk -o NAME,FSTYPE,MOUNTPOINT | grep crypt # disk is LUKS-encrypted
sudo lynis audit system --quick # CIS-style audit score
From a second machine, scan the host: nmap -p- <host-ip>. You should see
only the WireGuard UDP port (and nothing if the scanner isn’t on the VPN). An
open SSH or 6443 to the world is a failure — fix the firewall before going further.
L1 — Immutable VM
sudo virsh list --all # the k3s VM is running
# Confirm the guest root is read-only (Flatcar/Talos): a write to /usr must fail.
L2 — k3s hardening
kubectl get --raw='/readyz?verbose' # all checks pass
kubectl get apiservices | grep -i False && echo "DEGRADED" || echo "OK"
# Secrets must be encrypted at rest — read the raw etcd value, it should be ciphertext:
sudo k3s secrets-encrypt status # 'Encryption Status: Enabled'
# Audit log is being written:
sudo tail -n 2 /var/lib/rancher/k3s/server/logs/audit.log
L3 — Sealed runtime 🔴 attack test
# A sandboxed pod should report the gVisor kernel, not the host kernel:
kubectl run gv --image=alpine --restart=Never --overrides='{"spec":{"runtimeClassName":"gvisor"}}' -- uname -a
kubectl logs gv # gVisor reports a distinctive synthetic kernel string
# A container trying to gain privileges must fail (read-only rootfs / dropped caps):
kubectl exec gv -- sh -c 'touch /test' 2>&1 | grep -i 'read-only' && echo "rootfs locked ✓"
L4 — Network zero-trust 🔴 attack test
# Two pods in different namespaces should NOT reach each other by default:
kubectl run a -n ns1 --image=nicolaka/netshoot --restart=Never -- sleep 1d
kubectl run b -n ns2 --image=nicolaka/netshoot --restart=Never -- sleep 1d
kubectl exec -n ns1 a -- curl -m 3 http://<b-pod-ip> # PASS = times out (blocked)
# Confirm pod traffic is WireGuard-encrypted:
kubectl -n kube-system exec ds/cilium -- cilium status | grep -i encryption # 'Wireguard'
# See the flows live:
hubble observe --verdict DROPPED | head # default-deny drops are visible
L5 — Encrypted storage
kubectl get storageclass | grep encrypted # the encrypted class exists
# Provision a test volume, confirm Bound, then confirm it's LUKS on the node:
kubectl apply -f scripts/config/storage/example-pvc.yaml
kubectl get pvc -A | grep Bound
sudo dmsetup ls --target crypt | head # the volume shows as a crypt device
L6 — Observability
kubectl -n monitoring get pods # Prometheus/Grafana/Loki/Tetragon Running
# Generate a benign 'suspicious' event and confirm Tetragon/Falco catches it:
kubectl run shell --image=alpine --restart=Never -- sh -c 'cat /etc/shadow'
# then look for the detection:
kubectl -n kube-system logs ds/tetragon | grep -i shadow # the read was observed
✦ — Supply chain & policy 🔴 attack test
# An image from a registry NOT on the allow-list must be rejected:
kubectl run rogue --image=docker.io/library/nginx --restart=Never
# PASS = denied by Kyverno restrict-registries (if docker.io isn't allow-listed)
# An UNSIGNED image must be rejected when verifyImages is enforced:
kubectl run unsigned --image=<your-registry>/test:unsigned --restart=Never # PASS = denied
Continuous validation (set and forget)
You don’t have to remember to run these — the cluster scans itself:
| Tool | What it checks | Where results show up |
|---|---|---|
| Trivy Operator | Image CVEs, misconfigs, exposed secrets, RBAC | kubectl get vulnerabilityreports -A; Grafana |
| Kubescape | NSA/CISA + MITRE ATT&CK posture | Kubescape dashboard / reports |
| kube-bench (weekly CronJob) | CIS benchmark drift | Job logs; alert on new FAIL |
| Tetragon / Falco | Live runtime attacks | Loki + your alert channel |
kubectl get vulnerabilityreports -A | sort -k7 -r | head # worst CVEs first
kubectl get configauditreports -A | head
How this guide itself was validated
The scripts and manifests in this repository were checked before publishing — not just claimed to work:
shellcheck+bash -non every shell script → 0 errors.kubeconformschema-validation on every Kubernetes manifest → 0 invalid (custom-resource manifests such as Cilium and Kyverno policies are schema-skipped, which is expected offline).- A live boot of the hardened k3s (
v1.36.1+k3s1) using the exactconfig.yaml,audit-policy.yaml, andpsa.yamlfrom this repo. The control plane came up with zero configuration errors, and the logs confirmed the hardening is active:
| Check | Result |
|---|---|
Hardened config.yaml accepted by k3s v1.36.1 |
Pass no fatal/parse/validation errors |
| Secrets encryption-at-rest provider loaded | Pass encryption-provider-config controller started |
| Admission chain loaded (incl. PodSecurity) | Pass 19 validating + 16 mutating controllers |
| PSA + EventRateLimit admission config accepted | Pass admission-control-config-file loaded |
| Audit logging active | Pass 1.4 MB of valid JSON audit events written |
protect-kernel-defaults satisfied by sysctls |
Pass kubelet did not reject |
The full seven-layer stack — host firmware, nested VM, TPM-sealed LUKS, USB
passthrough — can only be fully exercised on your real hardware, by design (those
layers are the hardware). Request-level checks (the privileged-pod denial, node
Ready, kube-bench’s full score) likewise need a long-running cluster with
registry access. This chapter is exactly how you run those on your machine; the
repository’s automated checks cover everything that can be validated without it.
What this chapter bought you
Proof. You can now demonstrate — not just claim — that the firewall is closed, the sandbox holds, the network denies by default, the disk is encrypted, bad pods are rejected, and the sensors are watching. Re-run this checklist after every change and you’ll never wonder whether a tweak quietly opened a hole.