Kubernetes Service Account Token Theft from Pod


Description

This detection monitors for commands executed in pods that attempt to read service account tokens from /var/run/secrets/kubernetes.io/serviceaccount/token. Attackers who gain exec access to a pod can steal its service account token to authenticate as that service account to the Kubernetes API server. This enables privilege escalation and lateral movement within the cluster. This is a known attack technique documented by Stratus Red Team.

Query · python

from urllib.parse import unquote

from panther_kubernetes_helpers import (
    is_failed_request,
    is_system_namespace,
    is_system_principal,
    k8s_alert_context,
)

# Paths related to service account tokens that attackers target
SERVICE_ACCOUNT_TOKEN_PATHS = {
    # Standard Kubernetes service account token
    "/var/run/secrets/kubernetes.io/serviceaccount/token",
    "/var/run/secrets/kubernetes.io/serviceaccount",
    # AWS EKS with IRSA (IAM Roles for Service Accounts)
    "/var/run/secrets/eks.amazonaws.com/serviceaccount/token",
    "/var/run/secrets/eks.amazonaws.com/serviceaccount",
    # Azure AKS with Workload Identity
    "/var/run/secrets/azure/tokens/azure-identity-token",
    "/var/run/secrets/azure/tokens",
    # GCP GKE with Workload Identity
    "/var/run/secrets/tokens/gcp-ksa/token",
    "/var/run/secrets/tokens/gcp-ksa",
    # Partial path match for various token access patterns
    "serviceaccount/token",
}


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

    # Only check exec subresource operations
    if verb != "create" or resource != "pods" or subresource != "exec":
        return False

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

    # Exclude system principals in system namespaces (legitimate operations)
    # but alert on users stealing tokens from system namespaces (malicious)
    if is_system_principal(username) and is_system_namespace(namespace):
        return False

    # Check command in requestObject
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command", [])
    command_str = " ".join(str(cmd) for cmd in command).lower()

    # Check if command references service account token paths
    if any(path.lower() in command_str for path in SERVICE_ACCOUNT_TOKEN_PATHS):
        return True

    # Check requestURI (may contain URL-encoded paths)
    request_uri = event.udm("requestURI") or ""
    # URL decode to handle encoded characters like %2F for /
    decoded_uri = unquote(request_uri).lower()

    if any(path.lower() in decoded_uri for path in SERVICE_ACCOUNT_TOKEN_PATHS):
        return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_POD>"

    return (
        f"[{username}] attempted to steal service account token from pod " f"[{namespace}/{name}]"
    )


def dedup(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_POD>"
    return f"k8s_steal_token_{username}_{namespace}_{name}"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command", [])
    request_uri = event.udm("requestURI") or ""

    return k8s_alert_context(
        event,
        extra_fields={
            "pod_name": event.udm("name"),
            "command": command,
            "request_uri": request_uri,
            "container": request_object.get("container"),
        },
    )

Analyst notes

  1. Immediately investigate the user executing commands in the pod and determine if they are authorized to access this pod
  2. Review the service account permissions for this pod to assess what privileges the attacker could gain with the stolen token
  3. Search for API operations using the stolen service account token in the past 24 hours by filtering audit logs for this service account principal
Raw source Kubernetes Service Account Token Theft from Pod · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
RuleID: "Kubernetes.Pod.StealServiceAccountToken"
DisplayName: "Kubernetes Service Account Token Theft from Pod"
Enabled: true
Filename: k8s_steal_serviceaccount_token.py
LogTypes:
  - Amazon.EKS.Audit
  - Azure.MonitorActivity
  - GCP.AuditLog
Tags:
  - Kubernetes
  - Credential Access
  - Privilege Escalation
  - Lateral Movement
  - Unified Detection
Severity: High
Description: >
  This detection monitors for commands executed in pods that attempt to read service account tokens from
  /var/run/secrets/kubernetes.io/serviceaccount/token. Attackers who gain exec access to a pod can steal
  its service account token to authenticate as that service account to the Kubernetes API server. This
  enables privilege escalation and lateral movement within the cluster. This is a known attack technique
  documented by Stratus Red Team.
Runbook: |
  1. Immediately investigate the user executing commands in the pod and determine if they are authorized to access this pod
  2. Review the service account permissions for this pod to assess what privileges the attacker could gain with the stolen token
  3. Search for API operations using the stolen service account token in the past 24 hours by filtering audit logs for this service account principal
Reports:
  Stratus Red Team:
    - k8s.credential-access.steal-serviceaccount-token
  MITRE ATT&CK:
    - TA0006:T1552.007 # Credential Access: Unsecured Credentials - Container API
    - TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
    - TA0008:T1550.001 # Lateral Movement: Use Alternate Authentication Material - Application Access Token
Reference: >
  - https://stratus-red-team.cloud/attack-techniques/kubernetes/k8s.credential-access.steal-serviceaccount-token/
  - https://medium.com/google-cloud/from-whoami-to-whoarewe-with-gke-workload-identity-for-fleets-3795ee942187
  - https://dev.to/piyushjajoo/understanding-kubernetes-projected-service-account-tokens-205f
DedupPeriodMinutes: 60
SummaryAttributes:
  - username
  - namespace
  - name
  - p_source_label
Tests:
  - Name: EKS cat service account token
    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": "pods",
          "subresource": "exec",
          "namespace": "production",
          "name": "webapp-pod",
          "apiVersion": "v1"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"],
          "container": "webapp",
          "stdin": false,
          "stdout": true,
          "tty": false
        },
        "requestURI": "/api/v1/namespaces/production/pods/webapp-pod/exec?command=cat&command=%2Fvar%2Frun%2Fsecrets%2Fkubernetes.io%2Fserviceaccount%2Ftoken&stdout=true",
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: AKS read serviceaccount directory
    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\":\"pods\",\"subresource\":\"exec\",\"namespace\":\"default\",\"name\":\"api-pod\"},\"responseStatus\":{\"code\":101},\"requestObject\":{\"command\":[\"ls\",\"/var/run/secrets/kubernetes.io/serviceaccount\"],\"container\":\"api\"}}"
        },
        "p_source_label": "aks-cluster"
      }
  - Name: GKE steal token with sh -c
    ExpectedResult: true
    Log:
      {
        "protoPayload": {
          "authenticationInfo": {"principalEmail": "user@company.com"},
          "authorizationInfo": [{
            "granted": true,
            "permission": "io.k8s.core.v1.pods.exec.create",
            "resource": "core/v1/namespaces/production/pods/database/exec"
          }],
          "methodName": "io.k8s.core.v1.pods.exec.create",
          "requestMetadata": {"callerIP": "8.8.8.8"},
          "resourceName": "core/v1/namespaces/production/pods/database/exec",
          "serviceName": "k8s.io",
          "request": {
            "command": ["sh", "-c", "cat /var/run/secrets/kubernetes.io/serviceaccount/token"],
            "container": "postgres"
          }
        },
        "resource": {
          "type": "k8s_cluster",
          "labels": {"project_id": "test-project"}
        },
        "p_log_type": "GCP.AuditLog",
        "p_source_label": "gke-cluster"
      }
  - Name: EKS copy entire serviceaccount directory
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "default",
          "name": "compromised-pod"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["tar", "czf", "-", "/var/run/secrets/kubernetes.io/serviceaccount"]
        },
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: Regular exec without token access
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "default",
          "name": "debug-pod"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["/bin/sh"],
          "stdin": true,
          "tty": true
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Regular user in system namespace (alert)
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "kube-system",
          "name": "system-pod"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: System principal in regular namespace (alert)
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "system:serviceaccount:kube-system:controller"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "default",
          "name": "app-pod"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: System principal in system namespace (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "system:serviceaccount:kube-system:kube-proxy"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "kube-system",
          "name": "system-pod"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: Failed exec (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "production",
          "name": "secure-pod"
        },
        "responseStatus": {"code": 403},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"]
        },
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: EKS IRSA token theft
    ExpectedResult: true
    Log:
      {
        "kind": "Event",
        "apiVersion": "audit.k8s.io/v1",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "sourceIPs": ["10.0.1.50"],
        "objectRef": {
          "resource": "pods",
          "subresource": "exec",
          "namespace": "production",
          "name": "irsa-pod",
          "apiVersion": "v1"
        },
        "responseStatus": {"code": 101},
        "requestObject": {
          "command": ["cat", "/var/run/secrets/eks.amazonaws.com/serviceaccount/token"],
          "container": "app",
          "stdout": true
        },
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-prod-cluster"
      }
  - Name: AKS workload identity token theft
    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\":\"attacker@company.com\"},\"sourceIPs\":[\"192.168.1.10\"],\"objectRef\":{\"resource\":\"pods\",\"subresource\":\"exec\",\"namespace\":\"production\",\"name\":\"workload-id-pod\"},\"responseStatus\":{\"code\":101},\"requestObject\":{\"command\":[\"cat\",\"/var/run/secrets/azure/tokens/azure-identity-token\"],\"container\":\"main\"}}"
        },
        "p_source_label": "aks-prod-cluster"
      }
  - Name: GKE workload identity token theft
    ExpectedResult: true
    Log:
      {
        "protoPayload": {
          "authenticationInfo": {"principalEmail": "malicious@example.com"},
          "authorizationInfo": [{
            "granted": true,
            "permission": "io.k8s.core.v1.pods.exec.create",
            "resource": "core/v1/namespaces/default/pods/gke-pod/exec"
          }],
          "methodName": "io.k8s.core.v1.pods.exec.create",
          "requestMetadata": {"callerIP": "35.1.2.3"},
          "resourceName": "core/v1/namespaces/default/pods/gke-pod/exec",
          "serviceName": "k8s.io",
          "request": {
            "command": ["cat", "/var/run/secrets/tokens/gcp-ksa/token"],
            "container": "application"
          }
        },
        "resource": {
          "type": "k8s_cluster",
          "labels": {"project_id": "production-project"}
        },
        "p_log_type": "GCP.AuditLog",
        "p_source_label": "gke-production"
      }

# ------ paired body: k8s_steal_serviceaccount_token.py ------

from urllib.parse import unquote

from panther_kubernetes_helpers import (
    is_failed_request,
    is_system_namespace,
    is_system_principal,
    k8s_alert_context,
)

# Paths related to service account tokens that attackers target
SERVICE_ACCOUNT_TOKEN_PATHS = {
    # Standard Kubernetes service account token
    "/var/run/secrets/kubernetes.io/serviceaccount/token",
    "/var/run/secrets/kubernetes.io/serviceaccount",
    # AWS EKS with IRSA (IAM Roles for Service Accounts)
    "/var/run/secrets/eks.amazonaws.com/serviceaccount/token",
    "/var/run/secrets/eks.amazonaws.com/serviceaccount",
    # Azure AKS with Workload Identity
    "/var/run/secrets/azure/tokens/azure-identity-token",
    "/var/run/secrets/azure/tokens",
    # GCP GKE with Workload Identity
    "/var/run/secrets/tokens/gcp-ksa/token",
    "/var/run/secrets/tokens/gcp-ksa",
    # Partial path match for various token access patterns
    "serviceaccount/token",
}


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

    # Only check exec subresource operations
    if verb != "create" or resource != "pods" or subresource != "exec":
        return False

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

    # Exclude system principals in system namespaces (legitimate operations)
    # but alert on users stealing tokens from system namespaces (malicious)
    if is_system_principal(username) and is_system_namespace(namespace):
        return False

    # Check command in requestObject
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command", [])
    command_str = " ".join(str(cmd) for cmd in command).lower()

    # Check if command references service account token paths
    if any(path.lower() in command_str for path in SERVICE_ACCOUNT_TOKEN_PATHS):
        return True

    # Check requestURI (may contain URL-encoded paths)
    request_uri = event.udm("requestURI") or ""
    # URL decode to handle encoded characters like %2F for /
    decoded_uri = unquote(request_uri).lower()

    if any(path.lower() in decoded_uri for path in SERVICE_ACCOUNT_TOKEN_PATHS):
        return True

    return False


def title(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_POD>"

    return (
        f"[{username}] attempted to steal service account token from pod " f"[{namespace}/{name}]"
    )


def dedup(event):
    username = event.udm("username") or "<UNKNOWN_USER>"
    namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
    name = event.udm("name") or "<UNKNOWN_POD>"
    return f"k8s_steal_token_{username}_{namespace}_{name}"


def alert_context(event):
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command", [])
    request_uri = event.udm("requestURI") or ""

    return k8s_alert_context(
        event,
        extra_fields={
            "pod_name": event.udm("name"),
            "command": command,
            "request_uri": request_uri,
            "container": request_object.get("container"),
        },
    )

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.