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.