AnalysisType: rule
RuleID: "GCP.K8S.Pod.Create.Or.Modify.Host.Path.Volume.Mount"
DisplayName: "GCP K8S Pod Create Or Modify Host Path Volume Mount"
Enabled: false
Status: Deprecated
LogTypes:
- GCP.AuditLog
Severity: High
Tags:
- Deprecated
Description: >
This detection monitors for pod creation with a hostPath volume mount. The attachment to a node's volume can allow
for privilege escalation through underlying vulnerabilities or it can open up possibilities for data exfiltration
or unauthorized file access. It is very rare to see this being a pod requirement. System service accounts in the
kube-system namespace are excluded to prevent false positives from legitimate system components.
Runbook: |
Investigate the reason of adding hostPath volume mount. Advise that it is discouraged practice.
Create ticket if appropriate.
Reference: https://kubernetes.io/docs/concepts/security/pod-security-standards/#host-namespaces
Reports:
MITRE ATT&CK:
- TA0010:T1041 # Exfiltration Over C2 Channel
- TA0004:T1611 # Escape to Host
Filename: gcp_k8s_pod_create_or_modify_host_path_vol_mount.py
DedupPeriodMinutes: 360
Tests:
- Name: Pod With Suspicious Volume Mount Created
ExpectedResult: true
Log:
{
"logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": { "principalEmail": "some.user@company.com" },
"authorizationInfo":
[
{
"granted": true,
"permission": "io.k8s.core.v1.pods.create",
"resource": "core/v1/namespaces/default/pods/test",
},
],
"methodName": "io.k8s.core.v1.pods.create",
"request":
{
"@type": "core.k8s.io/v1.Pod",
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "test", "namespace": "default" },
"spec":
{
"containers":
[
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "test",
"volumeMounts":
[{ "mountPath": "/test", "name": "test-volume" }],
},
],
"volumes":
[
{
"hostPath":
{
"path": "/var/lib/kubelet",
"type": "DirectoryOrCreate",
},
"name": "test-volume",
},
],
},
},
"requestMetadata":
{
"callerIP": "1.2.3.4",
"callerSuppliedUserAgent": "kubectl/v1.28.2 (darwin/amd64) kubernetes/89a4ea3",
},
"resourceName": "core/v1/namespaces/default/pods/test",
"response":
{
"spec":
{
"containers":
[
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "test",
"volumeMounts":
[{ "mountPath": "/test", "name": "test-volume" }],
},
],
"volumes":
[
{
"hostPath":
{
"path": "/var/lib/kubelet",
"type": "DirectoryOrCreate",
},
"name": "test-volume",
},
],
},
"status": { "phase": "Pending", "qosClass": "BestEffort" },
},
},
"receiveTimestamp": "2024-02-16 11:48:43.531373988",
"resource":
{
"labels":
{
"cluster_name": "some-project-cluster",
"location": "us-west1",
"project_id": "some-project",
},
"type": "k8s_cluster",
},
"timestamp": "2024-02-16 11:48:22.742154000",
}
- Name: Pod With Non-Suspicious Volume Mount Created
ExpectedResult: false
Log:
{
"logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": { "principalEmail": "some.user@company.com" },
"authorizationInfo":
[
{
"granted": true,
"permission": "io.k8s.core.v1.pods.create",
"resource": "core/v1/namespaces/default/pods/test",
},
],
"methodName": "io.k8s.core.v1.pods.create",
"request":
{
"@type": "core.k8s.io/v1.Pod",
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "test", "namespace": "default" },
"spec":
{
"containers":
[
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "test",
"volumeMounts":
[{ "mountPath": "/test", "name": "test-volume" }],
},
],
"volumes":
[
{
"hostPath":
{ "path": "/data", "type": "DirectoryOrCreate" },
"name": "test-volume",
},
],
},
},
"requestMetadata":
{
"callerIP": "1.2.3.4",
"callerSuppliedUserAgent": "kubectl/v1.28.2 (darwin/amd64) kubernetes/89a4ea3",
},
"resourceName": "core/v1/namespaces/default/pods/test",
"response":
{
"spec":
{
"containers":
[
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "test",
"volumeMounts":
[{ "mountPath": "/test", "name": "test-volume" }],
},
],
"volumes":
[
{
"hostPath":
{ "path": "/data", "type": "DirectoryOrCreate" },
"name": "test-volume",
},
],
},
"status": { "phase": "Pending", "qosClass": "BestEffort" },
},
},
"receiveTimestamp": "2024-02-16 11:48:43.531373988",
"resource":
{
"labels":
{
"cluster_name": "some-project-cluster",
"location": "us-west1",
"project_id": "some-project",
},
"type": "k8s_cluster",
},
"timestamp": "2024-02-16 11:48:22.742154000",
}
- Name: Pod Not Created
ExpectedResult: False
Log:
{
"logName": "projects/some-project/logs/cloudaudit.googleapis.com%2Factivity",
"protoPayload":
{
"at_sign_type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": { "principalEmail": "some.user@company.com" },
"authorizationInfo":
[
{
"granted": true,
"permission": "io.k8s.core.v1.pods.create",
"resource": "core/v1/namespaces/default/pods/test",
},
],
"methodName": "io.k8s.core.v1.pods.create",
"request":
{
"@type": "core.k8s.io/v1.Pod",
"apiVersion": "v1",
"kind": "Pod",
"metadata": { "name": "test", "namespace": "default" },
"spec":
{
"containers":
[
{
"image": "nginx",
"imagePullPolicy": "Always",
"name": "test",
"volumeMounts":
[{ "mountPath": "/test", "name": "test-volume" }],
},
],
"volumes":
[
{
"hostPath":
{
"path": "/var/lib/kubelet",
"type": "DirectoryOrCreate",
},
"name": "test-volume",
},
],
},
"status": {},
},
"resourceName": "core/v1/namespaces/default/pods/test",
"response": { "status": "Failure" },
},
"receiveTimestamp": "2024-02-16 12:55:17.003485190",
"resource":
{
"labels":
{
"cluster_name": "some-project-cluster",
"location": "us-west1",
"project_id": "some-project",
},
"type": "k8s_cluster",
},
"timestamp": "2024-02-16 12:55:00.510160000",
}
# ------ paired body: gcp_k8s_pod_create_or_modify_host_path_vol_mount.py ------
from panther_gcp_helpers import gcp_alert_context, is_gke_system_namespace, is_gke_system_principal
SUSPICIOUS_PATHS = [
"/var/run/docker.sock",
"/var/run/crio/crio.sock",
"/var/lib/kubelet",
"/var/lib/kubelet/pki",
"/var/lib/docker/overlay2",
"/etc/kubernetes",
"/etc/kubernetes/manifests",
"/etc/kubernetes/pki",
"/home/admin",
]
def rule(event):
# Check basic conditions
if event.deep_get("protoPayload", "response", "status") == "Failure" or event.deep_get(
"protoPayload", "methodName"
) not in (
"io.k8s.core.v1.pods.create",
"io.k8s.core.v1.pods.update",
"io.k8s.core.v1.pods.patch",
):
return False
# Check if volume mount path is suspicious
volume_mount_path = event.deep_walk(
"protoPayload", "request", "spec", "volumes", "hostPath", "path"
)
has_suspicious_path = volume_mount_path and (
volume_mount_path in SUSPICIOUS_PATHS
or any(path in SUSPICIOUS_PATHS for path in volume_mount_path)
)
if not has_suspicious_path:
return False
# Check if this is a known GKE system service account or system namespace
principal_email = event.deep_get(
"protoPayload", "authenticationInfo", "principalEmail", default=""
)
resource_name = event.deep_get("protoPayload", "resourceName", default="")
if is_gke_system_principal(principal_email) or is_gke_system_namespace(resource_name):
return False
# Check authorization
authorization_info = event.deep_walk("protoPayload", "authorizationInfo")
if not authorization_info:
return False
for auth in authorization_info:
if (
auth.get("permission")
in (
"io.k8s.core.v1.pods.create",
"io.k8s.core.v1.pods.update",
"io.k8s.core.v1.pods.patch",
)
and auth.get("granted") is True
):
return True
return False
def title(event):
actor = event.deep_get(
"protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
)
pod_name = event.deep_get("protoPayload", "resourceName", default="<RESOURCE_NOT_FOUND>")
project_id = event.deep_get("resource", "labels", "project_id", default="<PROJECT_NOT_FOUND>")
return (
f"[GCP]: [{actor}] created k8s pod [{pod_name}] with a hostPath volume mount "
f"in project [{project_id}]"
)
def dedup(event):
return event.deep_get(
"protoPayload", "authenticationInfo", "principalEmail", default="<ACTOR_NOT_FOUND>"
)
def alert_context(event):
context = gcp_alert_context(event)
volume_mount_path = event.deep_walk(
"protoPayload", "request", "spec", "volumes", "hostPath", "path"
)
context["volume_mount_path"] = volume_mount_path
return context