| Tested on | Rocky Linux 10.2 (Red Quartz) — kubeadm control plane and worker nodes |
|---|---|
| Package | kubectl 1.36.3kubelet 1.36.3kubeadm 1.36.3 |
| Applies to | Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora Kubernetes cluster |
| Cert prep | CKS |
| Lab environment | Multi-node kubeadm cluster with containerd and SSH node access — install Kubernetes with kubeadm |
| Privilege | root on cluster nodes for kubelet configuration and firewall checks; cluster-admin kubectl for nodes/proxy audit paths |
| Scope | Map kubelet and metadata access paths, inspect kubelet authentication and authorization, verify read-only port status, audit nodes/proxy RBAC, test direct and proxied kubelet access, and document cloud metadata risks with validation steps. Does not cover general Linux hardening, full cloud IAM design, kubelet installation, Node NotReady troubleshooting, Pod SecurityContext, or CNI installation. |
A compromised Pod that can reach the kubelet API or a cloud metadata service may steal credentials without ever passing your API RBAC checks. This walkthrough maps those two paths on a kubeadm lab, inspects kubelet settings in /var/lib/kubelet/config.yaml, tests authentication and nodes/proxy access, and records what ordinary identities can reach.
Map the node access paths
Attackers can reach node-level interfaces through the API or through the network:
Two paths matter in almost every review:
| Path | Entry | What it reaches |
|---|---|---|
API nodes/proxy |
kubectl get --raw /api/v1/nodes/<name>/proxy/... with RBAC permission |
Kubelet HTTPS API on the target node |
| Direct network | https://<nodeIP>:10250/... from any routable client |
Kubelet HTTPS API without going through API RBAC |
| Pod to metadata | http://169.254.169.254/... on clouds that expose IMDS |
Instance identity and short-lived cloud credentials |
RBAC on the Kubernetes API does not automatically protect a kubelet port that is open on the node network. Treat authentication, authorization, firewall rules, and nodes/proxy bindings as one review.
Discover the kubelet configuration
kubeadm stores kubelet settings in /var/lib/kubelet/config.yaml. Systemd passes --config=/var/lib/kubelet/config.yaml through /etc/kubernetes/kubelet.conf bootstrap wiring — read the file instead of guessing from process arguments alone.
On a worker node, print the authentication and authorization block:
grep -E '^(authentication|authorization|kind):|anonymous|webhook|mode|clientCA|readOnly|port|address' /var/lib/kubelet/config.yamlauthentication:
anonymous:
webhook:
authorization:
mode: Webhook
webhook:
kind: KubeletConfigurationConfirm which port kubelet listens on:
ss -tlnp | grep kubeletLISTEN 0 4096 127.0.0.1:10248 0.0.0.0:* users:(("kubelet",pid=781683,fd=25))
LISTEN 0 4096 *:10250 0.0.0.0:* users:(("kubelet",pid=781683,fd=17))This lab kubelet binds the secure API on 10250 on all interfaces (*:10250). The healthz endpoint for monitoring sits on 127.0.0.1:10248. Serving certificates are issued to the node name — here subject=CN=worker01@... from the kubelet server cert.
Other fields to note in the same file:
authentication.x509.clientCAFile— which CA validates client certificatescontainerRuntimeEndpoint— CRI socket path (separate hardening topic)- Legacy
readOnlyPort— when absent or zero, the unauthenticated read-only port should be off (verify with a connection test below)
Disable anonymous authentication
kubeadm defaults should leave anonymous kubelet access disabled. Confirm in config:
authentication:
anonymous:
enabled: falseTest an unauthenticated request to the secure port on the node:
curl -sk https://127.0.0.1:10250/pods -o /dev/null -w "HTTP %{http_code}\n"HTTP 401The body prints Unauthorized. That is the expected baseline — the kubelet answered but rejected the caller.
Repeat from another host on the node network (control plane to worker in this lab):
curl -sk https://192.168.56.109:10250/healthzUnauthorizedEven when anonymous auth is disabled, a reachable port still leaks existence of the API. Combine authentication settings with network restrictions in the next sections.
Enable webhook token authentication
Bearer tokens from ServiceAccounts or kubectl create token authenticate through the API server TokenReview API when webhook authentication is enabled:
authentication:
webhook:
enabled: trueTest a token that authenticates but should not be authorized. On the control plane:
export KUBECONFIG=/etc/kubernetes/admin.conf
TOKEN=$(kubectl create token default -n default --duration=1h)
curl -sk -H "Authorization: Bearer ${TOKEN}" https://192.168.56.109:10250/pods -o /dev/null -w "HTTP %{http_code}\n"HTTP 403401 means authentication failed. 403 means the kubelet accepted the identity and webhook authorization denied the verb — the desired split for a low-privilege token.
The API server and monitoring agents that legitimately call the kubelet use client certificates or identities bound to system:kubelet-api-admin (see below). After you disable weaker methods, confirm those agents still pass health checks.
Use webhook authorization
With kubeadm, authorization mode is typically:
authorization:
mode: WebhookThe kubelet sends SubjectAccessReview requests to the API server. This is not an admission webhook — it runs on the node for each kubelet HTTP request after authentication succeeds.
A kubelet client certificate alone does not bypass this layer. Testing with the node's own client cert from localhost:
curl -sk --cert /var/lib/kubelet/pki/kubelet-client-current.pem --key /var/lib/kubelet/pki/kubelet-client-current.pem --cacert /etc/kubernetes/pki/ca.crt https://127.0.0.1:10250/healthzForbidden (user=system:node:worker01, verb=get, resource=nodes, subresource(s)=[healthz proxy])The node identity is recognized, but the kubelet still enforces API authorization for the subresource.
Verify the read-only port status
Older clusters exposed an unauthenticated read-only kubelet on port 10255. Modern kubeadm builds omit it — verify with a connection attempt rather than assuming:
curl -sk http://127.0.0.1:10255/pods -o /dev/null -w "readOnly HTTP %{http_code}\n"readOnly HTTP 000HTTP 000 means nothing is listening. If you see 200 on 10255, treat it as a critical finding and disable the read-only port in kubelet configuration.
Restrict direct network reachability
Even with authentication enabled, limit who can open TCP connections to port 10250.
On this lab, firewalld on the worker allows 10250/tcp from broad sources. Production baselines usually narrow sources to:
- Control-plane nodes that call the kubelet through the API
- Approved monitoring collectors
- Administrative jump networks
List open ports:
firewall-cmd --list-ports179/tcp 1636/tcp 2379-2380/tcp ... 10250/tcp 10256/tcp ...Pair host firewall rules with cloud security groups or network ACLs when nodes run in a VPC. NetworkPolicy controls Pod-to-Pod traffic; it does not replace blocking Pod CIDRs from reaching nodeIP:10250 when the CNI allows node routing.
Audit nodes/proxy permissions
API clients with nodes/proxy can tunnel to the kubelet. Inspect the built-in system:kubelet-api-admin ClusterRole:
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl describe clusterrole system:kubelet-api-adminPolicyRule:
Resources Non-Resource URLs Resource Names Verbs
nodes/proxy [] [] [*]
nodes/log [] [] [*]
nodes/metrics [] [] [*]
nodes/pods [] [] [*]
nodes/stats [] [] [*]
nodes [] [] [get list watch proxy]Subresources to hunt in custom Roles and ClusterRoleBindings:
nodes/proxy
nodes/log
nodes/metrics
nodes/statsCheck whether low-privilege identities can use them:
kubectl auth can-i get nodes/proxy --as=system:serviceaccount:default:defaultnoThe default ServiceAccount in default cannot proxy to nodes in this cluster — a good sign. Cluster-admin can:
kubectl auth can-i '*' nodes/proxyyesReview bindings to system:kubelet-api-admin and any custom monitoring Roles during access reviews — see Kubernetes RBAC for least-privilege patterns.
Test kubelet access paths
Record HTTP status and which gate applied. Summary from this lab:
| Test | Command / path | Result | Meaning |
|---|---|---|---|
| Unauthenticated local | curl https://127.0.0.1:10250/pods |
401 |
Anonymous auth disabled |
| Unauthenticated remote | curl https://<workerIP>:10250/healthz |
401 |
Port reachable; auth required |
| Bearer token (default SA) | Authorization: Bearer from kubectl create token |
403 |
Authenticated; authorization denied |
API nodes/proxy |
kubectl get --raw /api/v1/nodes/worker01/proxy/healthz |
ok |
Cluster-admin may tunnel to kubelet |
| Read-only port | curl http://127.0.0.1:10255/pods |
not listening | Legacy port disabled |
Exercise the API proxy path as cluster-admin:
kubectl get --raw "/api/v1/nodes/worker01/proxy/healthz"okThe same identity can reach kubelet paths such as /proxy/pods — powerful during incident response, dangerous when granted to application ServiceAccounts.
From an ordinary Pod on the overlay network, expect either refusal (401/403) or a timeout if routing or firewall blocks Pod CIDR to the node port. Validate on your CNI — a timeout still means the port should not be reachable from workloads.
Explain cloud metadata risk
Cloud and some on-prem platforms expose an instance metadata service (IMDS) at a link-local address. Responses may include:
- Instance identity and region
- Short-lived cloud API credentials
- User-data bootstrap scripts
- Network configuration
A compromised application Pod can query metadata even when Kubernetes RBAC is tight. hostNetwork: true Pods use the node network namespace and inherit the same metadata path as the host unless you add restrictions.
This bare-metal lab has no cloud metadata endpoint. From the worker node:
curl -sk --max-time 3 http://169.254.169.254/latest/meta-data/ -o /dev/null -w "IMDS HTTP %{http_code}\n"IMDS HTTP 000On AWS, GCP, or Azure you would see 200 responses with instance metadata unless hop limits or firewall rules block access.
Restrict metadata access
Provider controls differ; combine platform settings with node-level blocks:
| Layer | Examples |
|---|---|
| Cloud platform | Workload identity instead of node IAM roles; IMDSv2 required on AWS; metadata hop limit; disable metadata where unused |
| Host firewall | Drop 169.254.169.254 from Pod-facing interfaces or forward chain |
| Kubernetes | Dedicated node pools for sensitive tenants; avoid unnecessary hostNetwork |
| CNI / policy | Egress policy to metadata CIDR where your CNI enforces it |
Test three contexts after each change:
- Ordinary Pod on the overlay network
hostNetworkPod (metadata path matches the node)- Required system agents and monitoring that legitimately need metadata or kubelet access
Document exceptions with owner and review date like any kube-bench deviation.
Validate required access
After tightening, confirm legitimate automation still works:
kubectl get --raw nodes/<name>/proxy/healthzas the monitoring ServiceAccount you actually use- Kubelet client certificate rotation (
rotateCertificates: truein config) - Node metrics scrapers bound to
system:monitoringor a custom least-privilege Role - Cloud agents that need metadata — only on nodes where that remains policy
Re-run targeted checks from kube-bench section 4 (worker node) after kubelet or firewall edits.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Monitoring fails after disabling anonymous auth | Scrapers still use unauthenticated kubelet URL | Switch to authenticated scrape config or API metrics path; grant least-privilege nodes/stats |
403 for all kubelet token calls |
Webhook authorization denies the identity | Create RoleBinding for the monitoring SA or use cert-based kubelet client |
| API server cannot reach kubelet | Firewall blocks control plane to 10250 |
Allow node IPs of control-plane components; verify system:kubelet-api-admin binding for apiserver client |
Broad nodes/proxy remains on app SA |
Copy-paste ClusterRoleBinding | Remove binding; use dedicated monitoring identity |
| Pods still reach IMDS | No hop limit or host firewall | Enable IMDSv2 / workload identity; block link-local egress from Pod CIDR |
hostNetwork Pod reaches metadata |
Shares node net namespace | Treat hostNetwork as privileged; restrict with admission policy |
What's Next
- Verify Kubernetes Release Binaries and Container Images
- Kubernetes RBAC with Examples
- Kubernetes ServiceAccounts with Examples
References
- kubelet authentication and authorization — upstream kubelet access model
- Node authorization mode — API authorization for kubelet-submitted requests
- Protecting kubelet with NodeRestriction — admission control for node objects
- Kubernetes security overview — platform security context
Summary
Kubelet security is a separate boundary from Kubernetes API RBAC. Harden /var/lib/kubelet/config.yaml so anonymous authentication stays off, webhook authentication and webhook authorization are enabled, and the read-only port is not listening. Then test what you actually get: 401 without credentials, 403 with a low-privilege token, and controlled access through nodes/proxy only for identities that truly need it.
Cloud metadata is the second path. Link-local metadata can hand cloud credentials to any workload that can route to it — especially hostNetwork Pods. Platform hop limits, workload identity, and host firewall rules belong in the same review as kubelet port 10250.
For CKS study, pair this lesson with CIS benchmark scanning on worker nodes and the access-path overview in Kubernetes security architecture. Re-test after every kubelet upgrade or firewall change — defaults drift, and a reachable kubelet port with weak RBAC on nodes/proxy is a common lateral-movement pivot.

