Kubernetes ships with permissive defaults that make getting started easy but leave production clusters wide open. I've seen clusters breached because a single misconfigured pod had cluster-admin privileges or because secrets were stored in plain ConfigMaps. Hardening a production cluster takes a methodical approach—pod security, network isolation, RBAC scoping, and secret encryption all need attention.
This checklist walks through seven configuration changes that close the most common attack vectors.
1. Enforce Pod Security Standards
Pod Security Standards replaced the deprecated PodSecurityPolicy in Kubernetes 1.25. Three levels exist: Privileged (unrestricted), Baseline (blocks the worst), and Restricted (opinionated hardening). Most production workloads should run under Restricted.
Apply standards at the namespace level with labels:
apiVersion: v1
kind: Namespace
metadata:
name: production-apps
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
The enforce label blocks non-compliant pods. audit logs violations. warn notifies at creation time.
Restricted mode requires explicit security contexts in every pod spec:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
This pod runs as a non-root user, drops all Linux capabilities, prevents privilege escalation, and mounts the root filesystem read-only. Most apps need a writable /tmp, so add an emptyDir volume for that.
Start with Baseline on existing clusters to avoid breaking everything, then tighten to Restricted namespace by namespace.
2. Lock Down RBAC with Least Privilege
Role-Based Access Control assigns permissions to users and service accounts. Default service accounts often get cluster-wide read, which is enough to map your entire infrastructure.
Create a dedicated service account per application:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
namespace: production-apps
automountServiceAccountToken: false
Set automountServiceAccountToken: false to prevent the API token from being injected unless the pod explicitly needs cluster API access. Then bind the smallest possible role:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production-apps
name: myapp-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["myapp-config"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-binding
namespace: production-apps
subjects:
- kind: ServiceAccount
name: myapp-sa
namespace: production-apps
roleRef:
kind: Role
name: myapp-role
apiGroup: rbac.authorization.k8s.io
This grants read access to exactly one ConfigMap. No wildcards, no cluster-wide scope.
Audit existing bindings with:
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.subjects[]?.kind=="ServiceAccount") | "\(.metadata.name) -> \(.roleRef.name)"'
Look for cluster-admin bound to service accounts. That's a red flag.
3. Isolate Workloads with Network Policies
By default, every pod can talk to every other pod across all namespaces. Network policies create firewall rules inside the cluster.
Deny all traffic by default:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: production-apps
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Then allow specific paths. For a web app that talks to a database:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production-apps
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-db
namespace: production-apps
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
The backend can reach Postgres and CoreDNS, nothing else. The frontend can reach the backend. Everything else is blocked.
Network policies require a CNI plugin that supports them—Calico, Cilium, or Weave work. The default kubenet does not.
4. Encrypt Secrets at Rest
Kubernetes stores Secrets in etcd as base64-encoded strings, not encrypted. If someone dumps etcd, they get every database password and API key in your cluster.
Enable encryption at rest with an EncryptionConfiguration:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
Generate the key with:
head -c 32 /dev/urandom | base64
Pass the config to the API server with --encryption-provider-config=/etc/kubernetes/enc/config.yaml and restart the API server. Then re-encrypt existing secrets:
kubectl get secrets --all-namespaces -o json | kubectl replace -f -
For managed Kubernetes (EKS, GKE, AKS), encryption at rest is a toggle in the control plane settings. Turn it on.
Better yet, use an external secret manager—AWS Secrets Manager, HashiCorp Vault, Google Secret Manager—and inject secrets at runtime with a CSI driver or init container. That keeps secrets out of etcd entirely.
5. Restrict API Server Access
The API server is the cluster's control plane. If it's exposed to the internet, you will be scanned and probed constantly.
Limit access with:
- IP allowlisting: Configure your cloud provider's firewall to allow only your office IP, VPN ranges, and CI/CD runners.
- Disable anonymous auth: Set
--anonymous-auth=falseon the API server unless you need unauthenticated health checks. - Audit logging: Enable audit logs to see who's calling what. Configure a policy that logs metadata for all requests and request/response bodies for secrets and configmaps:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: Metadata
omitStages:
- RequestReceived
Pass it with --audit-policy-file and --audit-log-path flags. Ship logs to a SIEM or log aggregator.
For self-managed clusters, run the API server behind a bastion host or VPN. For managed clusters, enable private endpoints and disable public access entirely if your CI/CD and operators run inside the VPC.
6. Harden Node Security
Pods inherit the security posture of the underlying node. If the node OS is outdated or runs unnecessary services, attackers can pivot from a compromised pod to the host.
Apply these settings on every worker node:
- Disable SSH password auth: Use key-based authentication only. Set
PasswordAuthentication noin/etc/ssh/sshd_config. - Enable a host firewall: Use
ufworfirewalldto block everything except kubelet (10250), node ports, and your CNI's required ports. - Run a minimal OS: Use a container-optimized distribution like Flatcar, Bottlerocket, or Talos. Less surface area, fewer CVEs.
- Keep the kernel updated: Subscribe to security updates and apply them during maintenance windows.
- Restrict kubelet permissions: Set
--anonymous-auth=falseand--authorization-mode=Webhookon the kubelet to require API server authentication.
For managed node groups, enable automatic security updates and use the latest AMI or node image version.
7. Scan Images and Enforce Policies
A vulnerable base image can undo every other hardening step. Scan every image before it runs in production.
Integrate a scanner into your CI pipeline—Trivy, Grype, and Clair are open-source options. Fail the build if high or critical CVEs are found:
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:1.0
At runtime, enforce policies with an admission controller. Open Policy Agent (OPA) with Gatekeeper or Kyverno can block images from untrusted registries, require specific labels, or enforce resource limits.
Example Gatekeeper policy to allow only images from your private registry:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "myregistry.example.com"
Deploy the constraint, and any pod referencing docker.io or another external registry will be rejected.
What breaks when you harden?
Legacy workloads often assume they can run as root, write anywhere, or talk to everything. Expect pushback from developers and some initial breakage.
The most common issues I've handled:
- Read-only root filesystem: Apps that write logs or temp files to
/varor/appwill crash. Add anemptyDirvolume for writable paths. - Non-root user: Some images bake in
USER root. Rebuild them with a non-root user or override withrunAsUserin the pod spec. - Network policies: Egress to external APIs or databases will be blocked unless you explicitly allow it. Start by logging violations with
auditmode, then enforce. - RBAC too tight: CI/CD pipelines that deploy with
kubectl applyneed create/update/patch permissions. Service accounts for monitoring tools need read access to pods and nodes.
Test in a staging environment first. Roll out changes incrementally, one namespace at a time.
FAQ
Do I need all seven of these configs?
Pod security, RBAC, and network policies are non-negotiable for production. Secret encryption and API server hardening are table stakes. Image scanning and node hardening depend on your compliance requirements and threat model.
Can I use PodSecurityPolicy instead of Pod Security Standards?
No. PodSecurityPolicy was removed in Kubernetes 1.25. Migrate to Pod Security Standards.
What if my CNI doesn't support network policies?
Switch to one that does. Calico and Cilium are drop-in replacements for most clusters. Weave works too.
How do I rotate the encryption key?
Add a new key to the EncryptionConfiguration as the first entry, restart the API server, re-encrypt secrets, then remove the old key.
Should I run a service mesh for security?
Service meshes (Istio, Linkerd) add mutual TLS and fine-grained traffic control, but they're complex. Start with network policies. Add a mesh only if you need zero-trust pod-to-pod encryption or advanced traffic shaping.
Check your current posture first
Before changing anything, audit what you have. Run kubectl get psp to see if deprecated policies are still active. Check kubectl get networkpolicies --all-namespaces for coverage gaps. List service accounts with cluster-admin and challenge every one.
Hardening isn't a one-time task. New workloads, new developers, and new dependencies will chip away at your security posture if you don't enforce policies at admission time and audit regularly. Build these seven configs into your platform baseline so every new namespace and deployment inherits them by default.
![Kubernetes Security Best Practices: 7 Configs [Solved]](/images/blog/kubernetes-security-best-practices-7-configs-solved.jpg)