Security

Kubernetes Security Hardening: CIS Benchmarks, Pod Security Standards, and the Misconfiguration Checklist That Maps to Real Breaches

A practical guide to hardening Kubernetes clusters using CIS benchmarks, Pod Security Standards, RBAC best practices, etcd encryption, and automated scanning with kube-bench and Kubescape.

Kubernetes security hardening architecture showing CIS benchmark control categories, Pod Security Standards tiers, and cluster defense layers

I have looked at a lot of Kubernetes clusters over twenty years of cloud infrastructure work. I have been called in after breaches, asked to do pre-launch security reviews for regulated industries, and watched well-intentioned teams ship clusters to production that were, by any reasonable measure, open doors for attackers. The pattern is almost always the same: the team focused on getting the application running and never came back to lock down the platform underneath it.

Default Kubernetes is not secure. It is designed for flexibility and rapid adoption, which means sensible security defaults take a back seat to getting things working. Containers can run as root. Service accounts get mounted into every pod whether the app needs them or not. The API server might have anonymous access enabled. etcd is sitting there, plaintext, assuming you have secured the network around it. Nobody put network policies on any namespace.

This is not a theoretical concern. The Kubernetes security incidents that made headlines over the past several years share a short list of root causes: exposed dashboards, overprivileged pods that could escape to the host, stolen service account tokens granting cluster-admin, and crypto miners running because nobody noticed the resource spike. All of these are preventable with the controls I am going to walk through here.

The goal of this article is a practical hardening sequence: what to run, what to check, and what actually matters versus what is security theater.

Why the CIS Kubernetes Benchmark Exists

The Center for Internet Security publishes benchmarks for dozens of platforms, and the Kubernetes benchmark is one of the most comprehensive. It covers control plane configuration, etcd, worker node configuration, network policies, RBAC, and pod security. The current benchmark runs to 200-plus controls.

That number sounds overwhelming, and it is if you try to address every finding on day one. In practice, the controls group into tiers by exploitability and impact. There is a meaningful difference between “enable audit logging” (critical, you are blind without it) and “ensure the kubelet does not have the --make-iptables-util-chains flag set” (low-impact, iptables is not your threat model here).

The tool that runs CIS checks against a live cluster is kube-bench, open sourced by Aqua Security. You can run it as a Job on the cluster:

kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench

The output tells you which checks passed, which failed, and what the remediation is. A newly provisioned EKS or GKE cluster without any hardening will fail a surprising number of controls, because managed Kubernetes providers make different trade-offs than the benchmark assumes. That does not mean your cloud vendor’s managed cluster is insecure; it means the benchmark is written against self-managed clusters and some controls require manual opt-in.

The highest-impact control families are: API server configuration, etcd security, kubelet authorization, RBAC least privilege, and pod security restrictions. I will cover each of these in turn.

CIS Kubernetes Benchmark control categories organized by risk tier, covering API server, etcd, kubelet, network policy, and workload configuration domains

Pod Security Standards: The PSP Replacement

PodSecurityPolicy was the original Kubernetes mechanism for restricting what pods could do. It was deprecated in Kubernetes 1.21 and removed in 1.25. If you are still running a cluster older than 1.25 with PSP active, that is its own problem, but the replacement is Pod Security Standards (PSS) enforced by the Pod Security Admission (PSA) controller, which has been built into Kubernetes since 1.23.

PSS defines three profiles:

Privileged: No restrictions. Allows host namespaces, host path volumes, privilege escalation, and any Linux capabilities. This exists for infrastructure tooling that genuinely needs unrestricted access, such as CNI plugins and node-level monitoring agents.

Baseline: Blocks known privilege escalation paths. Disallows host namespaces, host process containers on Windows, privileged mode, CAP_SYS_ADMIN, and hostPath volumes to sensitive paths. Most application workloads can run under Baseline without modification.

Restricted: Full hardening. Requires running as non-root, requires dropping all Linux capabilities (adding back only the minimum needed), and requires seccomp profiles. This is the right target for any namespace running application code.

You apply these profiles to namespaces via labels:

apiVersion: v1
kind: Namespace
metadata:
  name: production-apps
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

The enforce label blocks non-compliant pods from starting. The audit label logs violations to the audit log without blocking. The warn label sends a warning in the API response. I recommend running warn and audit in non-production namespaces for a few weeks before switching to enforce, so you can identify what your workloads actually need.

The migration from PSP takes longer than teams expect. PSP had fine-grained controls (specific allowed host paths, specific allowed sysctls) that PSS does not replicate. For those cases, you supplement PSS with a policy engine, which I cover in the article on policy as code with OPA and Kyverno.

The operational goal is simple: enforce: restricted should be the default label for any namespace running application workloads. System namespaces (kube-system, your CNI plugin namespace, your CSI driver namespace) need higher permissions and should be labeled privileged, but that should be the documented exception, not the rule.

Pod Security Standards three-tier hierarchy showing Privileged with no restrictions, Baseline blocking known privilege escalation, and Restricted requiring non-root and capability dropping

API Server Hardening

The Kubernetes API server is the control plane entry point. Everything flows through it. If you get the API server configuration wrong, everything else is moot.

Disable anonymous authentication. The --anonymous-auth=false flag prevents unauthenticated requests from reaching the API server. It defaults to true, which means requests that do not present credentials are still processed as system:anonymous. Most clusters have no legitimate reason to allow anonymous access. Disable it.

Lock down authorization modes. The API server should run with --authorization-mode=Node,RBAC. Some older configurations include --authorization-mode=AlwaysAllow, which bypasses all authorization checks. Find that setting in your kubeadm config or control plane manifests and remove it immediately. The Node authorizer handles kubelet-to-API-server communication. RBAC handles everything else.

Enable audit logging. Without audit logging, you cannot answer “who did what and when” after a security incident. Configure an audit policy file and ship logs to a SIEM or log aggregation system. At minimum, log all requests at the Metadata level and sensitive resource operations at RequestResponse:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["secrets", "serviceaccounts", "pods/exec", "pods/portforward"]
  - level: Metadata
    omitStages:
      - RequestReceived

Enable the right admission controllers. Make sure NodeRestriction is active (prevents kubelets from modifying objects bound to other nodes), PodSecurity is enabled (enforces PSS profiles), and ServiceAccount is active (manages service account token lifecycle). The AlwaysPullImages controller is worth considering in multi-tenant environments, as it prevents cached images from being used without fresh registry authentication.

For managed clusters (EKS, GKE, AKS), most of these settings are not exposed to you because the vendors have already made reasonable choices. Where managed clusters need supplemental hardening is at the workload and RBAC level, not the API server flags.

etcd Security

etcd stores all cluster state: secrets, configurations, pod definitions, certificates, everything. If an attacker gets read access to etcd, they own the cluster completely, regardless of RBAC controls at the API server level.

The two critical controls are encryption at rest and access restriction.

Encryption at rest is not enabled by default on self-managed clusters. Configure an encryption provider in the API server:

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources:
      - secrets
      - configmaps
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: <base64-encoded-32-byte-key>
      - identity: {}

This encrypts secrets and configmaps in etcd using AES-CBC. For production, use a KMS provider (AWS KMS, GCP Cloud KMS, HashiCorp Vault) so the encryption keys are not stored on the same machine as the data they protect. The identity provider as a fallback means existing data can still be read; after enabling encryption, run kubectl get secrets --all-namespaces -o json | kubectl replace -f - to force re-encryption of all existing secrets.

Access control: etcd should be reachable only from the API server, using mTLS with a dedicated client certificate. etcd ports (2379 for client communication, 2380 for peer communication) should not be reachable from worker nodes or external networks. Use security groups, iptables rules, or network-level controls to enforce this.

On managed Kubernetes, etcd is managed by the cloud provider and encryption at rest is generally available as an opt-in setting. Enable it.

Kubelet Hardening

The kubelet runs on every node and manages pod lifecycle. It exposes an API that the control plane uses, but historically had some dangerous defaults.

Disable anonymous auth: --anonymous-auth=false. This prevents unauthenticated requests to the kubelet API from the internal network.

Set authorization mode to Webhook: --authorization-mode=Webhook. This delegates kubelet authorization decisions to the API server, so RBAC controls apply. The alternative is AlwaysAllow, which means anyone who can reach the kubelet API port can execute commands in any pod on that node.

Disable the read-only port: --read-only-port=0. The kubelet historically exposed pod and node information on port 10255 without authentication. There is no reason to have this open.

Enable protect-kernel-defaults: --protect-kernel-defaults=true. This causes the kubelet to fail startup if system-level kernel parameters do not match expected values, preventing malicious containers from having already modified sysctl settings.

These settings go into the kubelet configuration file, typically /etc/kubernetes/kubelet.conf or applied via a kubeadm configuration block.

RBAC Hardening Beyond the Basics

Our dedicated article on Kubernetes RBAC covers the conceptual framework for roles, bindings, and least privilege. Hardening RBAC in an existing cluster has a few specific patterns that go beyond initial setup.

Disable auto-mounting of service account tokens. By default, every pod gets the service account token for the default service account in its namespace mounted at /var/run/secrets/kubernetes.io/serviceaccount. Most applications never need to talk to the Kubernetes API. That token represents unnecessary attack surface if the container is compromised.

Disable it at the service account level:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: production-apps
automountServiceAccountToken: false

Only re-enable it on pods or service accounts that genuinely need API access.

Audit cluster-admin bindings regularly. Run this and examine the output critically:

kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name == "cluster-admin") | {name: .metadata.name, subjects: .subjects}'

In my experience, every organization that has been running Kubernetes for more than a year has at least one unexpected cluster-admin binding from an integration that “just needed to test something” and never got cleaned up. Treat cluster-admin bindings like root access on a server: they should be rare, documented, and reviewed quarterly.

Eliminate wildcard resource grants. A role that grants get, list, watch on * in a namespace exposes every resource type to that identity. Audit for these with:

kubectl get roles,clusterroles -A -o json | \
  jq '.items[] | select(.rules[].resources[]? == "*") | .metadata.name'

Create dedicated service accounts per workload. Shared service accounts mean one compromised application can leverage the API permissions of every other application sharing that account. The namespace default service account should have no permissions.

Network Policies as Security Controls

Network policies in Kubernetes are enforced by the CNI plugin. Cilium and Calico both support full network policy enforcement. The article on Kubernetes CNI plugins and network policies covers the implementation layer in depth.

For security hardening, the key pattern is default-deny at the namespace level with explicit allow rules:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production-apps
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

This blocks all ingress and egress for every pod in the namespace. You then add specific allow rules for legitimate traffic.

I acknowledge the operational overhead here. Managing network policies at pod-selector granularity slows initial deployments because engineers need to map out their application’s traffic patterns before they can write allow rules. Teams under delivery pressure abandon network policies because the friction is real.

My recommended compromise: start with namespace-level isolation (block cross-namespace traffic by default, allow all traffic within a namespace). This eliminates lateral movement across teams without requiring pod-level analysis. Add pod-level restrictions only for the highest-risk services: databases, secrets stores, internal payment APIs.

Container Image Security and Admission Control

A hardened cluster running unscanned images with known CVEs is not actually hardened. Image security is the supply chain layer, and it connects to the cluster through admission control.

The article on distroless images and Chainguard covers image hardening in detail. At the cluster level, Kyverno and OPA Gatekeeper can enforce image signing verification at admission time, blocking pods that reference unsigned images or images from registries not in your allowlist.

For runtime security after admission, Falco gives you behavioral detection: containers spawning unexpected shells, writing to sensitive paths, or making outbound connections to unexpected destinations. If you need kernel-level isolation for multi-tenant environments or untrusted code, gVisor and Kata Containers provide deeper workload separation.

The Kubernetes ValidatingAdmissionPolicy with CEL is worth adding to this stack. It allows you to write policy expressions like “no container may request more than 4 CPUs without a justification label” or “pods must have resource limits set” without deploying a separate webhook server.

Continuous Scanning with Kubescape

kube-bench checks your cluster configuration against the CIS benchmark. It does not scan your workload manifests. Kubescape fills that gap.

Kubescape is a CNCF incubating project that scans both cluster configuration and running workloads against the CIS Kubernetes Benchmark, NSA-CISA Kubernetes hardening guidelines, and MITRE ATT&CK for containers:

# Install
curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

# Scan the running cluster
kubescape scan --enable-host-scan

# Scan manifests before deploying
kubescape scan deployment.yaml

# Scan against NSA framework
kubescape scan framework nsa

The feature that changes the security posture of your CI/CD pipeline is manifest scanning before apply. Running kubescape scan against manifest files in your CI job catches privileged containers, missing resource limits, containers requesting excessive capabilities, and hardcoded secrets in environment variables before they ever reach the cluster.

Kubernetes security hardening pipeline showing Kubescape scanning in CI before deployment, Pod Security Admission enforcement at the API server, and Falco runtime detection in production

The Production Hardening Sequence

After running through this with multiple teams, here is the sequence I recommend:

Phase 1: Stop the bleeding (week 1)
Run kube-bench. Fix any CRITICAL findings immediately. Audit cluster-admin bindings and revoke unnecessary ones. Disable anonymous auth on the API server and kubelets. Enable audit logging and ship logs to your SIEM.

Phase 2: Workload controls (weeks 2 to 4)
Add PSS labels to namespaces: start with warn mode, move to enforce after validating your workloads run cleanly. Set automountServiceAccountToken: false on default service accounts. Create dedicated service accounts for workloads that legitimately need API access.

Phase 3: Network segmentation (month 2)
Deploy default-deny NetworkPolicies per namespace. Add allow rules for legitimate traffic patterns. Enable RBAC audit logging for anomalous API access patterns. Review the audit log weekly.

Phase 4: Supply chain (months 2 to 3)
Integrate Kubescape into CI pipelines. Implement image signing with Sigstore/cosign. Deploy Kyverno or OPA Gatekeeper for admission enforcement on image verification and policy guardrails beyond what PSS covers.

Phase 5: etcd and node hardening
For self-managed clusters: configure encryption at rest with a KMS provider, restrict etcd network access, harden kubelet configuration. For managed clusters: enable the provider’s encryption features and audit node security groups.

Phase 6: Continuous compliance
Schedule Kubescape scans daily. Track your compliance score against the CIS benchmark over time. Set remediation SLAs by severity: Critical within 24 hours, High within 7 days, Medium within 30 days.

A War Story: The Crypto Miner We Almost Missed

A few years into my career, I was doing infrastructure work for a fintech startup with a GKE cluster. CPU usage on several nodes was consistently 20% higher than expected for weeks, and the team dismissed it as organic growth. It was not growth.

An engineer had deployed a debugging pod with privileged: true a month earlier to troubleshoot a filesystem issue. They forgot about it afterward. The image was built from a public Docker Hub base image that had been backdoored with a crypto miner that ran as a background process. The privileged container gave it unrestricted access to the host CPU.

There was no network policy blocking outbound egress. There was no admission control rejecting privileged containers. There was no Falco rule alerting on unexpected CPU-intensive processes in a container. The kube-bench scan had been done once at cluster launch and never repeated.

We remediated in a day once we identified the source. Finding the source took three weeks of log archaeology. Audit logging would have cut that to hours. Admission control blocking privileged containers (now trivially done with PSS baseline mode) would have prevented it entirely.

That experience is why I am methodical about hardening sequencing and why I start every engagement by asking when the last kube-bench scan was run.

Pulling It Together

Kubernetes security hardening is not a single tool or a single scan. It is overlapping controls: Pod Security Standards constrain what workloads can do, RBAC constrains what identities can do, network policies constrain what traffic can flow, admission control prevents bad manifests from ever being applied, and continuous scanning catches configuration drift.

The CIS benchmark and Kubescape give you the inventory of what to check. kube-bench gives you the compliance report against the CIS standard. PSA and PSS give you enforcement built into Kubernetes itself. OPA and Kyverno give you the policy layer for controls that PSS does not cover.

None of this is optional if you are running Kubernetes in a regulated environment or with any sensitive data. The controls I have described are not exotic hardening for paranoid security teams. They are baseline hygiene. The default Kubernetes configuration was not designed with your threat model in mind. Hardening is how you correct for that.

Start with Phase 1 this week. Get audit logging running. Everything else builds on knowing what is actually happening in your cluster.