What each risk means, a command to check your own cluster for it, and how to fix it.
The OWASP Kubernetes Top 10 lists the most common ways clusters get compromised. This post goes through the 2025 edition one risk at a time. Each one has the same layout: what it is, how to check for it, and how to fix it. Where a risk is covered in depth in my Kubernetes series, I link to it.
| ID | Risk | One-line fix |
|---|---|---|
| K01 | Insecure workload configurations | Non-root, no privileged, drop capabilities |
| K02 | Overly permissive authorization | Least-privilege RBAC |
| K03 | Secrets management failures | Encrypt, restrict, use a secrets manager |
| K04 | Lack of cluster-level policy enforcement | Admission policies, Pod Security Admission |
| K05 | Missing network segmentation | Default-deny NetworkPolicies |
| K06 | Overly exposed components | Keep API server, kubelet, etcd private |
| K07 | Misconfigured and vulnerable components | Patch, and run kube-bench |
| K08 | Cluster to cloud lateral movement | Workload identity, block metadata service |
| K09 | Broken authentication | SSO with MFA, short-lived tokens |
| K10 | Inadequate logging and monitoring | Audit logs, central logging, runtime alerts |
Pods run with more power than they need: as root, privileged, with host access, or with a writable filesystem. If an attacker gets code execution in such a Pod, escaping to the node is easy. This is the most common finding in real clusters.
# Pods running privileged
kubectl get pods -A -o json | jq -r '.items[]
| select(.spec.containers[].securityContext.privileged==true)
| .metadata.namespace + "/" + .metadata.name'
# Scan manifests for risky settings
kube-linter lint ./k8s/
kubesec scan deployment.yaml
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
privileged: true, hostPath, hostNetwork, hostPID unless you can explain why.RBAC rules that give users or ServiceAccounts more than they need: cluster-admin everywhere, wildcards (*), or powerful verbs. A stolen ServiceAccount token is only as dangerous as its role.
These permissions are almost as good as admin: create pods (can start a privileged Pod), get or list secrets, pods/exec, escalate, bind, impersonate.
# Who is bound to cluster-admin?
kubectl get clusterrolebindings -o json | jq -r '.items[]
| select(.roleRef.name=="cluster-admin")
| .metadata.name + " -> " + ([.subjects[]?.name] | join(","))'
# What can this ServiceAccount do?
kubectl auth can-i --list --as=system:serviceaccount:prod:web -n prod
# Who can read secrets? (krew plugin)
kubectl who-can get secrets -n prod
Role and RoleBinding instead of ClusterRole where you can.default one for real work.automountServiceAccountToken: false on Pods that never call the API.Kubernetes Secrets are only base64-encoded, not encrypted. By default they sit in etcd in readable form, and anyone with get secrets permission can read them. Secrets also leak through Git repositories, environment variables printed in logs, and container images.
# Long-lived token Secrets (should be rare)
kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
# Secrets committed to your repo
gitleaks detect --source .
EncryptionConfiguration, ideally with a KMS provider). On managed clusters, enable envelope encryption.get and list Secrets (K02).kubectl describe, crash dumps and logs.Good practices that are only written in a document do not stop anyone. Without a policy that the cluster enforces, one team's mistake (a privileged Pod, an image from a public registry) goes straight through.
# Is Pod Security Admission configured on your namespaces?
kubectl get ns --show-labels | grep pod-security
# Is a policy engine installed?
kubectl get pods -A | grep -Ei "kyverno|gatekeeper"
# Built in: enforce the "restricted" Pod Security Standard
kubectl label ns prod \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted
By default, every Pod can talk to every other Pod, and out to the internet. One compromised Pod can then scan and attack everything else.
# Namespaces with no NetworkPolicy at all
kubectl get networkpolicy -A
# Compare with: kubectl get ns
# Deny all traffic in the namespace, then allow only what is needed
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: prod
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
Control plane and node components reachable from places they should not be: an API server open to the internet, a kubelet that accepts anonymous requests, an etcd without authentication, an exposed dashboard, or an internal service published with a LoadBalancer or NodePort.
# Services exposed outside the cluster
kubectl get svc -A | grep -E "LoadBalancer|NodePort"
# From outside your network (only on clusters you own):
nmap -p 6443,10250,2379 <node-or-api-ip>
--anonymous-auth=false and --authorization-mode=Webhook.LoadBalancer and NodePort Service. Use an Ingress with TLS for web apps.Old Kubernetes versions, unpatched ingress controllers, container runtimes with known escapes, and insecure settings on the components themselves. Real examples include the runc escape CVE-2024-21626 and the ingress-nginx flaws known as IngressNightmare (CVE-2025-1974).
# Versions of the cluster and nodes
kubectl version
kubectl get nodes -o wide
# Compare against the CIS Kubernetes Benchmark
kube-bench run
A compromised Pod reaches the cloud account behind the cluster. The usual path: the Pod calls the node's metadata service (169.254.169.254) and gets the node's cloud credentials. If the node role is broad, the attacker now has access to storage, databases and other cloud services, well beyond the cluster.
# From inside a test Pod: can it reach the metadata service?
kubectl run test --rm -it --image=curlimages/curl --restart=Never -- \
curl -s -m 3 http://169.254.169.254/latest/meta-data/
If that returns data, any compromised Pod could do the same.
Weak ways of proving who you are: shared kubeconfig files, static tokens, long-lived ServiceAccount token Secrets, client certificates (Kubernetes cannot revoke them), or anonymous access left on.
# Can an unauthenticated user do anything?
kubectl auth can-i --list --as=system:anonymous
kubectl auth can-i --list --as=system:unauthenticated
# Legacy long-lived ServiceAccount tokens
kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token
If you cannot see what happened, you cannot detect an attack or investigate one. Many Kubernetes breaches are found by someone outside the company, weeks later.
kubectl exec into a production Pod?Run these on a cluster you own. They map to the risks above and show where you stand:
kube-bench run for cluster settings (K07).can-i commands from K02, and kubectl who-can get secrets (K02, K03).kube-linter lint and trivy config on your manifests (K01).kubectl get networkpolicy -A and kubectl get ns --show-labels (K04, K05).kubectl get svc -A filtered for LoadBalancer and NodePort (K06).