Most Kubernetes compromises trace back to the same handful of misconfigurations. I see them in incident reports, in customer clusters, and in public post-mortems. The attack paths are predictable.
This article walks through eight specific fixes that address the policy gaps showing up in production breaches right now. Each one includes the commands and manifests you need to lock down your cluster.
1. Lock down RBAC to the minimum scope
Default service accounts get more permissions than they need. An application pod rarely needs to list secrets across namespaces or create new deployments, but too many clusters grant those permissions anyway.
Start by auditing what each service account can do:
kubectl auth can-i --list --as=system:serviceaccount:default:default
That command shows every API action the default service account in the default namespace is allowed to perform. If you see cluster-wide permissions for verbs like create, delete, or patch on sensitive resources, tighten them.
Create a least-privilege role for each application. Here's a minimal read-only role for a service that only needs to read ConfigMaps and Secrets in its own namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-reader
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list"]
Bind it to a dedicated service account, not the default one:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-reader-binding
namespace: production
subjects:
- kind: ServiceAccount
name: myapp-sa
namespace: production
roleRef:
kind: Role
name: app-reader
apiGroup: rbac.authorization.k8s.io
Disable automounting of service account tokens for pods that don't need API access at all:
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-api-sa
automountServiceAccountToken: false
In support tickets I've handled, the usual path from container escape to cluster takeover was an overprivileged service account token mounted into a compromised pod.
2. Enforce Pod Security Standards at the namespace level
Kubernetes replaced PodSecurityPolicy with Pod Security Standards. These are three levels: Privileged, Baseline, and Restricted.
Most workloads should run under Restricted. That profile blocks privileged containers, host namespace access, and insecure capabilities.
Label your namespaces to enforce the policy:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
The enforce label rejects non-compliant pods. The audit and warn labels log violations without blocking them, useful during a transition period.
If a workload legitimately needs host access or extra capabilities, create a separate namespace with a Baseline policy and document why. Don't lower the bar cluster-wide.
Check what the Restricted profile actually blocks:
kubectl explain podsecuritystandards.restricted
Common failures after applying Restricted: containers running as root, missing runAsNonRoot: true, or requesting CAP_NET_RAW. Fix those in your Deployment manifests.
3. Segment workloads with Network Policies
By default, every pod can talk to every other pod. An attacker who compromises one service can immediately pivot to your database, your secrets store, or your control plane.
Network policies act as a firewall inside the cluster. Start with a default-deny rule in each namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
That blocks all traffic unless explicitly allowed. Then add rules for legitimate flows.
Allow your frontend to reach your API:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Allow egress to DNS and external HTTPS:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-and-https
namespace: production
spec:
podSelector: {}
egress:
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: UDP
port: 53
- to:
- podSelector: {}
ports:
- protocol: TCP
port: 443
Test your policies by exec-ing into a pod and trying to curl a service that should be blocked. If the connection hangs or times out, the policy is working.
Not all CNI plugins enforce Network Policies. Check yours first.
What if runtime protection isn't in place?
You can block known-bad manifests at admission time, but runtime security catches threats after they're inside the cluster. A container might start clean and then download malicious binaries, spawn a shell, or exfiltrate data.
4. Deploy a runtime security tool
Runtime security monitors system calls, process trees, and file access inside containers. Tools like Falco detect anomalous behavior even if the initial image was clean.
Install Falco with Helm:
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
--namespace falco --create-namespace \
--set tty=true
Falco ships with default rules that catch common exploits: unexpected shells, writes to /etc, privilege escalation attempts. You can add custom rules for your environment.
Example rule to alert on any shell spawned in a production namespace:
- rule: Shell Spawned in Production
desc: Detects shell execution in production pods
condition: >
spawned_process and
container and
k8s.ns.name = "production" and
proc.name in (sh, bash, dash, zsh)
output: >
Shell spawned in production (user=%user.name command=%proc.cmdline
container=%container.name namespace=%k8s.ns.name)
priority: WARNING
Connect Falco alerts to your monitoring stack so they don't get lost in logs. A shell in a production pod is worth waking someone up.
5. Restrict API server access
The API server is the front door to your cluster. If an attacker can reach it from the internet without authentication, they'll try credential brute-forcing and known exploits.
First, check if your API server is publicly exposed:
kubectl cluster-info
If the control plane endpoint is a public IP, lock it down. Use a firewall or security group to allow access only from trusted networks: your office, your VPN, your CI/CD runner IPs.
Disable anonymous auth:
kubectl get configmap kube-apiserver -n kube-system -o yaml | grep anonymous-auth
You want --anonymous-auth=false in the API server flags. Anonymous users shouldn't be able to query the API at all.
Enable audit logging so you have a record of who did what:
apiVersion: v1
kind: Pod
metadata:
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --audit-log-path=/var/log/kube-audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
Audit logs are verbose. Ship them to a SIEM or log aggregator, don't just leave them on the control plane node.
6. Scan images before they run
A vulnerability scanner finds known CVEs in your container images. Scanning at build time stops bad images from reaching the cluster in the first place.
Integrate a scanner into your CI pipeline. Most tools support this:
trivy image --severity HIGH,CRITICAL myregistry.com/myapp:latest
Fail the build if critical vulnerabilities are present. Don't deploy the image until they're patched.
Use an admission controller to enforce scanning at deploy time too. Something like OPA Gatekeeper or Kyverno can block images that lack a recent scan result or contain high-severity CVEs.
Example Kyverno policy:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-scan
spec:
validationFailureAction: enforce
rules:
- name: check-scan-annotation
match:
resources:
kinds:
- Pod
validate:
message: "Image must have a scan annotation"
pattern:
metadata:
annotations:
image-scan-status: "passed"
Scanning doesn't catch zero-day exploits or logic bugs, but it stops you from deploying last year's Bash or OpenSSL vulnerabilities.
7. Rotate secrets and limit their lifespan
Hardcoded secrets in manifests show up in Git history and ConfigMaps. Use a secrets management system instead: Sealed Secrets, External Secrets Operator, or a vault.
External Secrets Operator pulls secrets from an external source at runtime:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-secret
data:
- secretKey: password
remoteRef:
key: database/production
property: password
Secrets never live in Git. They're fetched from Vault or AWS Secrets Manager when the pod starts.
Rotate secrets on a schedule. A database password that hasn't changed in three years is a time bomb. Automate rotation where possible.
Limit secret access with RBAC. Not every service account needs to read every secret in the namespace.
8. Harden the underlying nodes
Kubernetes runs on Linux. If the host OS is misconfigured, container escapes get a lot easier.
Disable unnecessary services on worker nodes. You don't need a print server or Avahi running there.
systemctl list-unit-files --type=service --state=enabled
Patch the kernel regularly. Container escapes often exploit kernel vulnerabilities.
Use a minimal OS designed for containers: Flatcar, Bottlerocket, or Talos. These distributions strip out everything that isn't needed to run Kubernetes, reducing the attack surface.
Enable kernel hardening flags:
sysctl -w kernel.dmesg_restrict=1
sysctl -w kernel.kptr_restrict=2
sysctl -w net.ipv4.conf.all.rp_filter=1
Restrict SSH access to nodes. Better yet, use an immutable infrastructure pattern where nodes are replaced, not patched. If you SSH into a node to debug, document what you did and update your automation.
How do I prioritize these fixes?
Start with RBAC and Pod Security Standards. Those two changes block the most common privilege escalation paths.
Add Network Policies next. Lateral movement is how small breaches become total compromises.
Then layer in runtime security and image scanning. Those catch threats that slip through the earlier controls.
Finally, audit your API server access, rotate secrets, and harden the nodes. These are important but have less immediate impact than the first four.
Do I need all eight fixes?
Yes, if you're running production workloads. Each control addresses a different stage of the attack chain.
Can I test these changes in a non-prod cluster first?
You should. Apply each policy to a dev or staging namespace first. Check that your workloads still start, that CI/CD pipelines still deploy, and that nothing breaks in unexpected ways.
What if my CNI doesn't support Network Policies?
Switch to one that does: Calico, Cilium, or Weave Net. Running without network segmentation is a risk you shouldn't accept.
How often should I audit RBAC permissions?
Every quarter at a minimum. Service accounts accumulate permissions over time as teams add features. Regular audits catch drift.
What's the performance impact of runtime security tools?
Minimal if configured correctly. Falco's overhead is typically under five percent CPU. Image scanning happens at build and deploy time, not during runtime.
What to check first
If you're not sure where your cluster stands, run these three checks right now.
List all ClusterRoles bound to default service accounts:
kubectl get clusterrolebindings -o json | \
jq '.items[] | select(.subjects[]?.name=="default") | .metadata.name'
Check if Pod Security Standards are enforced anywhere:
kubectl get namespaces -o json | \
jq '.items[] | select(.metadata.labels["pod-security.kubernetes.io/enforce"] != null)'
See if default-deny Network Policies exist:
kubectl get networkpolicies --all-namespaces | grep default-deny
If any of those commands return empty results, start there. Those gaps are the ones attackers look for first.
