
Here's a fact that made me sit up when I first met it: on a lot of clusters, you can point curl at a worker node, ask for a list of running pods, and get an answer. No login. No token. Nothing. And from that same door, if it's left open, you can run commands inside those pods. That door is the kubelet, and this post is about shutting it properly.
The kubelet is the little agent that runs on every worker node. It's the thing that actually starts your containers when the scheduler says so, and reports back whether they're alive. It talks to the API server constantly. And because it has that much power over a node, an exposed kubelet is one of the cleaner ways to go from "I can reach the network" to "I own this node". Let's fix that.
What the kubelet is, in one minute
The kubelet is the on-node worker. When you schedule a pod, the API server doesn't run it. It just records the intent, and the kubelet on the chosen node notices, pulls the images and tells the container runtime (Docker, containerd, whatever) to start them. Then it watches those containers and streams their health back up.
To do that job it exposes its own small HTTP API, and that API is the security story here. It's not just status either. The kubelet can hand you container logs, forward ports, and run commands inside a running container. Handy for the cluster. Very handy for an attacker.
Which is the whole point: the kubelet controls the workloads on a node, so whoever controls the kubelet controls the node.
Where the kubelet keeps its settings
Before hardening it, you need to find how it's configured. Older setups passed everything as long command-line flags in the systemd service file. Since around v1.10, most of that moved into a config file, usually /var/lib/kubelet/config.yaml, written in KubeletConfiguration YAML. One gotcha worth remembering: flag names use dashes (--read-only-port) while the YAML uses camelCase (readOnlyPort), and if a setting appears in both places, the command-line flag wins.
To see what's actually running, look at the live process and read its config:
# what flags is the kubelet running with?
ps -aux | grep kubelet
# and the config file it points at
cat /var/lib/kubelet/config.yaml
That config dump tells you everything you're about to check: whether anonymous access is on, which CA it trusts, the authorization mode, and whether the read-only port is open. This is also the first thing I read on a node during an assessment.
The two ports: 10250 and 10255
The kubelet listens on two ports by default, and they are not equal. One is the real API. The other is a quiet little information leak that a lot of people forget exists.
| Port | What it serves | Risk out of the box |
|---|---|---|
| 10250 | full read-write API: pods, logs, exec, port-forward | code execution |
| 10255 | read-only: metrics and pod data, no auth ever | info leak |
Out of the box the kubelet treats an unauthenticated caller as an anonymous user (username system:anonymous, group system:unauthenticated) and, worse, often lets that anonymous user through. So a plain request works:
Four changes that shut the door
The good news: hardening the kubelet is four settings, and none of them are hard. Here's the whole checklist, then each one in turn.
anonymous-auth = false
trust a CA with clientCAFile
AlwaysAllow → Webhook
readOnlyPort = 0
1. Turn off anonymous access
This is the big one. Stop the kubelet accepting requests with no identity. You can set it as a flag or in the config file, and you'll see both styles in the wild:
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false# kubelet.service
ExecStart=/usr/local/bin/kubelet \
--anonymous-auth=false \
...2. Require a client certificate
Turning off anonymous access is only half the job. Now you have to give legitimate callers a way to prove who they are. The clean way is certificates, the same TLS and PKI ideas from Topic 7. You tell the kubelet which certificate authority (CA) to trust, and only clients holding a certificate signed by that CA get in:
# /var/lib/kubelet/config.yaml
authentication:
x509:
clientCAFile: /etc/kubernetes/pki/ca.crt
Remember the direction here: from the kubelet's point of view, the API server is the client. So the API server has to present a certificate the kubelet trusts, which is why it's configured with its own kubelet client cert and key:
# kube-apiserver
--kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt
--kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key
3. Swap AlwaysAllow for Webhook
Authentication proves who you are. Authorization decides what you're allowed to do, and this is where the kubelet has an ugly default: AlwaysAllow, which means every authenticated request is permitted. Switch it to Webhook, and the kubelet stops deciding for itself. Instead it asks the API server "is this caller allowed to do this?" and honours the answer. It's the same Webhook authorization idea from the RBAC post, pointed at the kubelet:
# /var/lib/kubelet/config.yaml
authorization:
mode: Webhook
Now a random authenticated identity can't just call exec on any pod. The API server's RBAC rules get the final say.
4. Close the read-only port
Last one, and it's a one-liner. Port 10255 serves metrics and pod info with no authentication, ever. There's rarely a good reason to leave it open. Set it to zero and it's gone:
# /var/lib/kubelet/config.yaml
readOnlyPort: 0
Anonymous off, certs on, Webhook authorization, read-only port closed. Four lines, and probably the easiest node-level win in Kubernetes.
The attacker's checklist (and so, yours)
When I look at a cluster, the kubelet is an early target because the payoff is so high. The recon is quick, and flipping it round gives you your own audit list:
- Can I reach
10250or10255on any node from where I'm standing? Network reachability is half the battle, so segment these ports off. - Does
curl -sk https://node:10250/podsanswer without credentials? If yes, anonymous auth is on. - Is
10255responding at all? If so, that's free reconnaissance for anyone on the network. - On the node itself, does
config.yamlshowauthorization.mode: AlwaysAllow? That's the jackpot for an attacker and a finding for you.
anonymous.enabled: false, authorization.mode: Webhook, a real clientCAFile, and readOnlyPort: 0 on every node. Then firewall 10250 so only the control plane can reach it. The CIS Benchmark checks all of this, so kube-bench will flag it for you.Why this one is blunter than most
What stuck with me is how blunt this one is. Most Kubernetes security is layered and subtle. The kubelet is not: it's a handful of settings that are either right or catastrophically wrong, and the default leans wrong. I keep coming back to that anonymous exec path, because it turns a network position straight into code execution with nothing clever required. I've not yet built a lab to fire the exec endpoint end to end myself (it's on my list), so if you've walked that exact path I'd be keen to compare notes. Next up I want to look at securing the kubelet's siblings on the control plane, and how certificate rotation keeps all of this honest over time.
If this was useful, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.
Further reading
- Kubernetes docs: kubelet authentication and authorization
- Kubernetes docs: KubeletConfiguration reference
- Kubernetes docs: securing a cluster
FAQ
Why is an exposed kubelet dangerous?
The kubelet's API on port 10250 can list pods, read logs and even run commands inside containers. If it accepts anonymous requests, anyone who can reach that port gets code execution on the node's workloads, no credentials needed.
What are kubelet ports 10250 and 10255?
Port 10250 is the kubelet's full read-write API. Port 10255 is a read-only port that serves metrics and pod data with no authentication at all. Best practice is to secure 10250 and disable 10255 by setting readOnlyPort to 0.
How do I secure the kubelet?
Turn off anonymous access with anonymous-auth=false, require client certificates via clientCAFile, switch authorization from AlwaysAllow to Webhook so the API server vets each request, and disable the read-only port by setting readOnlyPort to 0.
Related reading
- Topic 9: Authorization and RBAC (Webhook authorization, explained)
- Topic 7: TLS, certificates and PKI (the certs behind clientCAFile)
- Topic 5: CIS Benchmarks and kube-bench (auto-check these settings)
- Browse the whole Kubernetes Journey