Kubernetes default configurations are optimized for ease of use, not security. Out of the box, any pod can communicate with any other pod, containers run as root, there are no resource limits, service accounts have broad permissions, and the Kubernetes API is accessible from within pods. A single compromised container in a default Kubernetes cluster can escalate to full cluster compromise in minutes.
This guide covers the security hardening measures that every production Kubernetes cluster needs. We've organized them by impact: start at the top and work your way down. Each section includes the specific configuration you need to apply.
Pod Security Standards: The Foundation
Pod Security Standards (PSS) replaced the deprecated PodSecurityPolicy in Kubernetes 1.25. PSS defines three security profiles β Privileged (unrestricted), Baseline (sensible defaults), and Restricted (maximum security) β and enforces them at the namespace level.
# Apply Restricted Pod Security Standard to production namespaces
# This prevents: running as root, privilege escalation, host networking,
# host path mounts, and many other dangerous configurations.
apiVersion: v1
kind: Namespace
metadata:
name: production
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
---
# Pod that complies with Restricted standard
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: production
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/app:v1.2.3@sha256:abc123...
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
ports:
- containerPort: 8080
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
automountServiceAccountToken: false # Don't mount SA token unless needed
Network Policies: Zero-Trust Networking
By default, every pod can communicate with every other pod in the cluster. Network Policies implement zero-trust networking: deny all traffic by default, then explicitly allow only the traffic that's needed.
# Default deny all ingress and egress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Applies to all pods in the namespace
policyTypes:
- Ingress
- Egress
---
# Allow backend API to receive traffic from frontend only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend-api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
# Allow backend to connect to database and external APIs
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-egress
namespace: production
spec:
podSelector:
matchLabels:
app: backend-api
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
# Allow database access
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
# Allow external API calls (HTTPS only)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # Block internal network access
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443
RBAC Hardening: Principle of Least Privilege
Kubernetes RBAC controls who can do what in the cluster. The default cluster-admin ClusterRole has unrestricted access to everything β if compromised, the attacker owns the entire cluster. Every user and service account should have the minimum permissions needed.
# Bad: Giving developers cluster-admin access
# kubectl create clusterrolebinding dev-admin # --clusterrole=cluster-admin --user=developer@company.com
# Good: Namespace-scoped role with specific permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: staging
name: developer
rules:
- apiGroups: ["", "apps"]
resources: ["pods", "deployments", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["pods/log", "pods/exec"]
verbs: ["get", "create"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"] # Can read secrets but not create/modify
---
# Service account for CI/CD with minimal deployment permissions
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: ci-deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
resourceNames: ["backend-api", "frontend"] # Only specific deployments
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
Image Security: Trust Nothing from the Internet
Container images are the primary vector for supply chain attacks in Kubernetes. Essential controls:
Use image digests, not tags. Tags are mutable β nginx:latest today is different from nginx:latest tomorrow. Use digests: nginx@sha256:abc123.... This guarantees you're running the exact image you tested.
Scan images in CI/CD. Run Trivy, Grype, or Snyk on every image before deployment. Fail the pipeline on critical or high vulnerabilities. Scan both your application images and your base images.
Use minimal base images. alpine (5MB) or distroless (2MB) instead of ubuntu (75MB). Fewer packages = fewer vulnerabilities. Distroless images don't even include a shell, making post-exploitation significantly harder.
Enforce allowed registries. Use admission controllers (OPA/Gatekeeper or Kyverno) to block images from untrusted registries. Only allow images from your private registry and verified public registries.
# Kyverno policy: Only allow images from trusted registries
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-image-registries
spec:
validationFailureAction: Enforce
rules:
- name: validate-registries
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Images must be from approved registries"
pattern:
spec:
containers:
- image: "registry.example.com/* | ghcr.io/yourorg/*"
initContainers:
- image: "registry.example.com/* | ghcr.io/yourorg/*"
Secrets Management: Don't Put Secrets in ConfigMaps
Kubernetes Secrets are base64-encoded, not encrypted. Anyone with get secrets RBAC permission can read them. For production, use external secret management:
Option 1: External Secrets Operator β Syncs secrets from Vault, AWS Secrets Manager, or Azure Key Vault into Kubernetes Secrets. The Git repo contains only references, not secret values.
Option 2: Sealed Secrets β Encrypt secrets with a cluster-specific public key. Only the controller in the cluster can decrypt them. Safe to commit to Git.
Option 3: CSI Secrets Store Driver β Mounts secrets from external stores as files in pods. No Kubernetes Secret object is created β secrets go directly from the external store to the pod filesystem.
Runtime Security: Detect and Respond
Everything above is preventive. Runtime security is detective β it monitors running containers for suspicious behavior and alerts or blocks in real-time. Falco (CNCF) is the standard tool for Kubernetes runtime security.
Deploy Falco as a DaemonSet on every node. It monitors syscalls using eBPF and detects: shell spawning inside containers, unauthorized process execution, file access to sensitive paths (/etc/shadow, /proc), network connections to known malicious IPs, privilege escalation attempts, and cryptominer deployment.
Audit Logging: Know What Happened
Enable Kubernetes audit logging to track all API server requests. This is essential for: investigating security incidents (who deployed that container?), compliance requirements (who accessed this namespace?), and detecting unauthorized access patterns.
# Audit policy: log security-relevant events
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Log all changes to pods, deployments, services
- level: RequestResponse
resources:
- group: ""
resources: ["pods", "services", "secrets", "configmaps"]
- group: "apps"
resources: ["deployments", "statefulsets", "daemonsets"]
verbs: ["create", "update", "patch", "delete"]
# Log all RBAC changes
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# Log authentication failures
- level: Metadata
stages: ["ResponseComplete"]
omitStages: ["RequestReceived"]
# Don't log read-only requests to reduce volume
- level: None
verbs: ["get", "list", "watch"]
resources:
- group: ""
resources: ["events"]
Security Checklist for Production Clusters
Use this checklist before deploying to production: β Pod Security Standards enforced (Restricted for production). β Network Policies deny all by default. β RBAC follows least privilege (no cluster-admin for humans). β Images use digests and are scanned for vulnerabilities. β Images come only from approved registries. β Secrets are managed externally (not plain Kubernetes Secrets). β Runtime security monitoring deployed (Falco or equivalent). β Audit logging enabled and forwarded to SIEM. β etcd is encrypted at rest. β API server authentication uses OIDC (not static tokens). β Nodes are hardened and auto-updated.
ZeonEdge provides Kubernetes security assessments, hardening implementation, and ongoing monitoring. View our security services.
Sarah Chen
Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.