Kubernetes is the orchestration layer that runs containerized applications across many machines. If you're hosting apps in containers—whether for clients or your own infrastructure—you need to lock down the cluster before anything goes to production. Most default Kubernetes installs leave the front door open.
This guide walks you through eight security fixes that close the most dangerous gaps. We define every term as we go and show working commands you can run today.
What is Kubernetes and why does it need hardening?
Kubernetes (often shortened to K8s) schedules Docker or containerd containers across worker nodes, manages networking between them, and restarts failed pods automatically. A pod is the smallest unit—one or more containers that share storage and network. A node is a physical or virtual machine running the Kubernetes agent.
Out of the box, Kubernetes trusts everything inside the cluster. Any pod can talk to any other pod. The API server often allows anonymous requests. Container images run as root. That trust model works in a lab but becomes a liability the moment you expose a service to the internet or run untrusted workloads.
In support tickets I handled, the usual entry point was either an exposed API server with weak authentication or a container breakout through a privileged pod. Both are preventable.
Fix 1: Disable anonymous API access
The Kubernetes API server is the control plane. Every kubectl command, every pod creation, every service update flows through it. By default, many distributions enable anonymous authentication so you can bootstrap the cluster without certificates.
Check the current setting:
kubectl cluster-info dump | grep anonymous-auth
If you see --anonymous-auth=true, anyone who can reach the API port (usually 6443) can query cluster metadata. Disable it by editing the API server manifest. On most installs that file lives at /etc/kubernetes/manifests/kube-apiserver.yaml.
Add or change this line under spec.containers[0].command:
- --anonymous-auth=false
The kubelet will restart the API server pod automatically. Verify:
curl -k https://your-api-server:6443/version
You should see Unauthorized instead of version JSON. Good.
Fix 2: Enforce role-based access control (RBAC)
RBAC is Kubernetes' permission system. A Role defines what actions (get, list, create, delete) are allowed on which resources (pods, secrets, services). A RoleBinding ties that Role to a user or service account.
Without RBAC, any authenticated entity can do anything. With RBAC, you grant the minimum permission needed.
First confirm RBAC is enabled:
kubectl api-versions | grep rbac
You should see rbac.authorization.k8s.io/v1. If not, add --authorization-mode=RBAC to your API server manifest and restart.
Create a read-only role for developers who need to view logs but not modify deployments:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: log-reader
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list"]
Bind it to a user:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-logs
namespace: production
subjects:
- kind: User
name: alice
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: log-reader
apiGroup: rbac.authorization.k8s.io
Apply both:
kubectl apply -f role.yaml -f binding.yaml
Now alice can read logs but cannot delete pods or access secrets. Every service account and human user should have an explicit RBAC policy. Never hand out cluster-admin unless someone truly needs full control.
Fix 3: Drop root privileges in containers
Most container images run as UID 0 (root) by default. If an attacker escapes the container namespace, they land on the host as root. Prevent that by forcing a non-root user.
In your pod spec, add a security context:
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsNonRoot: true tells Kubernetes to reject the pod if the image would run as UID 0. allowPrivilegeEscalation: false blocks the container from gaining more privileges than its parent process.
If your application legitimately needs to bind to port 80 or 443, use a capability instead of running as root:
securityContext:
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
That grants only the network binding capability and drops everything else. Much safer.
Fix 4: Enable pod security standards
Pod Security Standards (PSS) replaced the older Pod Security Policies in Kubernetes 1.25. PSS defines three levels: Privileged (unrestricted), Baseline (blocks the worst practices), and Restricted (defense in depth).
Apply the Restricted standard to your namespace:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
Now any pod that tries to run as root, mount the host filesystem, or use host networking will be rejected. Existing pods are not evicted, but new ones must comply.
Check what would be blocked without enforcing:
kubectl label namespace staging \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
You'll see warnings in the API response but pods will still deploy. Use audit and warn first to find violations, then switch to enforce.
So what if your app needs host access?
Some workloads—log collectors, monitoring agents, CNI plugins—must interact with the host. Put those in a separate namespace with Privileged or Baseline enforcement and lock down everything else.
Fix 5: Isolate pods with network policies
By default, every pod can reach every other pod in the cluster, even across namespaces. An attacker who compromises one pod can pivot to your database, your secret store, your management tools.
Network policies act as firewall rules inside Kubernetes. They require a CNI plugin that supports them—Calico, Cilium, and Weave all do. The default kubenet does not.
Check if your CNI supports network policies by creating a test policy and seeing if it takes effect. Here's a policy that blocks all ingress by default:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
Apply it:
kubectl apply -f deny-all.yaml
Now nothing can reach pods in the production namespace. Add exceptions for legitimate traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
This allows only pods labeled app=frontend to reach app=api on port 8080. Everything else is dropped.
Start with deny-all policies in each namespace, then add explicit allow rules for each service dependency. It's tedious but blocks lateral movement cold.
Fix 6: Scan and sign container images
Your cluster is only as secure as the images you run. A compromised base image or a dependency with a known vulnerability gives attackers a foothold.
Scan images before deployment with a tool like Trivy:
trivy image myapp:latest
Trivy checks for known CVEs in OS packages and application dependencies. Fix or replace images with HIGH or CRITICAL findings before pushing them to production.
Enforce signed images with an admission controller. Sigstore's policy-controller or Kyverno can reject unsigned images at admission time:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-signature
spec:
validationFailureAction: enforce
rules:
- name: check-signature
match:
resources:
kinds:
- Pod
verifyImages:
- image: "myregistry.example.com/*"
key: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
Now only images signed with your private key will deploy. An attacker who compromises your registry can't inject a backdoored image without the signing key.
Fix 7: Restrict the Kubernetes dashboard
The web-based Kubernetes Dashboard is convenient for browsing resources but dangerous if exposed. Many clusters ship it with a service account that has cluster-admin privileges.
Check if it's installed:
kubectl get pods -n kubernetes-dashboard
If you see a dashboard pod, review its service account permissions:
kubectl describe sa kubernetes-dashboard -n kubernetes-dashboard
kubectl get clusterrolebinding | grep kubernetes-dashboard
If you find a binding to cluster-admin, delete it and create a narrower role. Better yet, expose the dashboard only through kubectl proxy on your local machine, never over the internet:
kubectl proxy
Then access it at http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/. No public IP, no TLS certificate hassles, no risk of credential stuffing.
Fix 8: Audit and monitor API activity
Enable audit logging on the API server so you have a record of every action: who created that pod, who deleted that secret, who tried to escalate privileges.
Create an audit policy file, /etc/kubernetes/audit-policy.yaml:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: Metadata
omitStages:
- RequestReceived
Tell the API server to use it by adding these flags in /etc/kubernetes/manifests/kube-apiserver.yaml:
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
Restart the API server. Logs land in /var/log/kubernetes/audit.log in JSON format. Ship them to a SIEM or log aggregator for alerting. Watch for repeated authentication failures, privilege escalation attempts, and secret access by unexpected service accounts.
What to check first
If you walk away with one task, disable anonymous authentication and enable RBAC. Those two changes force every request to prove identity and permission, blocking the most common cluster compromises.
From there, layer on the other fixes as your workloads and team grow. Security is a ratchet—once you enforce a control, leaving it in place costs nothing and every new app benefits.
Kubernetes gives you the primitives to build a secure platform. You just have to turn them on.
