Minimize Access To The Proxy Sub-Resource Of Nodes
More Info:
Access to the node proxy sub-resource allows direct interaction with the kubelet API, bypassing normal controls. Restrict it.
Risk Level
Medium
Address
Security
Compliance Standards
- CIS Kubernetes
Triage and Remediation
- Remediation
Remediation
Manual Steps
-
Identify ClusterRoles granting node/proxy access
- Run on: any machine with
kubectlaccess - Command:
kubectl get clusterroles -o json | \jq -r '.items[]| select(.rules[]?| select((.resources[]? == "nodes/proxy")and (.verbs[]? == "get" or .verbs[]? == "create" or .verbs[]? == "update" or .verbs[]? == "delete" or .verbs[]? == "*")))| .metadata.name' | sort -u
- Save the resulting ClusterRole names for review.
- Run on: any machine with
-
Review detailed rules on those ClusterRoles
- For each ClusterRole name found in step 1, inspect its full spec:
kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
- Confirm which
rulesentries referenceresources: ["nodes/proxy"](or include it among multiple resources) and whatverbsare allowed. Determine whether each use is strictly required (for example, by an infrastructure component that legitimately needsnodes/proxy).
- For each ClusterRole name found in step 1, inspect its full spec:
-
Map ClusterRoles to subjects using them
- Run:
kubectl get clusterrolebindings -o yaml > /tmp/clusterrolebindings.yaml
- For each ClusterRole from step 1, list the bindings and subjects (users, groups, service accounts) that receive it:
yq '.items[]| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<CLUSTERROLE_NAME>")| {name: .metadata.name, subjects: .subjects}' /tmp/clusterrolebindings.yaml
- Decide, per subject, whether they truly require node proxy access.
- Run:
-
Remove unnecessary nodes/proxy permissions from ClusterRoles
- For any ClusterRole where
nodes/proxyaccess is not justified, edit it to drop that resource from the rules (or restrict verbs to only what is essential; ideally remove it entirely):kubectl edit clusterrole <CLUSTERROLE_NAME> - In your editor, remove
nodes/proxyfromresources:lists, or delete the entirerulesentry if that is its only resource. Save and exit to apply. - If some subjects only need a subset of permissions, consider:
- Creating a new, more restrictive ClusterRole without
nodes/proxy. - Rebinding those subjects to the new role with
kubectl edit clusterrolebinding <BINDING_NAME>.
- Creating a new, more restrictive ClusterRole without
- For any ClusterRole where
-
Verify no unintended nodes/proxy access remains
- Re-run the discovery query:
kubectl get clusterroles -o json | \jq -r '.items[]| select(.rules[]?| select((.resources[]? == "nodes/proxy")and (.verbs[]? == "get" or .verbs[]? == "create" or .verbs[]? == "update" or .verbs[]? == "delete" or .verbs[]? == "*")))| .metadata.name' | sort -u
- Confirm that only explicitly justified ClusterRoles (if any) appear, and that you have a documented rationale for each.
- Re-run the discovery query:
-
Optionally test from a non-privileged subject
- Using a user or service account that should not have node proxy access, attempt a proxy call and confirm it is forbidden:
kubectl --as=<USER_OR_SA_SUBJECT> get --raw /api/v1/nodes/<NODE_NAME>/proxy/ 2>&1 | sed -n '1,10p'
- Ensure the response indicates lack of authorization (e.g., “forbidden”) for all subjects that are not explicitly approved to use the node proxy sub-resource.
- Using a user or service account that should not have node proxy access, attempt a proxy call and confirm it is forbidden:
Using kubectl
# 1) List all ClusterRoles that grant any access to nodes/proxy
# Run on: any machine with kubectl access
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.resources // []) | index("nodes/proxy")
)
)
| .metadata.name
'
Problem indication: Any ClusterRole name listed here is potentially problematic and must be manually reviewed.
# 2) Show full definitions of those ClusterRoles for detailed review
# Replace <clusterrole-name> with names from the previous command
kubectl get clusterrole <clusterrole-name> -o yaml
What to look for as a problem:
In the rules section, find entries where resources includes nodes/proxy. For example:
rules:
- apiGroups: [""]
resources:
- nodes/proxy
verbs:
- get
- list
- watch
- create
- update
- delete
- proxy
- * # especially high risk
Risk indicators (subject to human judgment):
resourcesincludesnodes/proxywith broadverbs(e.g.,*,proxy,create,update,delete).- ClusterRole is bound to many subjects or to wide scopes (e.g., groups like
system:authenticated).
# 3) Find which subjects are actually using these ClusterRoles
# Run for each ClusterRole identified
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "<clusterrole-name>")
| {
binding: .metadata.name,
subjects: (.subjects // [])
}
'
Problem indication:
- Bindings where
subjectsinclude:- Broad groups (e.g.,
system:authenticated,system:unauthenticated). - ServiceAccounts in many namespaces.
- Human users who do not need direct kubelet access.
- Broad groups (e.g.,
These combinations suggest access to nodes/proxy is too wide and should be reconsidered.
# 4) (Optional) Check namespaced Roles as well, in case your cluster uses them for node access
# Run on: any machine with kubectl access
kubectl get roles -A -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.resources // []) | index("nodes/proxy")
)
)
| "\(.metadata.namespace)/\(.metadata.name)"
'
Again, inspect each with:
kubectl get role <role-name> -n <namespace> -o yaml
kubectl get rolebinding -n <namespace> -o yaml \
| jq -r '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == "<role-name>")
| {
binding: .metadata.name,
subjects: (.subjects // [])
}
'
Problem indication: Same as for ClusterRoles—nodes/proxy plus broad subjects or unnecessary use cases.
After you complete your review and any manual adjustments, re-run the initial listing:
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
(.resources // []) | index("nodes/proxy")
)
)
| .metadata.name
'
If no ClusterRoles remain (or only tightly scoped, justified ones), the risk from nodes/proxy access has been minimized per your policy.
Automation
#!/usr/bin/env bash
# Report ClusterRoles and Roles that can access the node/proxy sub-resource
# Run on any machine with kubectl context to the target cluster
set -euo pipefail
echo "=== Searching ClusterRoles with access to nodes/proxy ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
((.resources // []) | index("nodes/proxy")) != null
or (
((.resources // []) | index("nodes")) != null
and ((.verbs // []) | index("proxy")) != null
)
)
)
| .metadata.name
' | sort -u | awk '{print "ClusterRole:", $0}'
echo
echo "=== Detailed rules for matching ClusterRoles ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| . as $cr
| .rules[]
| select(
((.resources // []) | index("nodes/proxy")) != null
or (
((.resources // []) | index("nodes")) != null
and ((.verbs // []) | index("proxy")) != null
)
)
| "ClusterRole: \($cr.metadata.name)\n apiGroups: \(.apiGroups // [])\n resources: \(.resources // [])\n verbs: \(.verbs // [])\n"
'
echo
echo "=== Searching namespace-scoped Roles with access to nodes/proxy ==="
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| . as $role
| .rules[]
| select(
((.resources // []) | index("nodes/proxy")) != null
or (
((.resources // []) | index("nodes")) != null
and ((.verbs // []) | index("proxy")) != null
)
)
| "Role: \($role.metadata.name) Namespace: \($role.metadata.namespace)\n apiGroups: \(.apiGroups // [])\n resources: \(.resources // [])\n verbs: \(.verbs // [])\n"
'
echo
echo "=== Showing Subjects bound to suspect ClusterRoles (ClusterRoleBindings) ==="
suspect_crs=$(kubectl get clusterroles -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
((.resources // []) | index("nodes/proxy")) != null
or (
((.resources // []) | index("nodes")) != null
and ((.verbs // []) | index("proxy")) != null
)
)
)
| .metadata.name
' | sort -u)
if [ -n "${suspect_crs}" ]; then
kubectl get clusterrolebindings -o json \
| jq -r --argjson names "$(printf '%s\n' $suspect_crs | jq -R . | jq -s .)" '
.items[]
| select( (.roleRef.kind == "ClusterRole") and ( ($names | index(.roleRef.name)) != null ) )
| "ClusterRoleBinding: \(.metadata.name)\n RoleRef: \(.roleRef.kind)/\(.roleRef.name)\n Subjects: \(.subjects // [])\n"
'
else
echo "No ClusterRoles with nodes/proxy or nodes+proxy detected."
fi
echo
echo "=== Showing Subjects bound to suspect Roles (RoleBindings) ==="
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| select(
.rules[]
| select(
((.resources // []) | index("nodes/proxy")) != null
or (
((.resources // []) | index("nodes")) != null
and ((.verbs // []) | index("proxy")) != null
)
)
)
| {name: .metadata.name, namespace: .metadata.namespace}
' | while read -r line; do
name=$(printf '%s\n' "$line" | jq -r '.name')
ns=$(printf '%s\n' "$line" | jq -r '.namespace')
echo "Namespace: ${ns} Role: ${name}"
kubectl get rolebindings -n "${ns}" -o json \
| jq -r --arg role "${name}" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $role)
| " RoleBinding: \(.metadata.name)\n Subjects: \(.subjects // [])\n"
'
echo
done
How to interpret the output
- Any
ClusterRoleorRolewhose rules include:resources: ["nodes/proxy"]with any verbs, orresources: ["nodes"](or includingnodes) withverbscontaining"proxy", is a potential problem and should be reviewed.
- For each such role:
- Check the bindings sections to see which users, groups, or service accounts (Subjects) are granted this access.
- Access is overly broad if:
- The role is bound to generic groups (e.g.
system:authenticated,system:masters, large user groups), or - The role’s permissions are not strictly required for a narrowly scoped, documented use case (such as specific node debugging workflows).
- The role is bound to generic groups (e.g.