Remediation Guides

How to Disable the Kubelet Read-Only Port 10255 (kube-bench 4.2.4, Trivy KCV-0082)

26 September 2026 8 min read

The kubelet read-only port 10255 is a plain HTTP endpoint that serves pod specs, node stats and metrics without authentication or authorization. kube-bench check 4.2.4 and Trivy KCV-0082 flag any kubelet where it is enabled. Fix it by setting readOnlyPort: 0 (and dropping any –read-only-port flag), restarting the kubelet, and moving scrapers to authenticated port 10250.

What the scanner is actually detecting

Both checks read kubelet configuration on each node. Neither one probes port 10255 over the network.

Scanner ID and title What it checks
kube-bench (CIS Kubernetes Benchmark, cis-1.10) 4.2.4: Verify that the –read-only-port argument is set to 0 (Manual) Runs /bin/ps -fC kubelet and reads the kubelet config file. Passes if –read-only-port or readOnlyPort equals 0, or if neither is set.
Trivy (Kubernetes infra assessment) KCV-0082 (alias AVD-KCV-0082): Verify that the –read-only-port argument is set to 0, severity HIGH Evaluates node data gathered by Trivy’s node-collector job during trivy k8s: the –read-only-port value on the kubelet command line and the live readOnlyPort from the node’s configz endpoint. Any value other than 0 is a violation.

Two details matter when you read the report. Because 4.2.4 is an unscored (Manual) check, kube-bench shows a failure as WARN, not FAIL, so it is easy to overlook. And kube-bench’s GKE benchmark (gke-1.6.0) runs the same test as 3.2.4: Ensure that the –read-only-port argument is disabled (Automated).

Real-world risk

The read-only server accepts GET requests only. It serves /pods, /stats/, /healthz, /metrics, /metrics/cadvisor, /metrics/resource and /metrics/probes. It does not serve exec, attach, port-forward or container logs; those exist only on the authenticated HTTPS port 10250. This is an information disclosure issue, not remote code execution.

What leaks is still useful to an attacker. /pods returns the full spec of every pod on the node: namespaces, images and tags, commands, service account names, volume mounts, and any environment variable set as a literal value. Values pulled from a Secret with secretKeyRef appear only as a reference. The port binds to the kubelet’s address setting, which defaults to 0.0.0.0, so any pod that can reach node IPs can usually query it unless a firewall or network policy blocks it.

Honest priority: a solid hardening fix, most urgent where untrusted workloads share nodes or node IPs are reachable from outside the cluster.

How to confirm it on the host

Run these on a node. A readOnlyPort line that is missing does not prove the port is off: the command-line flag defaults to 10255, while the config file field defaults to 0. A missing line means disabled only if the kubelet runs with –config.

# Flags on the running kubelet (flags override the config file)
ps -o args= -C kubelet | tr ' ' 'n' | grep -E -- '^--(config|config-dir|read-only-port)'

# Where those flags come from (unit file and drop-ins)
systemctl cat kubelet

# Value in the kubeadm default config file
sudo grep -n readOnlyPort /var/lib/kubelet/config.yaml

# Is anything listening?
sudo ss -ltnp | grep ':10255'
curl -s http://127.0.0.1:10255/healthz

If curl prints ok, the port is open. To see the value every kubelet is actually running across the cluster (requires cluster-admin rights):

for n in $(kubectl get nodes -o name | cut -d/ -f2); do
  printf '%s ' "$n"
  kubectl get --raw "/api/v1/nodes/$n/proxy/configz" | jq '.kubeletconfig.readOnlyPort'
done

How to fix it

Step 1: find who still uses port 10255

Disabling the port breaks anything that scrapes it, so check first. The kubelet counts requests per server type:

kubectl get --raw /api/v1/nodes/NODE_NAME/proxy/metrics | grep http_requests_total | grep readonly
kubectl get pods --all-namespaces -o yaml | grep 10255
kubectl get configmaps --all-namespaces -o yaml | grep 10255

Lines with server_type=”readonly” mean something has called the port since the kubelet last started; the path label shows which endpoint. Typical consumers are older monitoring agents and custom metrics scripts.

Step 2: move consumers to port 10250

Point them at https://NODE_IP:10250 with a service account token, and grant only the subresources they need. The kubelet maps /metrics/* to nodes/metrics and /stats/* to nodes/stats:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kubelet-metrics-reader
rules:
- apiGroups: [""]
  resources: ["nodes/metrics", "nodes/stats"]
  verbs: ["get"]

Do not grant nodes/proxy to replace /pods. The Kubernetes documentation warns that even get on nodes/proxy allows executing commands in any container on the node. Query the API server instead: kubectl get pods -A –field-selector spec.nodeName=NODE_NAME. Token login and RBAC checks on port 10250 require webhook authentication and Webhook authorization on the kubelet; kubeadm enables both by default. kubeadm’s kubelet serving certificate is self-signed unless you set serverTLSBootstrap: true, so clients that verify TLS against the cluster CA may need that change.

Step 3a: kubeadm and self-managed nodes

  1. In /var/lib/kubelet/config.yaml (or the file named by –config), set the field explicitly:
    apiVersion: kubelet.config.k8s.io/v1beta1
    kind: KubeletConfiguration
    readOnlyPort: 0
  2. Remove any –read-only-port flag. Check KUBELET_EXTRA_ARGS in /etc/default/kubelet (DEB) or /etc/sysconfig/kubelet (RPM), /var/lib/kubelet/kubeadm-flags.env, the drop-ins shown by systemctl cat kubelet, and any .conf files in a –config-dir directory. Command-line flags win over the config file.
  3. Restart the kubelet: sudo systemctl daemon-reload, then sudo systemctl restart kubelet. Work one node at a time.

On kubeadm clusters, make the change stick. kubeadm upgrade overwrites /var/lib/kubelet/config.yaml from the kubelet-config ConfigMap, so edit it there as well:

kubectl edit cm -n kube-system kubelet-config   # set readOnlyPort: 0 under data.kubelet
# then on each node, one at a time:
sudo kubeadm upgrade node phase kubelet-config
sudo systemctl restart kubelet

kubeadm’s own default is readOnlyPort: 0, so on a kubeadm cluster this finding means someone set the port deliberately.

Step 3b: Google Kubernetes Engine

GKE supports disabling the port from version 1.26.4-gke.500, and new clusters on 1.32 or later have it disabled by default. For Standard clusters, set it at cluster level; existing node pools are not updated automatically, so update each one too:

gcloud container clusters update CLUSTER_NAME 
    --location=LOCATION 
    --no-enable-insecure-kubelet-readonly-port

gcloud container node-pools update NODE_POOL_NAME 
    --cluster=CLUSTER_NAME 
    --location=LOCATION 
    --no-enable-insecure-kubelet-readonly-port

For Autopilot, use gcloud container clusters update CLUSTER_NAME –location=LOCATION –no-autoprovisioning-enable-insecure-kubelet-readonly-port. Both the node pool and Autopilot commands start a rolling update of nodes, which can disrupt running workloads, so schedule them. If you manage kubelet settings with a node system configuration file, set it there instead; Google notes the gcloud flags cannot control the setting in that case. To stop the port from returning, Google documents an Organization Policy custom constraint that denies cluster and node pool changes with insecureKubeletReadonlyPortEnabled == true. Cluster settings like this are the kind of cloud configuration that posture management tools watch; kube-bench and Trivy check what each node’s kubelet is actually running.

How to verify the fix and rescan

  1. Rerun the configz loop above. Every node should print 0.
  2. On a node, ss -ltnp | grep ‘:10255’ prints nothing and curl http://127.0.0.1:10255/healthz fails with connection refused.
  3. On GKE Standard, gcloud container node-pools describe NODE_POOL_NAME –cluster=CLUSTER_NAME –location=LOCATION –flatten=config –format=”value(kubeletConfig)” should include insecureKubeletReadonlyPortEnabled: false. For the cluster default, describe the cluster with –flatten=nodePoolDefaults.nodeConfigDefaults –format=”value(nodeKubeletConfig)”; for Autopilot, use –flatten=nodePoolAutoConfig.
  4. Rescan: kube-bench run –targets node –benchmark cis-1.10 –check 4.2.4 should report PASS, and trivy k8s –report summary should no longer list KCV-0082.
  5. Confirm dashboards and alerts that used node metrics still receive data.

What can break and how to roll back

  • Metrics gaps. Scrape jobs still pointed at http://NODE:10255 start failing with connection refused.
  • Node inventory scripts that read /pods from the kubelet stop working until moved to the API server.
  • Health checks against :10255/healthz fail. On the node itself, the kubelet also serves /healthz on 127.0.0.1:10248 by default.
  • GKE node recreation during node pool updates can evict pods; PodDisruptionBudgets and spare capacity help.

To roll back on a self-managed node, restore the previous readOnlyPort value (and the ConfigMap on kubeadm) and restart the kubelet. On GKE, run the same update command with –enable-insecure-kubelet-readonly-port. Record the rollback as a temporary exception with a date to retry.

Common false positive reasons

  • kube-bench read the wrong file. It audits the first file that exists from a built-in list of candidate paths, and /etc/kubernetes/kubelet-config.yaml is checked before /var/lib/kubelet/config.yaml. A leftover file can produce a WARN the running kubelet does not match. Compare with the –config path from ps and with configz.
  • File edited, kubelet not restarted. Trivy reads the live value, so the finding stays until the restart. That is accurate, not a false positive.
  • GKE cluster default changed, node pools not. The cluster describe output looks fixed while existing pools still serve the port.
  • Stale results from a scan taken before the rollout finished.

The opposite also happens: kube-bench passes 4.2.4 when neither the flag nor the config key is set, even on a kubelet started without –config, where the flag default of 10255 applies. For the broader pattern, see these real causes of scanner false positives.

FAQ

Should I also close port 10250?

No. Port 10250 is the kubelet’s authenticated HTTPS API, which the API server uses for logs, exec and port-forward, and which metrics collection depends on. Keep it, with anonymous access disabled and Webhook authorization enabled.

Does kubeadm enable the read-only port by default?

No. kubeadm’s default KubeletConfiguration sets readOnlyPort to 0 and warns when you override it. If kubeadm nodes have it open, check your kubeadm config, the kubelet-config ConfigMap and extra kubelet flags.

Can I block 10255 with a firewall instead?

A firewall rule reduces exposure, but both scanners read kubelet configuration, so the finding stays open, and anything already on the node can still query the port. Use a firewall rule as a compensating control while you migrate consumers.

Is the –read-only-port flag deprecated?

Yes. The kubelet reference marks it deprecated in favor of the readOnlyPort field in the kubelet config file, so set the value there.

Tracking this finding across many hosts

The read-only port tends to come back when nodes are rebuilt from an older image, bootstrap script or node pool template. If you consolidate scanner output in one place, SITEY is one option: it imports findings from 16 scanners, merges duplicates per scanner (not across scanners), and uses AI triage to suggest false positives with evidence while a human decides. Confirm closure by rerunning kube-bench or Trivy against the affected nodes.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing