Skip to main content

Minimize Access To The Proxy Sub-Resource Of Nodes

More Info:

Access to the Node proxy sub-resource exposes the kubelet API and can bypass audit logging and admission control. It should be limited to trusted administrators only.

Risk Level

High

Address

Security

Compliance Standards

  • CIS AKS

Triage and Remediation

Remediation

Manual Steps
  1. List all roles/clusterroles that can access the Node proxy sub-resource

    • Run on: any machine with kubectl access
    kubectl get clusterrole -o yaml | grep -En 'kind: ClusterRole|(^[[:space:]]*- apiGroups:)|(^[[:space:]]*resources:)|(^[[:space:]]*verbs:)|(^[[:space:]]*resourceNames:)|proxy'
    kubectl get role --all-namespaces -o yaml | grep -En 'kind: Role|(^[[:space:]]*- apiGroups:)|(^[[:space:]]*resources:)|(^[[:space:]]*verbs:)|(^[[:space:]]*resourceNames:)|proxy'

    Focus on entries where resources: includes nodes/proxy or nodes and verbs: includes get, list, create, or *.

  2. Extract detailed definitions of suspicious roles/clusterroles

    • From the names found in step 1, get the full YAML for review.
    • Run on: any machine with kubectl access
    kubectl get clusterrole <CLUSTERROLE_NAME> -o yaml
    kubectl get role -n <NAMESPACE> <ROLE_NAME> -o yaml

    Review whether nodes/proxy (or broad nodes with * verbs) is really needed for the intended purpose of that role.

  3. Identify who is actually getting this proxy access (bindings)

    • Run on: any machine with kubectl access
    kubectl get clusterrolebinding -o yaml | grep -En 'kind: ClusterRoleBinding|roleRef:|name:|subjects:'
    kubectl get rolebinding --all-namespaces -o yaml | grep -En 'kind: RoleBinding|roleRef:|name:|subjects:'

    For each role/clusterrole from step 2 that has nodes/proxy access, find the corresponding roleRef in these bindings and note which users, groups, or serviceAccounts receive that access.

  4. Decide which subjects truly need Node proxy access

    • For each bound subject identified in step 3, validate with your operational/incident-response requirements:
      • Trusted cluster administrators: generally may retain this access.
      • Automation/service accounts, application accounts, and regular users: usually should not need direct kubelet API access via Node proxy.
    • Document a list of subjects and whether nodes/proxy is required, optional, or not justified.
  5. Tighten or remove excessive nodes/proxy permissions

    • Run on: any machine with kubectl access
    • Option A – Remove nodes/proxy from the role/clusterrole (preferred):
    kubectl edit clusterrole <CLUSTERROLE_NAME>
    # or for namespace-scoped:
    kubectl edit role -n <NAMESPACE> <ROLE_NAME>

    In the editor, under rules:, remove nodes/proxy (or overly broad nodes rules) from resources: for subjects that should not have it; narrow verbs: if needed. Save and exit.

    • Option B – If a shared role is too broad, create a new, restricted role and rebind:
    kubectl create role <NEW_ROLE_NAME> -n <NAMESPACE> --verb=<VERBS_WITHOUT_PROXY> --resource=<RESOURCES_WITHOUT_NODES/PROXY>
    kubectl edit rolebinding -n <NAMESPACE> <ROLEBINDING_NAME>

    Update roleRef to point to the new restricted role for non-admin subjects.

  6. Re-verify that only trusted admins retain Node proxy access

    • Run on: any machine with kubectl access
    # Re-audit roles/clusterroles for nodes/proxy
    kubectl get clusterrole -o yaml | grep -En 'nodes/proxy'
    kubectl get role --all-namespaces -o yaml | grep -En 'nodes/proxy'

    For any remaining nodes/proxy entries, repeat the binding review:

    kubectl get clusterrolebinding -o yaml | grep -En 'roleRef:|name:|subjects:'
    kubectl get rolebinding --all-namespaces -o yaml | grep -En 'roleRef:|name:|subjects:'

    Confirm that all roles with nodes/proxy are bound only to explicitly approved, trusted administrator identities.

Using kubectl
# 1. List all Roles/ClusterRoles that reference 'nodes' with 'proxy' access
# Run on: any machine with kubectl access

kubectl get clusterroles,roles -A -o json \
| jq -r '
.items[]
| . as $role
| (.rules[]? // [])
| select(
(.resources[]? == "nodes" or .resources[]? == "nodes/proxy") and
(.verbs[]? == "proxy" or .verbs[]? == "*")
)
| "\($role.kind)/\($role.metadata.namespace // "cluster")/\($role.metadata.name)"
' | sort -u

What to look for:
Each line is a ClusterRole or Role that can reach the nodes proxy sub-resource (either explicitly via proxy or implicitly via * on verbs). Any entry here must be reviewed; these are the only candidates that can expose the kubelet API through the Node proxy.


# 2. For each suspicious role, show full rule details
# Example for a ClusterRole named 'system:node-proxy-access'

kubectl get clusterrole system:node-proxy-access -o yaml

What to look for in the output:

In rules::

  • Problematic resource/verb combinations (need human judgement):

    rules:
    - apiGroups: [""]
    resources:
    - nodes
    - nodes/proxy
    verbs:
    - get
    - list
    - watch
    - proxy # explicit proxy access
    # or:
    - '*' # implicitly includes proxy
  • Higher risk patterns:

    • resources: ["*"] with verbs: ["*"] or including proxy
    • resources: ["nodes"] + verbs: ["*"]
    • Any role intended for non-admins (developers, CI, apps) that has nodes with proxy or * verbs.

Roles with such rules and bound to non-admin users/groups/service accounts are likely too permissive.


# 3. Show which subjects are bound to each identified ClusterRole/Role
# Replace NAME and NAMESPACE based on step 1 output

# For ClusterRole bindings:
kubectl get clusterrolebindings -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "ClusterRole" and .roleRef.name == "system:node-proxy-access")
| .metadata.name as $crb
| .subjects[]?
| "\($crb): \(.kind)/\(.namespace // "cluster")/\(.name)"
'

# For Role bindings (namespace-scoped):
kubectl get rolebindings -A -o json \
| jq -r '
.items[]
| select(.roleRef.kind == "Role" and .roleRef.name == "node-proxy-role")
| .metadata.namespace as $ns
| .metadata.name as $rb
| .subjects[]?
| "\($ns)/\($rb): \(.kind)/\(.namespace // "cluster")/\(.name)"
'

What to look for:

  • Non-admin or broad subjects, for example:
    • system:authenticated or system:unauthenticated
    • Wildcard groups (e.g., system:serviceaccounts, system:serviceaccounts:<ns>)
    • Application service accounts in app namespaces
    • External user groups used by regular developers

Access to nodes proxy should normally be restricted to a small, trusted administrator group or break-glass account.


# 4. Cluster-wide view: all roles that can proxy nodes with their bindings
# (This gives a review worksheet; still needs human judgement)

kubectl get clusterroles,roles -A -o json \
| jq -r '
def has_node_proxy:
any(.rules[]?; (
any(.resources[]?; . == "nodes" or . == "nodes/proxy") and
any(.verbs[]?; . == "proxy" or . == "*")
));
.items[]
| select(has_node_proxy)
| {
kind: .kind,
name: .metadata.name,
namespace: (.metadata.namespace // "cluster")
}
' | while read -r line; do
kind=$(echo "$line" | jq -r '.kind')
name=$(echo "$line" | jq -r '.name')
ns=$(echo "$line" | jq -r '.namespace')

echo "=== $kind $ns/$name ==="

if [ "$kind" = "ClusterRole" ]; then
kubectl get clusterrole "$name" -o json \
| jq '.rules[] | select(
( .resources? // [] | index("nodes") or index("nodes/proxy") ) and
( .verbs? // [] | index("proxy") or index("*") )
)'
echo "-- Bindings --"
kubectl get clusterrolebindings -o json \
| jq -r "
.items[]
| select(.roleRef.kind == \"ClusterRole\" and .roleRef.name == \"$name\")
| .metadata.name as \$crb
| .subjects[]?
| \"\(\$crb): \(.kind)/\(.namespace // \"cluster\")/\(.name)\"
"
else
kubectl get role -n "$ns" "$name" -o json \
| jq '.rules[] | select(
( .resources? // [] | index("nodes") or index("nodes/proxy") ) and
( .verbs? // [] | index("proxy") or index("*") )
)'
echo "-- Bindings --"
kubectl get rolebindings -n "$ns" -o json \
| jq -r "
.items[]
| select(.roleRef.kind == \"Role\" and .roleRef.name == \"$name\")
| .metadata.name as \$rb
| .subjects[]?
| \"${ns}/\(\$rb): \(.kind)/\(.namespace // \"cluster\")/\(.name)\"
"
fi
echo
done

What to look for:

  • Any role where:
    • The rule snippet shows nodes/nodes/proxy with proxy or * verbs, and
    • Bindings list subjects that are not clearly trusted administrators.

Those combinations are candidates to tighten or remove based on your access policy.


# 5. Verification after you adjust RBAC (manual policy decision required)

kubectl get clusterroles,roles -A -o json \
| jq -r '
.items[]
| . as $role
| (.rules[]? // [])
| select(
(.resources[]? == "nodes" or .resources[]? == "nodes/proxy") and
(.verbs[]? == "proxy" or .verbs[]? == "*")
)
| "\($role.kind)/\($role.metadata.namespace // "cluster")/\($role.metadata.name)"
' | sort -u

Success indication:

  • The output is empty, or
  • Any remaining roles are intentionally approved for trusted administrators only, with bindings you have reviewed and accepted.
Automation
#!/usr/bin/env bash
# Report Roles/ClusterRoles that can access the Node proxy sub‑resource
# Run from any machine with kubectl access and cluster‑wide RBAC read permissions.

set -euo pipefail

echo "=== ClusterRoles with Node proxy access ==="
kubectl get clusterroles -o json \
| jq -r '
.items[]
| {
name: .metadata.name,
rules: .rules[]
}
| select(
(
(.rules.resources? // []) | index("nodes/proxy")
)
or
(
(.rules.resources? // []) | any(. == "nodes" or . == "nodes/*")
and ((.rules.verbs? // []) | any(. == "proxy" or . == "*" ))
)
)
| [
"ClusterRole: " + .name,
" Resources: " + ((.rules.resources // []) | join(", ")),
" Verbs: " + ((.rules.verbs // []) | join(", ")),
" API grps: " + ((.rules.apiGroups // []) | join(", ")),
""
]
| .[]
'

echo
echo "=== Roles with Node proxy access (all namespaces) ==="
kubectl get roles --all-namespaces -o json \
| jq -r '
.items[]
| {
ns: .metadata.namespace,
name: .metadata.name,
rules: .rules[]
}
| select(
(
(.rules.resources? // []) | index("nodes/proxy")
)
or
(
(.rules.resources? // []) | any(. == "nodes" or . == "nodes/*")
and ((.rules.verbs? // []) | any(. == "proxy" or . == "*" ))
)
)
| [
"Role: " + .ns + "/" + .name,
" Resources: " + ((.rules.resources // []) | join(", ")),
" Verbs: " + ((.rules.verbs // []) | join(", ")),
" API grps: " + ((.rules.apiGroups // []) | join(", ")),
""
]
| .[]
'

echo
echo "=== Note ==="
echo "Any entry above represents a candidate for review."
echo "- High risk: rules that include 'nodes/proxy', 'nodes/*' or verb 'proxy' or '*' on 'nodes'."
echo "- Ensure only tightly controlled admin roles retain such access; others should be removed or scoped down."

Explanation of problematic output:

  • Any listed ClusterRole or Role is granting some form of Node proxy access.
  • Especially concerning:
    • resources contains nodes/proxy or nodes/*.
    • resources contains nodes and verbs contains proxy or *.
  • For each such role, review who is bound to it (via RoleBinding/ClusterRoleBinding) and decide whether they are trusted administrators. If not, adjust or remove the permission manually.