Kubernetes Role With Node Proxy Permissions Created


Description

This detection monitors for Roles or ClusterRoles being created with permissions to access node proxy endpoints (nodes/proxy or nodes/*). These permissions allow users to access the kubelet API through the Kubernetes API server proxy, enabling privilege escalation by executing commands on nodes, accessing container logs and filesystems, and potentially escaping to the underlying host. This technique is documented by Stratus Red Team as a privilege escalation vector.

Query · python

from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context


def rule(event):
    verb = event.udm("verb")
    resource = event.udm("resource")
    username = event.udm("username")
    response_status = event.udm("responseStatus")

    # Only check role/clusterrole creation events
    if verb != "create" or resource not in {"roles", "clusterroles"}:
        return False

    # Skip failed requests
    if is_failed_request(response_status):
        return False

    # Exclude system principals to reduce false positives
    if is_system_principal(username):
        return False

    # Check request object for node/proxy permissions
    request_object = event.udm("requestObject") or {}
    rules = request_object.get("rules") or []

    for rule_config in rules:
        resources = rule_config.get("resources") or []
        verbs = rule_config.get("verbs") or []

        # Check for nodes/proxy or nodes/* permissions
        # These allow accessing the kubelet API through the API server proxy
        if "nodes/proxy" in resources or ("nodes/*" in resources and verbs):
            return True

        # Also check for wildcard on nodes with specific verbs that enable proxy access
        if "nodes" in resources and "*" in resources:
            return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
    name = event.udm("name") or "<UNKNOWN_ROLE>"

    role_type = "ClusterRole" if resource == "clusterroles" else "Role"

    return f"[{username}] created {role_type} [{name}] with node proxy permissions"


def dedup(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    name = event.udm("name") or "<UNKNOWN_ROLE>"
    return f"k8s_node_proxy_{username}_{name}"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    rules = request_object.get("rules") or []

    # Extract rules with node/proxy permissions
    dangerous_rules = []
    for rule_config in rules:
        resources = rule_config.get("resources") or []
        if "nodes/proxy" in resources or "nodes/*" in resources or "nodes" in resources:
            dangerous_rules.append(rule_config)

    return k8s_alert_context(
        event,
        extra_fields={
            "role_name": event.udm("name"),
            "role_type": event.udm("resource"),
            "dangerous_rules": dangerous_rules,
        },
    )

Analyst notes

  1. Query all RBAC operations by the username in the 2 hours before and after the alert to identify the scope of role creation activity
  2. Find all RoleBindings or ClusterRoleBindings that reference this role name in the 24 hours after creation to determine who was granted these permissions
  3. Search for API operations to nodes/proxy endpoints from the p_source_label cluster in the past 7 days to identify if these permissions have been exploited
Raw source Kubernetes Role With Node Proxy Permissions Created · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
RuleID: "Kubernetes.Role.NodeProxyPermissions"
DisplayName: "Kubernetes Role With Node Proxy Permissions Created"
Enabled: true
Filename: k8s_role_node_proxy_permissions.py
LogTypes:
  - Amazon.EKS.Audit
  - Azure.MonitorActivity
  - GCP.AuditLog
Tags:
  - Kubernetes
  - Privilege Escalation
  - RBAC
  - Stratus Red Team
  - Unified Detection
Severity: High
Description: >
  This detection monitors for Roles or ClusterRoles being created with permissions to access node proxy endpoints
  (nodes/proxy or nodes/*). These permissions allow users to access the kubelet API through the Kubernetes API server
  proxy, enabling privilege escalation by executing commands on nodes, accessing container logs and filesystems, and
  potentially escaping to the underlying host. This technique is documented by Stratus Red Team as a privilege
  escalation vector.
Runbook: |
  1. Query all RBAC operations by the username in the 2 hours before and after the alert to identify the scope of role creation activity
  2. Find all RoleBindings or ClusterRoleBindings that reference this role name in the 24 hours after creation to determine who was granted these permissions
  3. Search for API operations to nodes/proxy endpoints from the p_source_label cluster in the past 7 days to identify if these permissions have been exploited
Reports:
  Stratus Red Team:
    - k8s.privilege-escalation.nodes-proxy
  MITRE ATT&CK:
    - TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
    - TA0005:T1562.001 # Defense Evasion: Impair Defenses - Disable or Modify Tools
Reference: https://stratus-red-team.cloud/attack-techniques/kubernetes/k8s.privilege-escalation.nodes-proxy/
DedupPeriodMinutes: 60
SummaryAttributes:
  - username
  - resource
  - name
  - p_source_label
Tests:
  - Name: EKS ClusterRole with nodes/proxy permission
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "apiVersion": "audit.k8s.io/v1",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "sourceIPs": ["203.0.113.42"],
        "userAgent": "kubectl/v1.28.0",
        "objectRef": {
          "resource": "clusterroles",
          "name": "node-accessor",
          "apiGroup": "rbac.authorization.k8s.io",
          "apiVersion": "v1"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "ClusterRole",
          "metadata": {"name": "node-accessor"},
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes/proxy"],
              "verbs": ["get", "list", "create"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: AKS ClusterRole with nodes/* permission
    ExpectedResult: true
    Log:
      {
        "p_log_type": "Azure.MonitorActivity",
        "category": "kube-audit",
        "operationName": "Microsoft.ContainerService/managedClusters/diagnosticLogs/Read",
        "properties": {
          "log": "{\"kind\":\"Event\",\"apiVersion\":\"audit.k8s.io/v1\",\"verb\":\"create\",\"user\":{\"username\":\"malicious-user@example.com\"},\"sourceIPs\":[\"1.2.3.4\"],\"objectRef\":{\"resource\":\"clusterroles\",\"name\":\"node-admin\",\"apiGroup\":\"rbac.authorization.k8s.io\"},\"responseStatus\":{\"code\":201},\"requestObject\":{\"kind\":\"ClusterRole\",\"metadata\":{\"name\":\"node-admin\"},\"rules\":[{\"apiGroups\":[\"\"],\"resources\":[\"nodes/*\"],\"verbs\":[\"*\"]}]}}"
        },
        "p_source_label": "aks-cluster"
      }
  - Name: GKE ClusterRole with nodes proxy access
    ExpectedResult: true
    Log:
      {
        "protoPayload": {
          "authenticationInfo": {"principalEmail": "user@company.com"},
          "authorizationInfo": [{
            "granted": true,
            "permission": "io.k8s.rbac.v1.clusterroles.create",
            "resource": "rbac.authorization.k8s.io/v1/clusterroles/node-proxy-role"
          }],
          "methodName": "io.k8s.rbac.v1.clusterroles.create",
          "requestMetadata": {"callerIP": "8.8.8.8"},
          "resourceName": "rbac.authorization.k8s.io/v1/clusterroles/node-proxy-role",
          "serviceName": "k8s.io",
          "request": {
            "kind": "ClusterRole",
            "metadata": {"name": "node-proxy-role"},
            "rules": [
              {
                "apiGroups": [""],
                "resources": ["nodes/proxy", "nodes/log"],
                "verbs": ["get", "create"]
              }
            ]
          }
        },
        "resource": {
          "type": "k8s_cluster",
          "labels": {"project_id": "test-project"}
        },
        "p_log_type": "GCP.AuditLog",
        "p_source_label": "gke-cluster"
      }
  - Name: EKS Role with nodes wildcard
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "suspicious-user@example.com"},
        "objectRef": {
          "resource": "clusterroles",
          "name": "node-reader",
          "apiGroup": "rbac.authorization.k8s.io"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "ClusterRole",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes", "*"],
              "verbs": ["get", "list"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: Role with only node list permission (no proxy)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "clusterroles",
          "name": "node-viewer",
          "apiGroup": "rbac.authorization.k8s.io"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "ClusterRole",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes"],
              "verbs": ["get", "list"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Role with pod permissions (not nodes)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "roles",
          "namespace": "default",
          "name": "pod-admin",
          "apiGroup": "rbac.authorization.k8s.io"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "Role",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["pods", "pods/log"],
              "verbs": ["get", "list", "create"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: System principal creating role (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "system:serviceaccount:kube-system:operator"},
        "objectRef": {
          "resource": "clusterroles",
          "name": "system-role"
        },
        "responseStatus": {"code": 201},
        "requestObject": {
          "kind": "ClusterRole",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes/proxy"],
              "verbs": ["get"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Failed request (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "objectRef": {
          "resource": "clusterroles",
          "name": "node-proxy-role"
        },
        "responseStatus": {"code": 403},
        "requestObject": {
          "kind": "ClusterRole",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes/proxy"],
              "verbs": ["get", "create"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Updating existing role (not creating)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "update",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "clusterroles",
          "name": "existing-role"
        },
        "responseStatus": {"code": 200},
        "requestObject": {
          "kind": "ClusterRole",
          "rules": [
            {
              "apiGroups": [""],
              "resources": ["nodes/proxy"],
              "verbs": ["get"]
            }
          ]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }


# ------ paired body: k8s_role_node_proxy_permissions.py ------

from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context


def rule(event):
    verb = event.udm("verb")
    resource = event.udm("resource")
    username = event.udm("username")
    response_status = event.udm("responseStatus")

    # Only check role/clusterrole creation events
    if verb != "create" or resource not in {"roles", "clusterroles"}:
        return False

    # Skip failed requests
    if is_failed_request(response_status):
        return False

    # Exclude system principals to reduce false positives
    if is_system_principal(username):
        return False

    # Check request object for node/proxy permissions
    request_object = event.udm("requestObject") or {}
    rules = request_object.get("rules") or []

    for rule_config in rules:
        resources = rule_config.get("resources") or []
        verbs = rule_config.get("verbs") or []

        # Check for nodes/proxy or nodes/* permissions
        # These allow accessing the kubelet API through the API server proxy
        if "nodes/proxy" in resources or ("nodes/*" in resources and verbs):
            return True

        # Also check for wildcard on nodes with specific verbs that enable proxy access
        if "nodes" in resources and "*" in resources:
            return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
    name = event.udm("name") or "<UNKNOWN_ROLE>"

    role_type = "ClusterRole" if resource == "clusterroles" else "Role"

    return f"[{username}] created {role_type} [{name}] with node proxy permissions"


def dedup(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    name = event.udm("name") or "<UNKNOWN_ROLE>"
    return f"k8s_node_proxy_{username}_{name}"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    rules = request_object.get("rules") or []

    # Extract rules with node/proxy permissions
    dangerous_rules = []
    for rule_config in rules:
        resources = rule_config.get("resources") or []
        if "nodes/proxy" in resources or "nodes/*" in resources or "nodes" in resources:
            dangerous_rules.append(rule_config)

    return k8s_alert_context(
        event,
        extra_fields={
            "role_name": event.udm("name"),
            "role_type": event.udm("resource"),
            "dangerous_rules": dangerous_rules,
        },
    )

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.