Production Kubernetes Hardening Guide: From Cluster to Pods

A SRE and DevOps checklist for securing production Kubernetes clusters using Pod Security Standards, default-deny NetworkPolicies, and RBAC least privilege.

KubernetesDevOpsSecurityPlatform Engineering

Running Kubernetes in production requires much more than getting kubectl apply to succeed. By default, a freshly provisioned Kubernetes cluster prioritizes developer convenience over isolation—pods can communicate freely across namespaces, workloads can run as root, and service account tokens are automatically mounted into every container.

In this guide, we walk through the defence-in-depth controls every Cloud and Platform Engineer should enforce before promoting a cluster to production.

1. Enforce Pod Security Standards (PSS)#

Kubernetes replaced the deprecated PodSecurityPolicy with built-in Pod Security Admission (PSA). At the namespace level, you can enforce three profiles: privileged, baseline, and restricted.

For all stateless application workloads, enforce the restricted standard:

namespace-hardening.yaml
yaml
1apiVersion: v1
2kind: Namespace
3metadata:
4 name: production-workloads
5 labels:
6 pod-security.kubernetes.io/enforce: restricted
7 pod-security.kubernetes.io/enforce-version: latest
8 pod-security.kubernetes.io/warn: restricted
9 pod-security.kubernetes.io/audit: restricted

Hardened Deployment SecurityContext#

Every production Deployment should explicitly drop Linux capabilities, run as a non-root UID, and mount the root filesystem as read-only:

hardened-deployment.yaml
yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: payment-api
5 namespace: production-workloads
6spec:
7 replicas: 3
8 selector:
9 matchLabels:
10 app: payment-api
11 template:
12 metadata:
13 labels:
14 app: payment-api
15 spec:
16 automountServiceAccountToken: false
17 securityContext:
18 runAsNonRoot: true
19 runAsUser: 10001
20 runAsGroup: 10001
21 fsGroup: 10001
22 seccompProfile:
23 type: RuntimeDefault
24 containers:
25 - name: api
26 image: ghcr.io/org/payment-api:v1.8.2
27 securityContext:
28 allowPrivilegeEscalation: false
29 readOnlyRootFilesystem: true
30 capabilities:
31 drop:
32 - ALL
33 resources:
34 requests:
35 cpu: "250m"
36 memory: "256Mi"
37 limits:
38 cpu: "500m"
39 memory: "512Mi"
40 volumeMounts:
41 - name: tmp-dir
42 mountPath: /tmp
43 volumes:
44 - name: tmp-dir
45 emptyDir:
46 medium: Memory
47 sizeLimit: 64Mi

2. Zero-Trust Networking with Default-Deny NetworkPolicies#

By default, Kubernetes uses a flat network where any pod in namespace-a can open a TCP socket to any pod in namespace-b. Start every namespace with a Default Deny All ingress and egress policy, then whitelist only required traffic paths.

default-deny-all.yaml
yaml
1apiVersion: networking.k8s.io/v1
2kind: NetworkPolicy
3metadata:
4 name: default-deny-all
5 namespace: production-workloads
6spec:
7 podSelector: {}
8 policyTypes:
9 - Ingress
10 - Egress
11---
12apiVersion: networking.k8s.io/v1
13kind: NetworkPolicy
14metadata:
15 name: allow-dns-egress
16 namespace: production-workloads
17spec:
18 podSelector: {}
19 policyTypes:
20 - Egress
21 egress:
22 - to:
23 - namespaceSelector:
24 matchLabels:
25 kubernetes.io/metadata.name: kube-system
26 podSelector:
27 matchLabels:
28 k8s-app: kube-dns
29 ports:
30 - protocol: UDP
31 port: 53
32 - protocol: TCP
33 port: 53

3. Auditing RBAC & Least-Privilege Access#

Avoid binding cluster-admin or wildcard verbs (*) to CI/CD service accounts or human operators. Scope permissions to specific namespaces using Role and RoleBinding rather than ClusterRoleBinding.

rbac-audit.sh — Inspecting High-Risk Bindings
bash
$kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'
$kubectl auth can-i --list --as=system:serviceaccount:production-workloads:default -n production-workloads

Hardening Checklist Comparison#

Control LayerDefault K8s BehaviorHardened Production State
Container UserRuns as root (UID 0)Enforced runAsNonRoot: true (UID > 10000)
Root FilesystemWritable container layerreadOnlyRootFilesystem: true + emptyDir /tmp
Pod-to-Pod TrafficUnrestricted flat networkDefault-deny NetworkPolicy + explicit allowlists
ServiceAccount TokenAuto-mounted in all podsautomountServiceAccountToken: false by default
Linux CapabilitiesDefault Docker/containerd setdrop: ["ALL"] + seccompProfile: RuntimeDefault

4. Automated Policy Verification in CI/CD#

Rather than waiting until runtime to discover a misconfigured manifest, shift validation left in your GitHub Actions or GitLab CI pipeline using kubeconform and trivy:

ci-verify-manifests.sh
bash
1#!/usr/bin/env bash
2set -euo pipefail
3
4echo "==> Validating Kubernetes schema compliance..."
5kubeconform -strict -summary ./k8s/production/
6
7echo "==> Scanning manifests for security misconfigurations..."
8trivy config --severity HIGH,CRITICAL --exit-code 1 ./k8s/production/