Skip to main content

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

Manual Steps
  1. Identify ClusterRoles granting node/proxy access

    • Run on: any machine with kubectl access
    • 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.
  2. 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 rules entries reference resources: ["nodes/proxy"] (or include it among multiple resources) and what verbs are allowed. Determine whether each use is strictly required (for example, by an infrastructure component that legitimately needs nodes/proxy).
  3. 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.
  4. Remove unnecessary nodes/proxy permissions from ClusterRoles

    • For any ClusterRole where nodes/proxy access 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/proxy from resources: lists, or delete the entire rules entry 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>.
  5. 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.
  6. 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 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):

  • resources includes nodes/proxy with broad verbs (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 subjects include:
    • Broad groups (e.g., system:authenticated, system:unauthenticated).
    • ServiceAccounts in many namespaces.
    • Human users who do not need direct kubelet access.

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 ClusterRole or Role whose rules include:
    • resources: ["nodes/proxy"] with any verbs, or
    • resources: ["nodes"] (or including nodes) with verbs containing "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).