Kubernetes Data Copy via kubectl cp


Description

This detection monitors for kubectl cp operations that copy files from pods to local machines, which can indicate data exfiltration. When kubectl cp is used to copy files from a pod, it executes a tar command with stdout output (tar cf -) inside the container and streams the data back through the Kubernetes API server. Attackers who gain cluster access can use this technique to steal application secrets, credentials, configuration files, or sensitive data from container filesystems without leaving obvious traces inside the pod itself. While kubectl cp has legitimate uses for debugging and backup, unexpected usage should be investigated.

Query · python

from urllib.parse import parse_qs, urlparse

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


def get_exec_command(event):
    """Extract exec command from requestObject (GCP/Azure) or requestURI query params (EKS).

    Returns:
        List of command arguments, or empty list if not found
    """
    # Try requestObject first (GCP/Azure format)
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command")
    if command:
        return command

    # Fall back to parsing requestURI query parameters (EKS format)
    # EKS format: /api/v1/.../exec?command=tar&command=cf&command=-
    request_uri = event.udm("requestURI") or ""
    if "command=" in request_uri:
        try:
            parsed = urlparse(request_uri)
            params = parse_qs(parsed.query)
            return params.get("command", [])
        except Exception:  # pylint: disable=broad-except
            return []

    return []


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 not in ("create", "get") or resource != "pods" or subresource != "exec":
        return False

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

    # Exclude system principals creating pods in system namespaces (legitimate)
    # but alert on system principals in user namespaces (malicious Deployments)
    # and alert on user-created pods in system namespaces (suspicious)
    if is_system_principal(username) and is_system_namespace(namespace):
        return False

    # Extract command from either requestObject or requestURI
    command = get_exec_command(event)
    if not command:
        return False

    # Check if command contains tar with cf - pattern
    # tar cf - indicates copying FROM pod (stdout output = exfil)
    tar_found = False
    cf_flag_found = False
    stdout_dash_found = False

    for i, arg in enumerate(command):
        arg_str = str(arg).lower()

        if "tar" in arg_str:
            tar_found = True

        # Look for cf flag (create+file) - handles: cf, -cf, czf, -czf, etc.
        if "cf" in arg_str and arg_str != "-c":
            cf_flag_found = True

        # Check if - appears after cf flag was found (stdout redirect)
        if arg_str == "-" and i > 0 and cf_flag_found:
            stdout_dash_found = True

    return tar_found and cf_flag_found and stdout_dash_found


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

    command = get_exec_command(event)

    # Extract the path being copied if possible
    # Command format: ["tar", "cf", "-", "/path/to/file"]
    path = "<UNKNOWN_PATH>"
    if len(command) >= 4:
        for i, arg in enumerate(command):
            if arg == "-" and i + 1 < len(command):
                path = str(command[i + 1])
                break

    return f"[{username}] copied data from pod [{namespace}/{name}] path [{path}] via kubectl cp"


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_kubectl_cp_{username}_{namespace}_{name}"


def severity(event):
    """Increase severity for copying from sensitive paths."""
    command = get_exec_command(event)
    command_str = " ".join(str(arg) for arg in command).lower()

    # Critical severity for copying credentials or SSH keys
    critical_patterns = [
        "/root",
        ".ssh",
        "id_rsa",
        "id_ecdsa",
        "id_ed25519",
        "credentials",
        "secrets",
        "token",
        ".kube",
        "serviceaccount",
    ]
    if any(pattern in command_str for pattern in critical_patterns):
        return "CRITICAL"

    # High severity for copying from sensitive system directories
    sensitive_paths = ["/etc", "/var/run", "/proc", "config", "password", "shadow"]
    if any(path in command_str for path in sensitive_paths):
        return "HIGH"

    return "MEDIUM"


def alert_context(event):
    command = get_exec_command(event)
    request_object = event.udm("requestObject") or {}

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

Analyst notes

  1. Review all exec and kubectl cp operations by this user in the 24 hours before and after the alert
  2. Identify what files or directories were exfiltrated and assess their sensitivity
  3. Search for other suspicious API activity from this user or service account across all clusters in the past 7 days
Raw source Kubernetes Data Copy via kubectl cp · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
RuleID: "Kubernetes.Kubectl.CP.Operation"
DisplayName: "Kubernetes Data Copy via kubectl cp"
Enabled: true
Filename: k8s_kubectl_cp_operation.py
LogTypes:
  - Amazon.EKS.Audit
  - Azure.MonitorActivity
  - GCP.AuditLog
Tags:
  - Kubernetes
  - Exfiltration
  - Data Theft
  - Credential Access
  - Unified Detection
Severity: Medium
Description: >
  This detection monitors for kubectl cp operations that copy files from pods to local machines,
  which can indicate data exfiltration. When kubectl cp is used to copy files from a pod, it
  executes a tar command with stdout output (tar cf -) inside the container and streams the data
  back through the Kubernetes API server. Attackers who gain cluster access can use this technique
  to steal application secrets, credentials, configuration files, or sensitive data from container
  filesystems without leaving obvious traces inside the pod itself. While kubectl cp has legitimate
  uses for debugging and backup, unexpected usage should be investigated.
Runbook: |
  1. Review all exec and kubectl cp operations by this user in the 24 hours before and after the alert
  2. Identify what files or directories were exfiltrated and assess their sensitivity
  3. Search for other suspicious API activity from this user or service account across all clusters in the past 7 days
Reports:
  MITRE ATT&CK:
    - TA0010:T1530 # Exfiltration: Data from Cloud Storage
    - TA0006:T1552 # Credential Access: Unsecured Credentials
Reference: https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#cp
DedupPeriodMinutes: 60
SummaryAttributes:
  - username
  - namespace
  - name
  - p_source_label
Tests:
  - Name: EKS kubectl cp from pod (requestURI format)
    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": 200},
        "requestURI": "/api/v1/namespaces/production/pods/webapp-pod/exec?command=tar&command=cf&command=-&command=/app/secrets/credentials.json&container=webapp&stdout=true",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit",
        "p_source_label": "eks-cluster"
      }
  - Name: AKS kubectl cp from pod with SSH keys
    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-server\"},\"responseStatus\":{\"code\":200},\"requestObject\":{\"command\":[\"tar\",\"cf\",\"-\",\"/root/.ssh/id_rsa\"],\"container\":\"api\"}}"
        },
        "p_source_label": "aks-cluster"
      }
  - Name: GKE kubectl cp from pod
    ExpectedResult: true
    Log:
      {
        "protoPayload": {
          "authenticationInfo": {"principalEmail": "user@company.com"},
          "authorizationInfo": [{
            "granted": true,
            "permission": "io.k8s.core.v1.pods.exec",
            "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": ["tar", "cf", "-", "/var/lib/postgresql/data/config"],
            "container": "postgres"
          }
        },
        "resource": {
          "type": "k8s_cluster",
          "labels": {"project_id": "test-project"}
        },
        "p_log_type": "GCP.AuditLog",
        "p_source_label": "gke-cluster"
      }
  - Name: EKS kubectl cp TO pod (not exfiltration)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "objectRef": {"resource": "pods", "subresource": "exec", "namespace": "default", "name": "app-pod"},
        "responseStatus": {"code": 200},
        "requestURI": "/api/v1/namespaces/default/pods/app-pod/exec?command=tar&command=xf&command=-&command=-C&command=/tmp&container=app",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: EKS regular tar archive creation
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "objectRef": {"resource": "pods", "subresource": "exec", "namespace": "default", "name": "backup-pod"},
        "responseStatus": {"code": 200},
        "requestURI": "/api/v1/namespaces/default/pods/backup-pod/exec?command=tar&command=czf&command=backup.tar.gz&command=/app/data",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: EKS regular exec without tar
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "admin@example.com"},
        "objectRef": {"resource": "pods", "subresource": "exec", "namespace": "default", "name": "debug-pod"},
        "responseStatus": {"code": 200},
        "requestURI": "/api/v1/namespaces/default/pods/debug-pod/exec?command=/bin/sh&stdin=true&tty=true",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: EKS system namespace (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "system:serviceaccount:kube-system:debug"},
        "objectRef": {"resource": "pods", "subresource": "exec", "namespace": "kube-system", "name": "debug-pod"},
        "responseStatus": {"code": 200},
        "requestURI": "/api/v1/namespaces/kube-system/pods/debug-pod/exec?command=tar&command=cf&command=-&command=/var/log",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit"
      }
  - Name: EKS failed request (excluded)
    ExpectedResult: false
    Log:
      {
        "kind": "Event",
        "verb": "create",
        "user": {"username": "attacker@example.com"},
        "objectRef": {"resource": "pods", "subresource": "exec", "namespace": "default", "name": "secret-pod"},
        "responseStatus": {"code": 403},
        "requestURI": "/api/v1/namespaces/default/pods/secret-pod/exec?command=tar&command=cf&command=-&command=/app/secrets",
        "requestObject": null,
        "p_log_type": "Amazon.EKS.Audit"
      }

# ------ paired body: k8s_kubectl_cp_operation.py ------

from urllib.parse import parse_qs, urlparse

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


def get_exec_command(event):
    """Extract exec command from requestObject (GCP/Azure) or requestURI query params (EKS).

    Returns:
        List of command arguments, or empty list if not found
    """
    # Try requestObject first (GCP/Azure format)
    request_object = event.udm("requestObject") or {}
    command = request_object.get("command")
    if command:
        return command

    # Fall back to parsing requestURI query parameters (EKS format)
    # EKS format: /api/v1/.../exec?command=tar&command=cf&command=-
    request_uri = event.udm("requestURI") or ""
    if "command=" in request_uri:
        try:
            parsed = urlparse(request_uri)
            params = parse_qs(parsed.query)
            return params.get("command", [])
        except Exception:  # pylint: disable=broad-except
            return []

    return []


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 not in ("create", "get") or resource != "pods" or subresource != "exec":
        return False

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

    # Exclude system principals creating pods in system namespaces (legitimate)
    # but alert on system principals in user namespaces (malicious Deployments)
    # and alert on user-created pods in system namespaces (suspicious)
    if is_system_principal(username) and is_system_namespace(namespace):
        return False

    # Extract command from either requestObject or requestURI
    command = get_exec_command(event)
    if not command:
        return False

    # Check if command contains tar with cf - pattern
    # tar cf - indicates copying FROM pod (stdout output = exfil)
    tar_found = False
    cf_flag_found = False
    stdout_dash_found = False

    for i, arg in enumerate(command):
        arg_str = str(arg).lower()

        if "tar" in arg_str:
            tar_found = True

        # Look for cf flag (create+file) - handles: cf, -cf, czf, -czf, etc.
        if "cf" in arg_str and arg_str != "-c":
            cf_flag_found = True

        # Check if - appears after cf flag was found (stdout redirect)
        if arg_str == "-" and i > 0 and cf_flag_found:
            stdout_dash_found = True

    return tar_found and cf_flag_found and stdout_dash_found


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

    command = get_exec_command(event)

    # Extract the path being copied if possible
    # Command format: ["tar", "cf", "-", "/path/to/file"]
    path = "<UNKNOWN_PATH>"
    if len(command) >= 4:
        for i, arg in enumerate(command):
            if arg == "-" and i + 1 < len(command):
                path = str(command[i + 1])
                break

    return f"[{username}] copied data from pod [{namespace}/{name}] path [{path}] via kubectl cp"


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_kubectl_cp_{username}_{namespace}_{name}"


def severity(event):
    """Increase severity for copying from sensitive paths."""
    command = get_exec_command(event)
    command_str = " ".join(str(arg) for arg in command).lower()

    # Critical severity for copying credentials or SSH keys
    critical_patterns = [
        "/root",
        ".ssh",
        "id_rsa",
        "id_ecdsa",
        "id_ed25519",
        "credentials",
        "secrets",
        "token",
        ".kube",
        "serviceaccount",
    ]
    if any(pattern in command_str for pattern in critical_patterns):
        return "CRITICAL"

    # High severity for copying from sensitive system directories
    sensitive_paths = ["/etc", "/var/run", "/proc", "config", "password", "shadow"]
    if any(path in command_str for path in sensitive_paths):
        return "HIGH"

    return "MEDIUM"


def alert_context(event):
    command = get_exec_command(event)
    request_object = event.udm("requestObject") or {}

    return k8s_alert_context(
        event,
        extra_fields={
            "pod_name": event.udm("name"),
            "command": command,
            "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.