The CIS (Center for Internet Security) Kubernetes Benchmark is a comprehensive set of security configuration recommendations for Kubernetes clusters. It covers everything from API server flags to etcd TLS configuration to pod security settings. If you’re running Kubernetes in any environment that takes security seriously, you should know what it says and how to implement it.
This isn’t a compliance post. It’s a practical guide to making your cluster actually more secure.
Running kube-bench
Before you can fix anything, you need to know where you are. kube-bench is the tool for assessing your cluster against the CIS benchmark:
# Run kube-bench as a Job
apiVersion: batch/v1
kind: Job
metadata:
name: kube-bench
namespace: kube-system
spec:
template:
spec:
hostPID: true
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: kube-bench
image: aquasec/kube-bench:latest
command: ["kube-bench", "--benchmark", "cis-1.8"]
volumeMounts:
- name: var-lib-etcd
mountPath: /var/lib/etcd
readOnly: true
- name: etc-kubernetes
mountPath: /etc/kubernetes
readOnly: true
restartPolicy: Never
volumes:
- name: var-lib-etcd
hostPath:
path: /var/lib/etcd
- name: etc-kubernetes
hostPath:
path: /etc/kubernetes
kubectl logs job/kube-bench -n kube-system
The output categorizes findings as PASS, FAIL, WARN, and INFO. Focus on FAILs first; WARNs are often manual checks or configuration choices.
API Server Hardening
The API server is the gateway to your cluster. Most CIS benchmark controls focus here.
Disable Anonymous Authentication
# kube-apiserver flags
--anonymous-auth=false
With anonymous auth enabled, unauthenticated requests get the system:anonymous user and system:unauthenticated group. If any RBAC policy grants permissions to these, you have an unauthenticated attack surface.
Enable Audit Logging
--audit-log-path=/var/log/kubernetes/audit.log
--audit-log-maxage=30
--audit-log-maxbackup=10
--audit-log-maxsize=100
--audit-policy-file=/etc/kubernetes/audit-policy.yaml
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
- level: Metadata
resources:
- group: ""
resources: ["pods", "services"]
- group: "apps"
resources: ["deployments"]
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
Enable Admission Controllers
--enable-admission-plugins=NodeRestriction,PodSecurity,ResourceQuota,LimitRanger
NodeRestriction: Limits what nodes can modify (prevents a compromised node from modifying other nodes’ objects).
PodSecurity: Enforces Pod Security Standards (replaces deprecated PodSecurityPolicy).
ResourceQuota: Enforces resource limits per namespace.
LimitRanger: Sets default resource requests/limits when not specified.
TLS Configuration
--tls-min-version=VersionTLS12
--tls-cipher-suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
Disable TLS 1.0 and 1.1. Restrict to ECDHE cipher suites (forward secrecy). Remove weak ciphers like RC4, DES, 3DES.
etcd Hardening
etcd stores all cluster state, including Secrets. A compromised etcd is a complete cluster compromise.
Enable Encryption at Rest
# encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
- configmaps
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {} # Fallback for unencrypted reads
# kube-apiserver flag
--encryption-provider-config=/etc/kubernetes/encryption-config.yaml
With encryption at rest, even if an attacker gets read access to etcd’s data files, secrets are encrypted.
etcd TLS
etcd should use mutual TLS for all communications:
# etcd flags
--cert-file=/etc/etcd/server.crt
--key-file=/etc/etcd/server.key
--client-cert-auth=true
--trusted-ca-file=/etc/etcd/ca.crt
--peer-cert-file=/etc/etcd/peer.crt
--peer-key-file=/etc/etcd/peer.key
--peer-client-cert-auth=true
--peer-trusted-ca-file=/etc/etcd/ca.crt
kubelet Hardening
# /etc/kubernetes/kubelet-config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false
webhook:
enabled: true
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
authorization:
mode: Webhook
readOnlyPort: 0
protectKernelDefaults: true
streamingConnectionIdleTimeout: 5m
rotateCertificates: true
serverTLSBootstrap: true
tlsMinVersion: VersionTLS12
Key settings:
anonymous.enabled: false: No unauthenticated access to kubelet APIauthorization.mode: Webhook: Authorization via API server (not always-allow)readOnlyPort: 0: Disable the unauthenticated read-only portprotectKernelDefaults: true: Refuse to start if kernel defaults don’t match expected values
Talos: Pre-Hardened by Default
This is where Talos Linux shines. Many of the CIS benchmark controls are built into Talos by default:
- API server has reasonable security defaults
- etcd TLS is configured automatically
- kubelet anonymous auth is disabled
- No SSH, no shell, no direct node access
- Read-only root filesystem
Running kube-bench against a freshly installed Talos cluster gives you a significantly better baseline than a freshly installed RKE2 or kubeadm cluster before hardening. You’re starting from a better position and spending less time on remediation.
RKE2: The Compliance-Focused Distribution
If you’re not using Talos, RKE2 (Rancher Kubernetes Engine 2) is built with CIS compliance in mind:
# /etc/rancher/rke2/config.yaml
profile: cis-1.8 # Apply CIS hardening profile
This single configuration applies most CIS benchmark recommendations automatically. RKE2 documentation lists exactly which controls are addressed and which require manual configuration.
Pod-Level Security
At the pod level, CIS benchmark requirements map directly to Pod Security Standards:
# Apply to all new namespaces via Admission Controller
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1
kind: PodSecurityConfiguration
defaults:
enforce: "restricted"
audit: "restricted"
warn: "restricted"
exemptions:
usernames: []
runtimeClasses: []
namespaces:
- kube-system
- kube-public
Setting restricted as the cluster-wide default means every new namespace enforces the restricted Pod Security Standard unless explicitly exempted.
Network Policies: Default Deny
CIS benchmark requires that all namespaces have a default deny NetworkPolicy:
# Apply to every namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Then explicitly allow required traffic. This is defense in depth: even if a pod is compromised, it can’t reach other services it wasn’t explicitly allowed to reach.
Ongoing Compliance
CIS benchmark compliance isn’t a one-time task. Clusters drift. New workloads are deployed that don’t meet the standards. Kubernetes versions change and new recommendations emerge.
Run kube-bench in your CI pipeline:
# Daily kube-bench job in CI
- schedule:
- cron: "0 3 * * *" # 3am daily
jobs:
kube-bench:
steps:
- run: kubectl apply -f kube-bench-job.yaml
- run: |
kubectl wait --for=condition=complete job/kube-bench -n kube-system --timeout=120s
FAILS=$(kubectl logs job/kube-bench -n kube-system | grep "^\[FAIL\]" | wc -l)
if [ "$FAILS" -gt "$ALLOWED_FAILS" ]; then
echo "CIS benchmark regression: $FAILS failures"
exit 1
fi
Treat CIS benchmark regressions like failing tests. They’re telling you your security posture got worse.
The benchmark isn’t perfect—some controls are overly conservative for some environments, and the automated tool can’t check everything. But it’s the best available starting point, and the process of working through each control teaches you more about Kubernetes internals than almost any tutorial.