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:
1apiVersion: v12kind: Namespace3metadata:4 name: production-workloads5 labels:6 pod-security.kubernetes.io/enforce: restricted7 pod-security.kubernetes.io/enforce-version: latest8 pod-security.kubernetes.io/warn: restricted9 pod-security.kubernetes.io/audit: restrictedHardened Deployment SecurityContext#
Every production Deployment should explicitly drop Linux capabilities, run as a non-root UID, and mount the root filesystem as read-only:
1apiVersion: apps/v12kind: Deployment3metadata:4 name: payment-api5 namespace: production-workloads6spec:7 replicas: 38 selector:9 matchLabels:10 app: payment-api11 template:12 metadata:13 labels:14 app: payment-api15 spec:16 automountServiceAccountToken: false17 securityContext:18 runAsNonRoot: true19 runAsUser: 1000120 runAsGroup: 1000121 fsGroup: 1000122 seccompProfile:23 type: RuntimeDefault24 containers:25 - name: api26 image: ghcr.io/org/payment-api:v1.8.227 securityContext:28 allowPrivilegeEscalation: false29 readOnlyRootFilesystem: true30 capabilities:31 drop:32 - ALL33 resources:34 requests:35 cpu: "250m"36 memory: "256Mi"37 limits:38 cpu: "500m"39 memory: "512Mi"40 volumeMounts:41 - name: tmp-dir42 mountPath: /tmp43 volumes:44 - name: tmp-dir45 emptyDir:46 medium: Memory47 sizeLimit: 64Mi2. 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.
1apiVersion: networking.k8s.io/v12kind: NetworkPolicy3metadata:4 name: default-deny-all5 namespace: production-workloads6spec:7 podSelector: {}8 policyTypes:9 - Ingress10 - Egress11---12apiVersion: networking.k8s.io/v113kind: NetworkPolicy14metadata:15 name: allow-dns-egress16 namespace: production-workloads17spec:18 podSelector: {}19 policyTypes:20 - Egress21 egress:22 - to:23 - namespaceSelector:24 matchLabels:25 kubernetes.io/metadata.name: kube-system26 podSelector:27 matchLabels:28 k8s-app: kube-dns29 ports:30 - protocol: UDP31 port: 5332 - protocol: TCP33 port: 533. 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.
$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-workloadsHardening Checklist Comparison#
| Control Layer | Default K8s Behavior | Hardened Production State |
|---|---|---|
| Container User | Runs as root (UID 0) | Enforced runAsNonRoot: true (UID > 10000) |
| Root Filesystem | Writable container layer | readOnlyRootFilesystem: true + emptyDir /tmp |
| Pod-to-Pod Traffic | Unrestricted flat network | Default-deny NetworkPolicy + explicit allowlists |
| ServiceAccount Token | Auto-mounted in all pods | automountServiceAccountToken: false by default |
| Linux Capabilities | Default Docker/containerd set | drop: ["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:
1#!/usr/bin/env bash2set -euo pipefail34echo "==> Validating Kubernetes schema compliance..."5kubeconform -strict -summary ./k8s/production/67echo "==> Scanning manifests for security misconfigurations..."8trivy config --severity HIGH,CRITICAL --exit-code 1 ./k8s/production/