A misconfigured Kubernetes cluster doesn’t announce itself. It just sits there, quietly reachable, until someone finds an exposed dashboard, a default-allow network policy, or a service account with cluster-admin rights it never needed. Kubernetes 1.37, released in late August 2026, keeps pushing security defaults in the right direction, but the platform still ships with more open doors than most teams realize. This tutorial walks through hardening a real cluster from the ground up: RBAC, Pod Security Standards, network policies, secrets management, image signing, runtime detection, and node-level lockdown, with copy-pasteable YAML at every step.
By the end you’ll have a working, layered kubernetes security hardening baseline you can drop into a staging cluster today and promote to production once you’ve tested it against your own workloads. This isn’t a checklist you skim once. Kubernetes security hardening is a maintenance habit, and this guide is built to be re-run every quarter as your cluster grows.
Why Kubernetes Security Hardening Can’t Wait Until After a Breach
The OWASP Kubernetes Top 10 and the official Kubernetes security checklist now converge on the same five pillars for 2026: RBAC least privilege, Pod Security Standards enforcement, network segmentation, image provenance, and runtime detection. That convergence matters because it means the industry has stopped debating what “secure Kubernetes” looks like. The open question left for most teams is simply whether they’ve actually implemented it.
Most breaches in container environments don’t start with a zero-day. They start with something mundane: a service account that automounts a token nobody uses, a namespace with no NetworkPolicy so a compromised pod can reach the entire cluster, or a Secret sitting unencrypted in etcd. Kubernetes security hardening closes those gaps systematically rather than reactively. Container security best practices in 2026 also increasingly assume a supply-chain angle: image scanning and signature verification catch compromised base images before they ever reach a node, which matters given how much of a typical container image is inherited from a public registry rather than written in-house.
The good news is that none of the steps below require ripping out your existing cluster. Every control here layers on top of a running deployment, and most can be rolled out namespace by namespace so you can validate behavior before enforcing it fleet-wide.
Prerequisites: Tools, Versions, and Access You’ll Need
Before starting, confirm you have the following. Version numbers below reflect what’s current and stable as of August 2026 — check each project’s release page before you install, since minor point releases ship frequently.
- Kubernetes cluster running version 1.31 or newer (this guide is written against Kubernetes 1.37.0, released August 26, 2026). Pod Security Admission and most RBAC behavior referenced here is stable as of 1.25+, so older supported clusters will still work with minor adjustments.
- kubectl matched to your cluster’s minor version (skew of no more than one minor version in either direction, per upstream policy).
- Cluster-admin access for the initial hardening pass — you’ll be editing RBAC, admission controllers, and node configuration.
- Helm 3.x for installing Falco, Kyverno, and other add-ons via chart.
- Falco 0.44.1 or newer for runtime threat detection.
- Kyverno 1.19 or newer for policy-as-code admission control (OPA Gatekeeper is a valid alternative if your team already standardized on Rego).
- kube-bench 0.16.0 or newer for automated CIS Kubernetes Benchmark scanning.
- An image scanner such as Trivy or Grype integrated into your CI pipeline (either works; this guide uses Trivy in examples since it’s the most widely adopted).
- A log aggregation target — Splunk, Elastic, or a cloud-native SIEM — to receive audit logs and Falco alerts.
- A non-production namespace or staging cluster to validate policies in
auditmode before switching toenforce.
If you’re running managed Kubernetes (EKS, AKS, GKE), some of these steps — especially node hardening and control plane audit logging — are partially handled by the provider. We’ll flag where that applies. If you’re comparing managed options first, our EKS vs AKS vs GKE breakdown covers the control-plane security defaults each one ships with out of the box.
Step 1: Audit Your Cluster’s Current Security Posture
Don’t harden blind. Run a baseline scan first so you know what you’re actually fixing, and so you have a before/after comparison to show whoever signs off on the change window. Install kube-bench and run it against your nodes and control plane:
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl get pods -l app=kube-bench
kubectl logs job/kube-bench
kube-bench checks your cluster against the CIS Kubernetes Benchmark, flagging items as PASS, FAIL, WARN, or INFO. Expect your first run on an unhardened cluster to return a long list of FAIL entries around file permissions, RBAC wildcards, and anonymous auth. That’s normal — treat it as your punch list for the rest of this guide, not a grade.
Next, check for the RBAC red flags directly:
# Find every ClusterRoleBinding granting cluster-admin
kubectl get clusterrolebindings -o json |
jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
# Find roles with wildcard permissions
kubectl get clusterroles -o json |
jq -r '.items[] | select(.rules[]?.resources[]? == "*" or .rules[]?.verbs[]? == "*") | .metadata.name'
Any service account bound to cluster-admin that isn’t a controller or your own CI/CD pipeline identity is worth investigating immediately. This is consistently the single highest-impact finding in a first-pass audit.
Step 2: Enforce RBAC Least Privilege and Kill Wildcard Permissions
RBAC least privilege means every service account, user, and group has exactly the permissions it needs and nothing more. In practice, most clusters drift toward over-permissioning because it’s faster to grant cluster-admin during a debugging session and never revoke it. Start by replacing broad ClusterRoleBindings with namespace-scoped Roles wherever the workload doesn’t genuinely need cross-namespace access.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: payments
name: payments-deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: payments-deployer-binding
namespace: payments
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: payments
roleRef:
kind: Role
name: payments-deployer
apiGroup: rbac.authorization.k8s.io
Notice there’s no wildcard anywhere in that Role — every resource and verb is spelled out. Audit your RoleBindings on a recurring schedule, not just once. A quarterly review catches the permissions that got added “temporarily” six months ago and never removed. Teams running Kubecost alongside their cluster for cost attribution can piggyback the same quarterly cadence onto a permissions review, since both require walking every namespace anyway — see our Kubecost setup guide if you haven’t wired that up yet.
Step 3: Apply Pod Security Standards at the Namespace Level
Pod Security Standards (PSS) replaced the deprecated PodSecurityPolicy and are enforced natively via the built-in Pod Security Admission controller — no extra installation required. There are three levels: Privileged (unrestricted), Baseline (blocks known privilege escalations), and Restricted (enforces current pod hardening best practice, including non-root and no privilege escalation). For 2026 production workloads, Restricted should be your default target.
apiVersion: v1
kind: Namespace
metadata:
name: payments
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
Don’t flip straight to enforce on a namespace with existing workloads — you’ll break pods that aren’t compliant yet. Set audit and warn first, watch the API server logs and `kubectl` warnings for a week, fix what surfaces, then move to enforce. Full level definitions live in the official Pod Security Standards documentation.
Table: Pod Security Standards Levels Compared
| Level | What It Blocks | Typical Use Case | 2026 Recommendation |
|---|---|---|---|
| Privileged | Nothing — fully unrestricted | Node-level agents, CNI plugins, CSI drivers | System namespaces only, never app workloads |
| Baseline | Known privilege escalations: host namespaces, privileged containers, most capabilities | Legacy apps mid-migration to Restricted | Transitional, not a long-term resting state |
| Restricted | Everything in Baseline plus: root user, privilege escalation, non-default capabilities, unconfined seccomp | Standard application workloads | Default enforcement target for all app namespaces |
Step 4: Disable Automatic Service Account Token Mounting
By default, every pod gets a Kubernetes API token mounted into its filesystem, whether or not the workload ever calls the API server. That token is a standing credential an attacker can lift the moment they get shell access to a container. Most workloads — a web server, a batch job, a static frontend — never need it.
apiVersion: v1
kind: ServiceAccount
metadata:
name: frontend-sa
namespace: payments
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: payments
spec:
template:
spec:
serviceAccountName: frontend-sa
automountServiceAccountToken: false
containers:
- name: frontend
image: registry.internal/frontend:1.14.2
Setting it at both the ServiceAccount and pod spec level is redundant but cheap insurance — if someone forgets one, the other still catches it. For the handful of workloads that genuinely need API access (operators, controllers, CI runners), leave the token mounted but scope its RBAC role as tightly as described in Step 2.
Step 5: Deploy Default-Deny Network Policies
Without a NetworkPolicy, every pod in a namespace can talk to every other pod in the cluster by default. That’s convenient during development and disastrous during an incident, because it means a single compromised container can reach your database, your internal admin panel, and anything else on the flat network. Default-deny for both ingress and egress, with explicit allow rules layered on top, is now baseline practice.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: payments
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8443
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
That last policy matters more than it looks — teams frequently deploy a default-deny egress rule, forget DNS still needs to resolve, and spend an afternoon debugging why every pod suddenly can’t reach anything. Build the DNS allowance in from the start. Note that NetworkPolicy enforcement depends on your CNI plugin actually supporting it — Calico, Cilium, and most managed cluster CNIs do, but the default kubenet on some older setups doesn’t enforce policies at all, which means the YAML applies with no error and silently does nothing.
Step 6: Encrypt Secrets at Rest and Move to External Secret Managers
Kubernetes Secrets are base64-encoded, not encrypted, by default. Anyone with etcd read access — or a backup of etcd — can trivially decode every Secret in your cluster. Enable encryption at rest at the API server level:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
Reference that file with --encryption-provider-config on the API server, then re-write existing Secrets so they get encrypted under the new config (kubectl get secrets --all-namespaces -o json | kubectl replace -f - forces a rewrite). For mature teams, the 2026 recommendation goes a step further: migrate sensitive Secrets out of etcd entirely into an external manager like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault, and inject them at runtime via a CSI driver instead of storing them as native Kubernetes objects. We cover the full setup in our HashiCorp Vault tutorial if you’re starting from scratch, and if you’re choosing between managed secret stores our CSPM tool comparison covers how each platform surfaces exposed Secrets automatically.
Step 7: Harden Container Images — Non-Root, Minimal, Read-Only
The smaller and more restricted your container image, the less an attacker can do once they’re inside it. 2026 hardening guidance is explicit on this: use distroless or slim base images, run as a non-root user, drop unnecessary Linux capabilities, and make the root filesystem read-only wherever the app doesn’t need to write locally.
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.internal/app:2.3.1
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
The emptyDir mount for /tmp is a common gotcha: setting readOnlyRootFilesystem: true without it will crash-loop any application that writes temp files, logs, or cache data to disk, which is most of them. Mount a writable volume explicitly for whatever paths the app actually touches, and leave everything else locked.
Step 8: Enforce Image Signing and Admission Control With Kyverno
Hardening a pod’s runtime configuration doesn’t help if the image itself was compromised before it ever reached your cluster — a poisoned dependency in a public base image, a malicious CI step, or a supply-chain injection. Admission controllers close that gap by blocking anything that doesn’t meet policy before it’s scheduled. Kyverno 1.19 (current as of August 2026) is the most widely adopted policy engine for this, alongside OPA Gatekeeper.
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace
Once Kyverno is running, add a ClusterPolicy that blocks unsigned images from being admitted:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
background: false
rules:
- name: check-image-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.internal/*"
attestors:
- count: 1
entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
<your-cosign-public-key>
-----END PUBLIC KEY-----
Sign your images at build time with Cosign (part of the Sigstore project) so this policy has something to actually verify. Just like Pod Security Standards, deploy this in Audit mode first — flipping straight to Enforce on a policy engine you haven’t tested against your real image inventory is the single fastest way to take production down during a deploy.
Step 9: Turn On API Server Audit Logging and Ship It to a SIEM
Audit logs are your forensic trail — without them, you can’t answer “what happened” after an incident, only “what state we’re in now.” Configure a policy that captures request/response bodies for sensitive operations (Secrets access, exec into pods) while keeping noisier, lower-value requests at metadata-only level to control log volume.
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
- level: RequestResponse
verbs: ["exec", "attach", "portforward"]
resources:
- group: ""
resources: ["pods"]
- level: Metadata
omitStages:
- RequestReceived
Point the API server at this policy with --audit-policy-file and --audit-log-path, then forward the resulting log file into whatever SIEM your security team already watches — Splunk, Elastic, or a cloud-native option. If you’re comparing SIEM platforms for this, our cloud security platform comparison breaks down ingestion pricing, which matters once you’re piping full RequestResponse-level Secrets access logs at scale.
Step 10: Add Runtime Threat Detection With Falco
Everything up to this point is static configuration — it prevents bad states from being created. Runtime detection catches bad behavior happening right now: a shell spawned inside a container that shouldn’t have one, a process reading /etc/shadow, an unexpected outbound connection. Falco 0.44.1 is the CNCF-graduated standard for this and reads directly from kernel syscalls via eBPF, so it sees activity that config auditing alone never will.
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco
--namespace falco --create-namespace
--set driver.kind=modern_ebpf
--set falcosidekick.enabled=true
Falco ships with a solid default ruleset (shell spawned in container, write below binary directory, unexpected network tool launched) but expect noisy false positives out of the box against your specific workloads. Tune the rules over your first two weeks rather than disabling categories wholesale — an ignored Falco alert is worse than no Falco at all, because it creates a false sense of coverage. Full rule syntax and tuning guidance is in the Falco documentation.
Step 11: Harden Nodes and the Control Plane
Pod-level and API-level hardening don’t help if the underlying node is soft. Node hardening in 2026 focuses on three things: disabling unnecessary kernel modules, isolating node roles so control-plane and worker responsibilities never share a machine, and locking SSH access down to break-glass only.
- Disable unused kernel modules (
dccp,sctp, and other rarely-needed network modules are common attack surface left enabled by default on stock images). - Never co-locate control-plane components with general workloads — taint control-plane nodes so scheduler assigns nothing else to them.
- Restrict SSH to a bastion host or SSM/session-manager style access, never a public IP with a static key.
- Keep kubelet’s read-only port disabled (
--read-only-port=0) — it exposes cluster metadata with no authentication if left on. - Rotate node OS images on a schedule (monthly at minimum) so kernel and container runtime CVEs get patched without a manual per-CVE scramble.
If you’re on managed Kubernetes, most of this is handled for you on the control plane side — GKE’s cluster hardening guide is a solid reference for what the provider covers versus what’s still your responsibility on worker nodes. AWS and Azure publish equivalent shared-responsibility breakdowns; check whichever cloud backs your EKS deployment before assuming node hardening is done for you.
Step 12: Layer In a Service Mesh With mTLS
Once RBAC, Pod Security Standards, network policies, and secrets are handled, a service mesh with mutual TLS is the next layer for teams operating at scale. mTLS encrypts and authenticates every service-to-service call inside the cluster, which matters because NetworkPolicies control who can talk to whom but not whether the traffic itself is encrypted and verified. Istio and Linkerd both support automatic mTLS with sidecar injection and require no application code changes.
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: payments
spec:
mtls:
mode: STRICT
Treat this as a Phase 3 control, not a Phase 1 one. A service mesh adds real operational complexity — sidecar resource overhead, an additional control plane to patch, and a new source of latency to debug. Deploy it after the foundational controls above are stable, not instead of them.
Step 13: Automate CIS Benchmark Compliance Scanning
Manual hardening decays. New nodes get added without the hardened base image, new namespaces get created without the Pod Security labels, and six months later half of what you did in Steps 1 through 12 has quietly drifted. Automate the kube-bench scan from Step 1 to run on a schedule, and alert when the pass rate drops.
apiVersion: batch/v1
kind: CronJob
metadata:
name: kube-bench-weekly
namespace: security
spec:
schedule: "0 6 * * 1"
jobTemplate:
spec:
template:
spec:
hostPID: true
containers:
- name: kube-bench
image: aquasec/kube-bench:v0.16.0
command: ["kube-bench", "run", "--targets", "node,policies"]
volumeMounts:
- name: var-lib-etcd
mountPath: /var/lib/etcd
readOnly: true
restartPolicy: Never
volumes:
- name: var-lib-etcd
hostPath:
path: /var/lib/etcd
Pipe the CronJob’s output into your SIEM or a Slack webhook so a failed run gets seen the same day, not discovered during the next annual audit.
Kubernetes Security Tools Compared
There’s no single tool that covers every layer of Kubernetes container security. Most production clusters run a combination of the categories below.
| Tool | Category | License Model | Best For |
|---|---|---|---|
| Falco | Runtime threat detection | Open source (CNCF graduated) | Detecting anomalous process/network activity in real time |
| Kyverno | Policy-as-code admission control | Open source (CNCF) | Teams that prefer YAML policies over Rego |
| OPA Gatekeeper | Policy-as-code admission control | Open source (CNCF graduated) | Teams already standardized on Rego/OPA elsewhere |
| kube-bench | CIS Benchmark compliance scanning | Open source (Aqua Security) | Automated, scheduled compliance checks |
| Trivy | Image vulnerability scanning | Open source (Aqua Security) | CI/CD pipeline scanning before image push |
| Cosign / Sigstore | Image signing and verification | Open source (Linux Foundation) | Supply-chain provenance and signature enforcement |
| Wiz / Prisma Cloud | Cloud-native application protection (CNAPP) | Commercial, per-node or per-workload pricing | Enterprises wanting a single pane across cloud + cluster |
| Sysdig / CrowdStrike Falcon Cloud Security | Runtime detection + posture management | Commercial | Teams needing vendor support and 24/7 detection tuning |
Open source covers every layer described in this tutorial at zero licensing cost, which is why most of the hands-on steps above use it. Commercial CNAPP platforms earn their price when you need a unified dashboard across dozens of clusters and multiple clouds rather than stitching together five open source projects yourself — see our CNAPP comparison for pricing at scale.
Common Pitfalls That Undermine Kubernetes Hardening
These are the mistakes that show up repeatedly in post-incident reviews, even on clusters that technically “did” a hardening pass.
- Enforcing before auditing. Flipping Pod Security Standards or Kyverno policies straight to Enforce mode without an audit period first breaks production deploys and trains teams to see security controls as things that cause outages.
- Default-deny egress without a DNS exception. Every namespace that gets a default-deny NetworkPolicy without an explicit DNS allow rule loses name resolution cluster-wide, and the failure mode looks like a networking bug, not a policy issue — wasting hours of debugging.
- Read-only root filesystem without scratch volumes. Setting
readOnlyRootFilesystem: truewithout mountingemptyDirvolumes for the paths an app actually writes to (temp files, caches, logs) causes immediate crash loops. - Treating RBAC as set-once. Permissions granted during an incident or a one-off migration rarely get revoked afterward. Without a recurring audit, RBAC only ever grows more permissive.
- Ignoring CNI-level NetworkPolicy support. NetworkPolicy YAML applies cleanly and silently does nothing if your CNI plugin doesn’t actually enforce it — always verify enforcement with a test pod-to-pod connection, not just a successful
kubectl apply. - Alert fatigue from untuned Falco rules. Deploying Falco with defaults and never tuning it against real workload behavior produces enough noise that the team starts ignoring the channel entirely, which erases the entire point of runtime detection.
- Forgetting node-level hardening because “the cloud provider handles it.” Managed Kubernetes providers harden the control plane, not your worker node OS images, kubelet configuration, or SSH access — that’s still your job on EKS, AKS, and GKE alike.
Troubleshooting Kubernetes Security Configuration Issues
Here’s what typically goes wrong during and after a hardening rollout, and how to fix it.
- Pods stuck in CreateContainerConfigError after applying Restricted Pod Security Standards. Check the pod’s events with
kubectl describe pod— most commonly the image runs as root by default and needs arunAsUseroverride, or it needs a capability the Restricted profile drops. - DNS resolution breaks after applying default-deny NetworkPolicies. Confirm the DNS egress allow rule from Step 5 is applied to the same namespace and that it targets UDP/TCP port 53 to
kube-system(or wherever your CoreDNS pods live). - NetworkPolicy applies with no error but traffic still isn’t blocked. Your CNI plugin likely doesn’t enforce NetworkPolicy. Confirm with
kubectl get pods -n kube-systemfor Calico, Cilium, or another policy-aware CNI — flannel and basic kubenet setups do not enforce policies by default. - Kyverno policy blocks a legitimate deploy in Enforce mode. Switch the policy back to
Audittemporarily, check the Kyverno policy report for the exact rule that failed, and either fix the image/manifest or scope an exception before re-enabling Enforce. - kube-bench reports FAIL on checks you believe are already fixed. Confirm you’re scanning the right target group (
master,node,etcd,policies) — a common mistake is running the node-targeted job against a control-plane-only node pool and getting false negatives. - Falco floods alerts immediately after install. This is expected with default rules against unfamiliar workloads. Start by muting rules tied to normal CI/CD activity (package installs during builds, for example) rather than disabling entire rule categories.
- Service account token still mounted despite setting automountServiceAccountToken: false on the ServiceAccount. Check whether the pod spec itself has an explicit
automountServiceAccountToken: truethat overrides the ServiceAccount-level setting — pod spec always wins. - Audit logs aren’t reaching the SIEM. Verify the API server actually has
--audit-log-pathand--audit-policy-fileflags set (check withkubectl -n kube-system describe pod kube-apiserver-<node>on self-managed clusters), and confirm your log shipper has read access to that path. - mTLS breaks service-to-service calls after enabling STRICT mode. Any workload outside the mesh (a job that calls an in-cluster service without the sidecar injected) will fail under STRICT. Use
PERMISSIVEmode during rollout so both plaintext and mTLS traffic are accepted, then move to STRICT once every caller has a sidecar.
Advanced Tips for Production-Grade Clusters
Once the baseline above is stable, a few additional practices separate a hardened cluster from a genuinely resilient one.
Rotate encryption keys and certificates on a schedule, not a trigger
Etcd encryption keys, service account signing keys, and TLS certificates should rotate on a calendar cadence rather than only after a suspected compromise. Waiting for a trigger means your rotation process is untested exactly when you need it most.
Run chaos and pen-test exercises against your own hardening
A NetworkPolicy that looks correct on paper isn’t verified until you’ve actually tried to violate it from inside the cluster. Periodically spin up a throwaway pod and attempt lateral movement, privilege escalation, and secrets access against your own hardened namespaces. If it succeeds, you’ve found a real gap before an attacker did.
Tie hardening scores to deployment gates, not dashboards
A kube-bench score or Kyverno policy report that only lives on a dashboard gets ignored under deadline pressure. Wire the CI/CD pipeline to block a deploy if the target namespace fails its Pod Security or Kyverno checks — visibility alone doesn’t change behavior, but a failed pipeline does.
Complete Working Project: A Hardened Baseline Manifest Bundle
Combining every step above into a single applyable bundle gives you a repeatable baseline for any new namespace. Save this as hardened-namespace-baseline.yaml and apply it as the first thing you do when standing up a new namespace, before any workload gets deployed into it.
apiVersion: v1
kind: Namespace
metadata:
name: NAMESPACE_NAME
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
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: default-restricted
namespace: NAMESPACE_NAME
automountServiceAccountToken: false
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: NAMESPACE_NAME
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: NAMESPACE_NAME
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: NAMESPACE_NAME
name: namespace-viewer
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
Run it end to end:
sed 's/NAMESPACE_NAME/checkout-service/g' hardened-namespace-baseline.yaml | kubectl apply -f -
kubectl get networkpolicy -n checkout-service
kubectl get namespace checkout-service --show-labels
Expected output after applying looks like this:
namespace/checkout-service created
serviceaccount/default-restricted created
networkpolicy.networking.k8s.io/default-deny-all created
networkpolicy.networking.k8s.io/allow-dns-egress created
role.rbac.authorization.k8s.io/namespace-viewer created
NAME POD-SELECTOR AGE
allow-dns-egress <none> 2s
default-deny-all <none> 2s
NAME LABELS
checkout-service pod-security.kubernetes.io/audit=restricted,pod-security.kubernetes.io/enforce=restricted,pod-security.kubernetes.io/enforce-version=latest,pod-security.kubernetes.io/warn=restricted
From here, layer Kyverno’s image-signing policy, Falco, and the kube-bench CronJob from Step 13 on top of the cluster as a whole (they’re cluster-scoped, not per-namespace), and every new namespace inherits a hardened starting point instead of the wide-open Kubernetes default.
How to Measure Whether Your Hardening Program Is Actually Working
Kubernetes security hardening is easy to declare “done” and hard to actually verify. A cluster can pass every step in this guide on the day you apply it and still drift back toward risk within a quarter as new namespaces, new engineers, and new third-party charts get added without the same rigor. Track a small set of metrics over time rather than relying on a one-time pass/fail from kube-bench.
| Metric | How to Measure | Healthy Target |
|---|---|---|
| CIS Benchmark pass rate | Weekly kube-bench CronJob output (Step 13) | 95%+ passing, with every FAIL triaged within a week |
| Namespaces without Pod Security labels | kubectl get ns -o json | jq filtering for missing pod-security.kubernetes.io/enforce |
Zero — every namespace inherits the baseline bundle from Step 3 |
| ClusterRoleBindings granting cluster-admin | The RBAC audit query from Step 1, run monthly | Limited to a documented, reviewed short list |
| Namespaces without a default-deny NetworkPolicy | Compare namespace count against NetworkPolicy count per namespace | Zero unprotected app namespaces |
| Falco alert-to-action ratio | Alerts triaged and closed vs. alerts ignored in your incident tracker | Every alert reaches a documented conclusion, even if “benign” |
| Unsigned images admitted | Kyverno policy reports in Audit mode before Enforce cutover | Trending to zero before flipping to Enforce |
None of these numbers matter in isolation — a 95% CIS pass rate on a cluster with three cluster-admin service accounts nobody can explain is not actually hardened, it just looks hardened on one dashboard. Review the full set together, monthly at minimum, and treat any metric moving in the wrong direction as equally urgent as a new CVE. Configuration drift is slower than a zero-day but just as reliable at eventually causing an incident.
Frequently Asked Questions
Is Kubernetes secure by default?
No. Out of the box, Kubernetes ships with permissive networking (no NetworkPolicy blocks anything unless one is created), unencrypted Secrets at rest, and service account tokens automounted into every pod whether needed or not. Security is opt-in and requires the explicit hardening steps covered in this guide.
What’s the difference between Pod Security Standards and OPA Gatekeeper or Kyverno?
Pod Security Standards are a built-in, Kubernetes-native admission control limited to pod-level security fields (privilege escalation, root user, capabilities). Kyverno and OPA Gatekeeper are general-purpose policy engines that can enforce arbitrary rules across any resource type, including image signing, label requirements, and resource limits — they’re complementary, not competing.
Do I need a service mesh for Kubernetes security?
Not as a first step. mTLS via a service mesh adds meaningful protection against internal traffic interception, but it should come after RBAC, Pod Security Standards, network policies, and secrets management are already solid. Adding mesh complexity before the fundamentals are in place tends to create more operational risk than security benefit.
How often should I run a CIS Kubernetes Benchmark scan?
Weekly at minimum via an automated CronJob, as shown in Step 13. Configuration drift happens continuously as new nodes, namespaces, and workloads get added — an annual audit alone will always be finding problems that existed for months.
Does managed Kubernetes (EKS, AKS, GKE) handle security hardening for me?
Partially. Managed providers secure the control plane — API server availability, etcd encryption keys, and control-plane audit logging are typically handled. Worker node OS hardening, RBAC configuration, Pod Security Standards, NetworkPolicies, and workload-level container hardening remain your responsibility regardless of which cloud you use.
What’s the single highest-impact first step if I can only do one thing?
Audit and fix RBAC ClusterRoleBindings granting cluster-admin to service accounts that don’t need it (Steps 1 and 2). Over-permissioned service accounts are the most commonly exploited misconfiguration because they turn a single compromised pod into full cluster control.
Can I use Trivy and Kyverno together?
Yes, and most production setups do. Trivy scans images for known vulnerabilities during CI/CD, before an image is ever pushed to a registry. Kyverno enforces admission-time policy — including verifying that an image was actually signed — at the moment a pod tries to schedule. They cover different points in the pipeline.
Will enforcing Restricted Pod Security Standards break my existing workloads?
Likely, if they weren’t built with it in mind. Legacy images that run as root or require specific Linux capabilities will fail under Restricted. Always deploy in audit and warn mode first (Step 3) to identify what breaks before switching to enforce.