← All posts

Common Kubernetes misconfigurations

Kubernetes CVEs make headlines, but they're rarely how we get in. What gets us in is the cluster shipped with defaults nobody revisited: an API left reachable, a token that can do more than it should, a secret sitting in plaintext. The list below is the short version of the OWASP Kubernetes Top 10: the categories we actually hit, with a real check for each so you can run it against your own cluster instead of taking our word for it.

All commands below assume a kubectl context with ordinary namespace-level access: no cluster-admin required to find out whether you should be worried.

1. Overly exposed cluster componentsHIGH

The API server, kubelet, etcd, and dashboard are all designed to be reached from inside the cluster network, not from the internet. We still find kubelets answering unauthenticated on 10250, and dashboards deployed with no auth in front of them at all. That second one isn't hypothetical: it's how Tesla's cloud environment ended up mining cryptocurrency in 2018, an internet-facing dashboard with no credential required.

$ curl -sk https://<node-ip>:10250/pods | jq '.items | length'
14

A number back instead of a connection refused or a 401 means anyone on that network segment can list every pod on the node, often enough to pivot further.

2. Broken authenticationHIGH

--anonymous-auth defaults to true on the API server unless someone turns it off, and every pod gets a service account token auto-mounted whether the workload needs the API or not. Both are "works out of the box" defaults that widen your attack surface for free.

$ kubectl auth can-i --list --as=system:anonymous
Resources                                       Verbs
selfsubjectaccessreviews.authorization.k8s.io   [create]
...

If that list is longer than the two selfsubjectreview entries every cluster ships with, unauthenticated callers can do more than check their own permissions.

3. Overly permissive RBACHIGH

cluster-admin is meant for people setting up the cluster, not for the CI service account that only needs to roll out one deployment. We routinely find it bound to service accounts because a role that actually scoped the permissions was never written: the binding was the fast way to get past a permission error during setup, and it stayed.

$ kubectl get clusterrolebindings -o json \
    | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?.name'
system:masters
ci-deploy-sa
argocd-application-controller

Any name in that list beyond the cluster's own control-plane identities is a token that, if leaked, hands over the whole cluster, not just the namespace it lives in.

4. Secrets management failuresMEDIUM

A Kubernetes Secret is base64, not encryption. Without encryption-at-rest configured on the API server, secrets sit in etcd as plaintext with a thin layer of encoding on top, readable by anyone with etcd access or a backup of it.

$ kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
Sup3rSecretPassw0rd!

If that round-trips to plaintext in one command, so does every backup, log scrape, or etcd snapshot that ever touches that value.

5. Missing network segmentationMEDIUM

Without a NetworkPolicy, every pod in the cluster can talk to every other pod by default: the internal network is flat. A bug in one low-value service becomes a straight line to your database, with nothing in between to slow it down.

$ kubectl get networkpolicy -A
No resources found

An empty result means there's no segmentation at all: a single compromised pod can reach anything else running in the cluster.

6. Insecure workload configuration and cloud lateral movementCRITICAL

Privileged containers, hostPath mounts of the host filesystem, and hostNetwork all trade container isolation for convenience during development, and then ship to production unchanged. On a cloud-hosted cluster the worst version of this chains into the cloud account itself: a pod that can reach the instance metadata service can often mint the node's cloud credentials and step out of Kubernetes entirely.

$ kubectl get pods -A -o json \
    | jq -r '.items[] | select(.spec.containers[].securityContext.privileged==true) | .metadata.name'
legacy-log-shipper

$ kubectl run probe --rm -it --image=curlimages/curl --restart=Never -- \
    curl -sf http://169.254.169.254/latest/meta-data/iam/security-credentials/
node-instance-role

A privileged pod is one syscall away from the node it runs on. A pod that can reach 169.254.169.254 (the link-local address every cloud instance uses to serve its own metadata and credentials) without restriction is one HTTP request away from the cloud account underneath it: the finding stops being "a Kubernetes issue" and starts being "we own your AWS account."

What to do with this list

None of the six checks above need special access, and none of them take longer than a minute to run. If more than one comes back positive, the fixes are ordinary.

If you'd rather have us run the full pass (including the parts that don't reduce to a one-liner), request an engagement.