Skip to main content

Minimize Access To The Proxy Sub-Resource Of Node Objects

More Info:​

The proxy sub-resource of Node objects allows direct access to the kubelet API, bypassing usual controls. Restrict access to this sub-resource to trusted administrators only.

Risk Level​

Medium

Address​

Security

Compliance Standards​

  • CIS EKS

Triage and Remediation​

Remediation​

Manual Steps
  1. List all ClusterRoles and Roles that can use the proxy subresource on Nodes

    • Run on: any machine with kubectl access
    kubectl get clusterroles -o json |
    jq -r '
    .items[] |
    select(
    .rules[]? |
    (.resources[]? | select(.=="nodes/proxy")) or
    (.resourceNames[]? | contains("nodes/proxy"))
    ) |
    .metadata.name
    ' | sort -u
    kubectl get roles --all-namespaces -o json |
    jq -r '
    .items[] |
    select(
    .rules[]? |
    (.resources[]? | select(.=="nodes/proxy")) or
    (.resourceNames[]? | contains("nodes/proxy"))
    ) |
    (.metadata.namespace + ":" + .metadata.name)
    ' | sort -u
  2. Inspect each identified Role/ClusterRole to confirm the exact permissions

    • Run on: any machine with kubectl access
    • For each ClusterRole name from step 1:
      kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
    • For each Role NAMESPACE:NAME from step 1:
      kubectl get role -n <NAMESPACE> <NAME> -o yaml
    • Manually review rules entries that reference nodes/proxy or resources: ["nodes"] with resourceNames: ["proxy"] and note who should legitimately have these permissions (typically only a small set of cluster admin roles).
  3. Identify which subjects are bound to those Roles/ClusterRoles

    • Run on: any machine with kubectl access
    kubectl get clusterrolebindings -o json |
    jq -r '
    .items[] |
    select(.roleRef.kind=="ClusterRole" and .roleRef.name as $r |
    $r == "<CLUSTERROLE_NAME_1>" or
    $r == "<CLUSTERROLE_NAME_2>" # add others from step 1
    ) |
    .metadata.name
    '
    kubectl get rolebindings --all-namespaces -o json |
    jq -r '
    .items[] |
    select(.roleRef.kind=="Role" and
    (.roleRef.name == "<ROLE_NAME_1>" or
    .roleRef.name == "<ROLE_NAME_2>")) | # add others
    (.metadata.namespace + ":" + .metadata.name)
    '
    • For each binding found, inspect subjects:
      kubectl get clusterrolebinding <CRB_NAME> -o yaml
      kubectl get rolebinding -n <NAMESPACE> <RB_NAME> -o yaml
    • Decide which users/groups/service accounts truly require node proxy access (trusted administrators) and which do not.
  4. Restrict or remove unnecessary nodes/proxy access in Roles/ClusterRoles

    • Run on: any machine with kubectl access
    • For each Role/ClusterRole where non-admin use is not required, edit to remove the nodes/proxy permission (or scope it more narrowly if absolutely required):
      kubectl edit clusterrole <CLUSTERROLE_NAME>
      or
      kubectl edit role -n <NAMESPACE> <ROLE_NAME>
    • In the editor, delete the rules entries granting nodes/proxy where not needed, or move such permissions into a dedicated admin-only ClusterRole. Save and exit.
  5. Tighten bindings so only trusted administrators receive nodes/proxy rights

    • Run on: any machine with kubectl access
    • For roles that must retain nodes/proxy:
      • Ensure they are only bound to trusted admin users/groups/service accounts.
      • Edit bindings to remove non-admin subjects or move them to safer roles lacking nodes/proxy:
        kubectl edit clusterrolebinding <CRB_NAME>
        kubectl edit rolebinding -n <NAMESPACE> <RB_NAME>
  6. Verify that only intended admin roles retain nodes/proxy access

    • Run on: any machine with kubectl access
    • Re-run discovery and confirm the remaining roles and bindings correspond only to trusted admins:
      kubectl get clusterroles -o json |
      jq -r '
      .items[] |
      select(.rules[]? | .resources[]? | select(.=="nodes/proxy")) |
      .metadata.name
      ' | sort -u
      kubectl get roles --all-namespaces -o json |
      jq -r '
      .items[] |
      select(.rules[]? | .resources[]? | select(.=="nodes/proxy")) |
      (.metadata.namespace + ":" + .metadata.name)
      ' | sort -u
    • Manually confirm that any remaining roles with nodes/proxy are intentionally granted only to trusted administrator subjects.
Using kubectl
# 1) List all ClusterRoles and Roles that grant node/proxy access
# Run on: any machine with kubectl access
kubectl get clusterroles,roles -A -o yaml \
| awk '
$1=="kind:"{k=$2}
$1=="name:"{n=$2}
/resources:/ {inres=1}
inres && /- nodes/ {nodes=1}
inres && /verbs:/ {inverbs=1}
inverbs && /- proxy/ && nodes {print k, n; inres=inverbs=nodes=0}
'

# 2) Show full definitions of the roles identified above
# Replace <ROLE_KIND> and <ROLE_NAME> with values from step 1
kubectl get <ROLE_KIND> <ROLE_NAME> -o yaml

# 3) Find which subjects are bound to these roles (ClusterRoleBindings)
# For ClusterRoles only
kubectl get clusterrolebindings -o yaml \
| awk '
$1=="kind:"{k=$2}
$1=="name:"{n=$2}
/roleRef:/ {inrole=1}
inrole && /kind: ClusterRole/ {getline; gsub(/name: /,""); cr=$2}
/subjects:/ {insub=1}
insub && /- kind:/ {subj_kind=$2}
insub && /name:/ {subj_name=$2; print k, n, "->", cr, "->", subj_kind, subj_name}
'

# 4) For namespace-scoped Roles, see which ServiceAccounts are bound
kubectl get rolebindings -A -o yaml \
| awk '
$1=="kind:"{k=$2}
$1=="name:"{n=$2}
/namespace:/ {ns=$2}
/roleRef:/ {inrole=1}
inrole && /kind: Role/ {getline; gsub(/name: /,""); r=$2}
/subjects:/ {insub=1}
insub && /- kind:/ {subj_kind=$2}
insub && /name:/ {subj_name=$2; print ns, k, n, "->", r, "->", subj_kind, subj_name}
'

What indicates a problem:

  • In step 1, any ClusterRole or Role that:
    • Lists resources: ["nodes", "nodes/proxy"] or includes nodes/proxy, and
    • Includes verbs such as proxy, *, or broad verbs (e.g., get, list, watch, create) that include proxy.
  • In step 3 and 4, those roles are:
    • Bound to broad subjects like system:authenticated, system:unauthenticated, system:masters, or large user groups.
    • Bound to application ServiceAccounts not intended to have direct kubelet access.
    • Used by default/built-in bindings in a way that grants many identities access to nodes/proxy.

These cases require human review to decide whether to narrow or remove nodes/proxy and/or re-scope the bindings to only trusted administrator identities.

Automation
#!/usr/bin/env bash
#
# Report subjects (users/groups/serviceaccounts) that can access the
# "proxy" sub-resource of "nodes" (any verb) via RBAC.
#
# Run on: any machine with kubectl access and appropriate privileges.
# Requirements: kubectl, jq

set -euo pipefail

echo "Checking ClusterRoles with access to 'nodes/proxy' ..."

# 1) Find all ClusterRoles that grant any verb on nodes/proxy
kubectl get clusterroles -o json | jq -r '
.items[]
| {
name: .metadata.name,
rules: (
.rules // []
| map(
select(
# resource must include "nodes"
((.resources // []) | index("nodes"))
and
# resourceNames or apiGroups do not matter for subresource
# subresource must include "proxy"
((.resources // []) | any(. == "nodes/proxy" or . == "nodes/proxy" or . == "nodes/*"))
or
(
# some roles use "nodes/proxy" explicitly, others may use "nodes/*"
((.resources // []) | index("nodes/proxy"))
or
((.resources // []) | index("nodes/*"))
)
)
)
)
}
| select(.rules | length > 0)
| .name
' | sort -u | tee /tmp/nodes-proxy-clusterroles.txt

echo
echo "Checking Roles with access to 'nodes/proxy' (namespaced) ..."

# 2) Find all namespaced Roles that grant any verb on nodes/proxy
kubectl get roles -A -o json | jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
rules: (
.rules // []
| map(
select(
((.resources // []) | index("nodes/proxy"))
or
((.resources // []) | index("nodes/*"))
)
)
)
}
| select(.rules | length > 0)
| "\(.ns) \(.name)"
' | sort -u | tee /tmp/nodes-proxy-roles.txt

echo
echo "Resolving which subjects are bound to these roles ..."

echo "ClusterRoleBindings involving nodes/proxy-related ClusterRoles:"
if [[ -s /tmp/nodes-proxy-clusterroles.txt ]]; then
while read -r cr; do
echo "=== ClusterRole: ${cr} ==="
kubectl get clusterrolebindings -o json | jq -r --arg CR "$cr" '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == $CR)
| {
crb: .metadata.name,
subjects: (.subjects // [])
}
| "ClusterRoleBinding: \(.crb)\nSubjects:\n" +
( .subjects[]
| " - kind=\(.kind) name=\(.name) namespace=\(.namespace // "-")"
)
' || true
done < /tmp/nodes-proxy-clusterroles.txt
else
echo " (none)"
fi

echo
echo "RoleBindings involving nodes/proxy-related Roles:"
if [[ -s /tmp/nodes-proxy-roles.txt ]]; then
while read -r ns name; do
echo "=== Role: ${name} (namespace: ${ns}) ==="
kubectl get rolebindings -n "$ns" -o json | jq -r --arg NS "$ns" --arg RN "$name" '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == $RN)
| {
rb: .metadata.name,
subjects: (.subjects // [])
}
| "RoleBinding: \(.rb)\nSubjects:\n" +
( .subjects[]
| " - kind=\(.kind) name=\(.name) namespace=\(.namespace // "-")"
)
' || true
done < /tmp/nodes-proxy-roles.txt
else
echo " (none)"
fi

echo
echo "Summary:"
echo " - ClusterRoles granting nodes/proxy: $(wc -l < /tmp/nodes-proxy-clusterroles.txt || echo 0)"
echo " - Roles (namespaced) granting nodes/proxy: $(wc -l < /tmp/nodes-proxy-roles.txt || echo 0)"

cat <<'EOF'

How to interpret this output:

1. Any ClusterRole or Role listed above grants some form of access to the
'proxy' sub-resource of Node objects (often via resources like
'nodes/proxy' or 'nodes/*').

2. For each such role, review the bound subjects:
- kind=User: individual users
- kind=Group: user groups
- kind=ServiceAccount: workloads running in the cluster

3. Potential problems:
- Broad, generic roles (e.g. cluster-admin, system:masters, or
custom "admin"/"dev" roles) bound to many subjects.
- ServiceAccounts used by application workloads that do not need
direct kubelet/proxy access.
- Wildcard access (verbs: ["*"] or resources: ["nodes/*"]) granted
to non-privileged users/groups.

4. Acceptable/expected:
- A very small set of trusted administrator identities or tightly
controlled operator/service accounts, where this access is
explicitly justified.

This script does NOT change any configuration. Use it to identify where
'access to nodes/proxy' exists, then manually decide which role rules
or bindings to tighten or remove.
EOF