Kubernetes CIS Benchmarks: A Practical Hardening Guide

Kubernetes CIS Benchmarks: A Practical Hardening Guide

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 API
  • authorization.mode: Webhook: Authorization via API server (not always-allow)
  • readOnlyPort: 0: Disable the unauthenticated read-only port
  • protectKernelDefaults: 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.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...