Protect Kubernetes Node Metadata and Kubelet Endpoints

Audit kubelet authentication and authorization, test nodes/proxy and direct kubelet access, restrict cloud metadata exposure from Pods, and validate monitoring paths on a kubeadm cluster.

Published

Updated

Read time 9 min read

Reviewed byDeepak Prasad

Kubernetes kubelet HTTPS API and cloud instance metadata access paths with RBAC and network controls
Tested on Rocky Linux 10.2 (Red Quartz) — kubeadm control plane and worker nodes
Package kubectl 1.36.3
kubelet 1.36.3
kubeadm 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.

IMPORTANT
This article hardens kubelet network interfaces and cloud metadata exposure. It does not replace general node OS hardening or NetworkPolicy design for application traffic — those are separate CKS domains.

Map the node access paths

Attackers can reach node-level interfaces through the API or through the network:

Kubernetes node access paths through API nodes proxy to kubelet and from Pods to cloud metadata

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:

bash
grep -E '^(authentication|authorization|kind):|anonymous|webhook|mode|clientCA|readOnly|port|address' /var/lib/kubelet/config.yaml
output
authentication:
  anonymous:
  webhook:
authorization:
  mode: Webhook
  webhook:
kind: KubeletConfiguration

Confirm which port kubelet listens on:

bash
ss -tlnp | grep kubelet
output
LISTEN 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 certificates
  • containerRuntimeEndpoint — 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:

yaml
authentication:
  anonymous:
    enabled: false

Test an unauthenticated request to the secure port on the node:

bash
curl -sk https://127.0.0.1:10250/pods -o /dev/null -w "HTTP %{http_code}\n"
output
HTTP 401

The 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):

bash
curl -sk https://192.168.56.109:10250/healthz
output
Unauthorized

Even 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:

yaml
authentication:
  webhook:
    enabled: true

Test a token that authenticates but should not be authorized. On the control plane:

bash
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"
output
HTTP 403

401 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:

yaml
authorization:
  mode: Webhook

The 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:

bash
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/healthz
output
Forbidden (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:

bash
curl -sk http://127.0.0.1:10255/pods -o /dev/null -w "readOnly HTTP %{http_code}\n"
output
readOnly HTTP 000

HTTP 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:

bash
firewall-cmd --list-ports
output
179/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:

bash
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl describe clusterrole system:kubelet-api-admin
output
PolicyRule:
  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:

text
nodes/proxy
nodes/log
nodes/metrics
nodes/stats

Check whether low-privilege identities can use them:

bash
kubectl auth can-i get nodes/proxy --as=system:serviceaccount:default:default
output
no

The default ServiceAccount in default cannot proxy to nodes in this cluster — a good sign. Cluster-admin can:

bash
kubectl auth can-i '*' nodes/proxy
output
yes

Review 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:

bash
kubectl get --raw "/api/v1/nodes/worker01/proxy/healthz"
output
ok

The 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:

bash
curl -sk --max-time 3 http://169.254.169.254/latest/meta-data/ -o /dev/null -w "IMDS HTTP %{http_code}\n"
output
IMDS HTTP 000

On 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:

  1. Ordinary Pod on the overlay network
  2. hostNetwork Pod (metadata path matches the node)
  3. 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/healthz as the monitoring ServiceAccount you actually use
  • Kubelet client certificate rotation (rotateCertificates: true in config)
  • Node metrics scrapers bound to system:monitoring or 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


References


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.


Frequently Asked Questions

1. Does Kubernetes RBAC protect the kubelet HTTPS API?

Not by itself. RBAC governs who may call the Kubernetes API, including the nodes/proxy subresource. A kubelet port that is reachable on the node network without authentication still exposes an independent attack path. Harden kubelet authentication and authorization, restrict network reachability to port 10250, and audit Roles that grant nodes/proxy, nodes/log, or nodes/stats.

2. What is the difference between kubelet webhook authentication and authorization?

Webhook authentication sends bearer tokens to the API server TokenReview endpoint so the kubelet learns who is calling. Webhook authorization mode sends SubjectAccessReview requests so the API server decides whether that identity may perform the verb on the requested kubelet subresource. Authentication answers who you are; authorization answers whether you may do this operation. Neither is an admission webhook.

3. Why is nodes/proxy dangerous in RBAC?

The nodes/proxy subresource lets an API client tunnel HTTP requests to the kubelet API on a node. Combined with get, list, watch, or proxy verbs on nodes, it can expose pod listing, logs, exec, and other kubelet operations depending on kubelet authorization. Treat bindings to system:kubelet-api-admin or custom Roles with nodes/proxy like cluster-admin-adjacent access.

4. How do Pods reach cloud instance metadata?

Many clouds expose a link-local metadata service, commonly 169.254.169.254, from the node network namespace. An ordinary Pod on the overlay network can often route to that address unless the platform restricts hops, blocks the path with host firewall rules, or replaces node credentials with workload identity. hostNetwork Pods share the node network namespace and inherit the same metadata reachability as the host unless you add separate controls.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)