Cluster Hardening
Kubelet
Locking down the node agent so a compromised node cannot be used to pivot.
The kubelet runs on every node and does the actual work: starting containers, streaming logs, and running exec sessions. It exposes an HTTP API on port 10250 to do this. That API is a second entrance to the cluster, less watched than the API server, and just as powerful on the node it controls.
If the kubelet API accepts unauthenticated requests, an attacker who reaches port 10250 can list pods, read their logs, and exec into them (reading Secrets from mounted volumes and environment variables) without ever authenticating to the API server or touching RBAC.
Read The Kubelet Config
On kubeadm and kind, the kubelet is configured by a file on the node:
docker exec k8s-security-lab-control-plane \
cat /var/lib/kubelet/config.yaml
Three sections decide the kubelet’s exposure.
Anonymous Access
docker exec k8s-security-lab-control-plane \
grep -A3 'authentication:' /var/lib/kubelet/config.yaml
Expected output:
authentication:
anonymous:
enabled: false
webhook:
enabled: true
anonymous.enabled: falserejects unauthenticated requests. This must be false. The historically dangerous setting istrue, which let anyone reaching the port run kubelet API calls.webhook.enabled: truelets the kubelet verify bearer tokens against the API server, so clients must present a real identity.
Authorization Mode
docker exec k8s-security-lab-control-plane \
grep -A2 'authorization:' /var/lib/kubelet/config.yaml
Expected output:
authorization:
mode: Webhook
Webhook means the kubelet asks the API server “is this caller allowed to do this?” for every request, using the same RBAC rules as everything else. The unsafe alternative is AlwaysAllow, which authenticates the caller and then permits any action, precisely what you do not want on a node agent.
The Read-Only Port
Older kubelets served an unauthenticated read-only API on port 10255. It required no auth and happily listed pods and their specs, including mounted Secret names and environment variables. It must be disabled:
docker exec k8s-security-lab-control-plane \
grep -i readOnlyPort /var/lib/kubelet/config.yaml
You want readOnlyPort: 0 (the modern default). If the field is absent on an older cluster, set --read-only-port=0 explicitly. There is no legitimate use for an unauthenticated cluster API in production.
Serving Certificates
The kubelet serves TLS on 10250. By default it uses a self-signed certificate, which means the API server cannot verify it is talking to the real kubelet (relevant for kubectl logs and exec, which the API server proxies to the kubelet). Enabling serverTLSBootstrap: true makes the kubelet request a proper serving cert from the cluster CA, which an approver (or the built-in signer) then issues. This closes a man-in-the-middle gap on the control-plane-to-kubelet path.
NodeRestriction Ties It Together
Kubelet hardening pairs with two API server settings from the API Server page:
- The
Nodeauthorizer limits each kubelet to reading and modifying only the objects tied to its own node and its own pods. - The
NodeRestrictionadmission plugin stops a kubelet from editing other nodes or labeling itself to attract sensitive workloads.
Together they mean a single compromised node cannot be used to pivot into controlling the whole cluster. Confirm both are on:
docker exec k8s-security-lab-control-plane sh -c '
grep -E "authorization-mode|enable-admission-plugins" \
/etc/kubernetes/manifests/kube-apiserver.yaml'
You should see Node in the authorization mode and NodeRestriction in the admission plugins.
Clean Up
Nothing to clean up. This lab only read configuration.
Practical Guidance
- Confirm
authentication.anonymous.enabled: falseon every node; anonymous kubelet access is a direct path to pod Secrets. - Confirm
authorization.mode: Webhook, neverAlwaysAllow. - Set
readOnlyPort: 0and verify nothing depends on 10255. - Enable
serverTLSBootstrapso kubelet serving certs are signed by the cluster CA rather than self-signed. - Keep the
Nodeauthorizer andNodeRestrictionadmission plugin enabled so one bad node cannot escalate to the cluster. - Firewall ports 10250 and 10255 so only the control plane can reach the kubelet API; on managed clusters, keep node pools off public subnets.