Detection rules › Panther
Panther rules: k8s
Kubernetes Admission Controller Webhook Created
#This detection monitors for creation of MutatingWebhookConfiguration or ValidatingWebhookConfiguration resources. Admission controller webhooks can intercept all API requests to the Kubernetes API server, allowing attackers to inspect, modify, or block any resource creation or modification. This provides powerful capabilities for persistence (modifying deployments to inject backdoors), credential theft (intercepting secrets), and reconnaissance (enumerating all cluster activity).
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Credential Access | |
| Collection |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
Detection logic
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
# Admission controller webhook resource types
WEBHOOK_RESOURCES = {
"mutatingwebhookconfigurations",
"validatingwebhookconfigurations",
}
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
response_status = event.udm("responseStatus")
# Only check webhook creation events
if verb != "create":
return False
if resource not in WEBHOOK_RESOURCES:
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude noise from cluster maintenance
username = event.udm("username")
if is_system_principal(username):
return False
# Alert on any admission controller webhook creation
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
resource = event.udm("resource") or "webhook"
name = event.udm("name") or "<UNKNOWN_NAME>"
webhook_type = "Mutating" if "mutating" in resource.lower() else "Validating"
return f"[{username}] created {webhook_type} admission controller webhook [{name}] "
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
return f"k8s_admission_webhook_{username}"
def severity(event):
"""Increase severity for webhooks that intercept all resources."""
webhooks = event.udm("webhooks") or []
for webhook in webhooks:
rules = webhook.get("rules", [])
for rule_config in rules:
resources = rule_config.get("resources", [])
api_groups = rule_config.get("apiGroups", [])
# Check for wildcard rules that intercept everything
if "*" in resources or "*" in api_groups:
return "HIGH"
return "MEDIUM"
def alert_context(event):
webhooks = event.udm("webhooks") or []
# Extract webhook details
webhook_details = []
for webhook in webhooks:
client_config = webhook.get("clientConfig", {})
webhook_details.append(
{
"name": webhook.get("name"),
"url": client_config.get("url"),
"service": client_config.get("service"),
"failure_policy": webhook.get("failurePolicy"),
"rules": webhook.get("rules", []),
}
)
return k8s_alert_context(
event,
extra_fields={
"webhook_name": event.udm("name"),
"webhook_type": event.udm("resource"),
"webhooks": webhook_details,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.AdmissionController.Created"
DisplayName: "Kubernetes Admission Controller Webhook Created"
Enabled: true
Filename: k8s_admission_controller_created.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Persistence
- Credential Access
- Collection
- Unified Detection
Severity: Medium
Description: >
This detection monitors for creation of MutatingWebhookConfiguration or ValidatingWebhookConfiguration
resources. Admission controller webhooks can intercept all API requests to the Kubernetes API server,
allowing attackers to inspect, modify, or block any resource creation or modification. This provides
powerful capabilities for persistence (modifying deployments to inject backdoors), credential theft
(intercepting secrets), and reconnaissance (enumerating all cluster activity).
Runbook: |
1. Review the webhook configuration details including the target webhook service URL and failure policy
2. Identify all API operations performed by the username in the 48 hours before webhook creation to establish intent
3. Search for other webhook configurations or suspicious API activity from this user across all clusters in the past 7 days
Reports:
MITRE ATT&CK:
- TA0003:T1546 # Persistence: Event Triggered Execution
- TA0006:T1552 # Credential Access: Unsecured Credentials
- TA0009:T1530 # Collection: Data from Cloud Storage Object
Reference: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceis one ofmutatingwebhookconfigurations,validatingwebhookconfigurationsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | eq |
| field:"verb" kind:eq value:"create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review the webhook configuration details including the target webhook service URL and failure policy
2. Identify all API operations performed by the username in the 48 hours before webhook creation to establish intent
3. Search for other webhook configurations or suspicious API activity from this user across all clusters in the past 7 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "admissionregistration.k8s.io",
"apiVersion": "v1",
"name": "custom-mutator",
"resource": "mutatingwebhookconfigurations"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "MutatingWebhookConfiguration",
"metadata": {
"name": "custom-mutator"
},
"webhooks": [
{
"clientConfig": {
"url": "https://webhook.example.com/mutate"
},
"name": "mutate.example.com",
"rules": [
{
"apiGroups": [
"*"
],
"apiVersions": [
"*"
],
"operations": [
"CREATE",
"UPDATE"
],
"resources": [
"*"
]
}
]
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes All Secrets Dumped Across Namespaces
#This detection monitors for cluster-wide secret list operations that dump all secrets across all namespaces in a single API call. Attackers with list secrets permissions at the cluster level can trivially access every secret in the cluster using the LIST /api/v1/secrets API, exposing all credentials, tokens, and sensitive configuration data. This is a known attack technique documented by Stratus Red Team and represents mass credential theft.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | list-secrets: list secrets |
Rules detecting the same action
These rules filter on the same operation.
- Azure AKS Secret get or list with Suspicious User Agent (Elastic)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Secret Access from Node or Denied Service Account (Elastic)
- GKE Secret Access via Unusual User Agent (Elastic)
- GKE Secret get or list with Suspicious User Agent (Elastic)
- GKE Secrets List from Unusual Source AS Organization (Elastic)
- Kubernetes Secret Access via Unusual User Agent (Elastic)
- Kubernetes Secret Enumeration by a User (Panther)
Detection logic
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
# System components that legitimately list all secrets cluster-wide
ALLOWED_SECRET_LISTERS = {
"system:serviceaccount:kube-system:namespace-controller",
"system:serviceaccount:kube-system:kube-state-metrics",
"system:apiserver",
}
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username") or ""
response_status = event.udm("responseStatus")
request_uri = event.udm("requestURI") or ""
# Only check secret list operations that are successful
if verb != "list" or resource != "secrets" or is_failed_request(response_status):
return False
# Key indicator: namespace is empty (cluster-wide list, not namespaced)
# and requestURI doesn't contain /namespaces/ in path
if namespace or (request_uri and "/namespaces/" in request_uri):
return False
# Exclude system principals and specific components
if is_system_principal(username) or username in ALLOWED_SECRET_LISTERS:
return False
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
request_uri = event.udm("requestURI") or ""
return f"[{username}] dumped all secrets across all namespaces (URI: {request_uri})"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
return f"k8s_secrets_dump_{username}"
def alert_context(event):
request_uri = event.udm("requestURI") or ""
# Extract query parameters if present (e.g., limit=500)
query_params = {}
if "?" in request_uri:
query_string = request_uri.split("?", 1)[1]
for param in query_string.split("&"):
if "=" in param:
key, value = param.split("=", 1)
query_params[key] = value
return k8s_alert_context(
event,
extra_fields={
"request_uri": request_uri,
"query_parameters": query_params,
"operation": "list_all_secrets",
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Secrets.DumpAllNamespaces"
DisplayName: "Kubernetes All Secrets Dumped Across Namespaces"
Enabled: true
Status: Experimental
Filename: k8s_secrets_dump_all_namespaces.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Credential Access
- Mass Exfiltration
- Secrets Management
- Unified Detection
Severity: Critical
Description: >
This detection monitors for cluster-wide secret list operations that dump all secrets across all namespaces in a
single API call. Attackers with list secrets permissions at the cluster level can trivially access every secret in
the cluster using the LIST /api/v1/secrets API, exposing all credentials, tokens, and sensitive configuration data.
This is a known attack technique documented by Stratus Red Team and represents mass credential theft.
Runbook: |
1. Immediately investigate the user or service account performing this operation and determine if they are compromised
2. Review what secrets exist in the cluster to assess the scope of credential exposure and rotate all sensitive credentials
3. Search for other suspicious API activity by this user in the past 24 hours and check for data exfiltration or lateral movement attempts
Reports:
Stratus Red Team:
- k8s.credential-access.dump-secrets
MITRE ATT&CK:
- TA0006:T1552.007 # Credential Access: Unsecured Credentials - Container API
- TA0009:T1530 # Collection: Data from Cloud Storage Object
Reference: https://stratus-red-team.cloud/attack-techniques/kubernetes/k8s.credential-access.dump-secrets/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbislistresourceissecretsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
namespaceis emptyany of:
requestURIis emptyrequestURIdoes not contain/namespaces/
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
usernameis not one ofsystem:serviceaccount:kube-system:namespace-controller,system:serviceaccount:kube-system:kube-state-metrics,system:apiserver
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Immediately investigate the user or service account performing this operation and determine if they are compromised
2. Review what secrets exist in the cluster to assess the scope of credential exposure and rotate all sensitive credentials
3. Search for other suspicious API activity by this user in the past 24 hours and check for data exfiltration or lateral movement attempts
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"resource": "secrets"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestURI": "/api/v1/secrets?limit=500",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"203.0.113.42"
],
"stage": "ResponseComplete",
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "list"
}
Kubernetes Anonymous API Access Detected
#This rule detects anonymous API requests made to Kubernetes API servers across AWS EKS, Azure AKS, and GCP GKE clusters. In production environments, anonymous access should be disabled to prevent unauthorized access to the API server.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Detection logic
from panther_kubernetes_helpers import k8s_alert_context
def is_system_access(event):
# Skip localhost and health check traffic
# nosec: Fallback to empty list is safe - we check for localhost separately
src_ip = event.udm("sourceIPs") or []
if src_ip == ["127.0.0.1"]:
return True
user_agent = event.udm("userAgent") or ""
# AWS EKS: Exclude ELB health checker from internal IPs
if user_agent == "ELB-HealthChecker/2.0" and src_ip and src_ip[0].startswith("10.0."):
return True
system_user_agents = (
"kube-probe/",
"GoogleHC/",
)
# GCP GKE & Azure AKS: Exclude kube-probe (Kubernetes liveness/readiness probes)
# GCP GKE: Exclude Google Cloud Load Balancer health checks
if any(user_agent.startswith(pattern) for pattern in system_user_agents):
return True
return False
def is_health_check(event):
request_uri = event.udm("requestURI") or ""
health_patterns = (
"/healthz",
"/readyz",
"/livez",
"/apis/healthz",
"/apis/readyz",
"/apis/livez",
)
if any(request_uri.startswith(pattern) for pattern in health_patterns):
return True
return False
def rule(event):
if event.udm("username") == "system:anonymous":
if not is_system_access(event) and not is_health_check(event):
return True
return False
def title(event):
# For failed attempts or /version endpoint, use generic titles
annotations = event.udm("annotations") or {}
if annotations.get("authorization.k8s.io/decision") != "allow":
return "Failed Anonymous Kubernetes API Access Attempt(s) Detected"
if event.udm("requestURI") == "/version":
return "Anonymous Kubernetes API Access to /version Endpoint Detected"
# For successful access to other endpoints, provide detailed information
source_ips = event.udm("sourceIPs") or []
source_ip = source_ips[0] if source_ips else "<UNKNOWN_IP>"
request_uri = event.udm("requestURI") or "<UNKNOWN_URI>"
return (
f"Anonymous API access detected on Kubernetes API server "
f"from [{source_ip}] to [{request_uri}]"
)
def severity(event):
annotations = event.udm("annotations") or {}
if annotations.get("authorization.k8s.io/decision") != "allow":
return "INFO"
if event.udm("requestURI") == "/version":
return "INFO"
return "DEFAULT"
def dedup(event):
user_agent = event.udm("userAgent") or "<UNKNOWN_USER_AGENT>"
return f"anonymous_access_{user_agent}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={"annotations": event.udm("annotations")},
)
Rule specification
AnalysisType: rule
Filename: k8s_anonymous_api_access.py
RuleID: "Kubernetes.API.AnonymousAccess"
DisplayName: "Kubernetes Anonymous API Access Detected"
Enabled: true
Status: Experimental
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Severity: Info
Reports:
MITRE ATT&CK:
- TA0001:T1190 # Initial Access: Exploit Public-Facing Application
Description: >
This rule detects anonymous API requests made to Kubernetes API servers across
AWS EKS, Azure AKS, and GCP GKE clusters. In production environments, anonymous
access should be disabled to prevent unauthorized access to the API server.
DedupPeriodMinutes: 60
Reference:
https://raesene.github.io/blog/2023/03/18/lets-talk-about-anonymous-access-to-Kubernetes/
Runbook: |
1. Query all API requests by the anonymous username (system:anonymous) in the 24 hours before and after the alert to identify scope of unauthorized access
2. Check if the sourceIPs field contains internal, cloud provider, or external addresses to determine access vector
3. Review other authentication attempts from the same sourceIPs in the past 7 days to identify related suspicious activity
SummaryAttributes:
- username
- p_any_ip_addresses
- p_source_label
Tags:
- Kubernetes
- Security Control
- API
- Initial Access
- Unified Detection
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
usernameissystem:anonymousany of:
userAgentis notELB-HealthChecker/2.0sourceIPsis empty
userAgentdoes not start withkube-probe/userAgentdoes not start withGoogleHC/requestURIdoes not start with/healthzrequestURIdoes not start with/readyzrequestURIdoes not start with/livezrequestURIdoes not start with/apis/healthzrequestURIdoes not start with/apis/readyzrequestURIdoes not start with/apis/livez
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
sourceIPs | is_not_null | excludes:sourceIPs | |
userAgent | eq | ELB-HealthChecker/2.0 | excludes:userAgent field:"userAgent" value:"ELB-HealthChecker/2.0" |
userAgent | starts_with | GoogleHC/ | excludes:userAgent field:"userAgent" value:"GoogleHC/" |
userAgent | starts_with | kube-probe/ | excludes:userAgent field:"userAgent" value:"kube-probe/" |
requestURI | starts_with | /apis/healthz | excludes:requestURI field:"requestURI" value:"/apis/healthz" |
requestURI | starts_with | /apis/livez | excludes:requestURI field:"requestURI" value:"/apis/livez" |
requestURI | starts_with | /apis/readyz | excludes:requestURI field:"requestURI" value:"/apis/readyz" |
requestURI | starts_with | /healthz | excludes:requestURI field:"requestURI" value:"/healthz" |
requestURI | starts_with | /livez | excludes:requestURI field:"requestURI" value:"/livez" |
requestURI | starts_with | /readyz | excludes:requestURI field:"requestURI" value:"/readyz" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | eq |
| field:"username" kind:eq value:"system:anonymous" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Query all API requests by the anonymous username (system:anonymous) in the 24 hours before and after the alert to identify scope of unauthorized access
2. Check if the sourceIPs field contains internal, cloud provider, or external addresses to determine access vector
3. Review other authentication attempts from the same sourceIPs in the past 7 days to identify related suspicious activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding system:public-info-viewer"
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "abcde12345",
"kind": "Event",
"level": "Request",
"objectRef": {
"apiVersion": "v1",
"name": "test-pod",
"namespace": "default",
"resource": "pods"
},
"p_any_ip_addresses": [
"8.8.8.8"
],
"p_any_usernames": [
"system:anonymous"
],
"p_event_time": "2022-11-29 00:09:04.38",
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "example-cluster-eks-logs",
"requestReceivedTimestamp": "2022-11-29 00:09:04.38",
"requestURI": "/api/v1/namespaces/default/pods/test-pod",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"8.8.8.8"
],
"stage": "ResponseComplete",
"user": {
"username": "system:anonymous"
},
"userAgent": "kubectl/v1.25.4"
}
Kubernetes API Activity from Tor Exit Node
#This detection monitors for Kubernetes API requests originating from known Indicators of Compromise, specifically Tor exit nodes. Tor usage may indicate attempts to hide the true source of malicious activity or unauthorized access attempts. This detection works across AWS EKS, Azure AKS, and GCP GKE clusters.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Detection logic
from panther_kubernetes_helpers import is_k8s_log, k8s_alert_context
def rule(event):
# Check if this is a Kubernetes audit log with Tor exit node enrichment
if is_k8s_log(event) and event.deep_get("p_enrichment", "tor_exit_nodes"):
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
tor_nodes = event.deep_get("p_enrichment", "tor_exit_nodes", default=[])
tor_ip = tor_nodes[0] if tor_nodes else "<UNKNOWN_IP>"
return f"Kubernetes API activity from Tor exit node [{tor_ip}] by user [{username}]"
def dedup(event):
tor_nodes = event.deep_get("p_enrichment", "tor_exit_nodes", default=[])
tor_ip = tor_nodes[0] if tor_nodes else "<UNKNOWN_IP>"
return f"k8s_tor_{tor_ip}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={"tor_exit_nodes": event.deep_get("p_enrichment", "tor_exit_nodes")},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.API.IOC.Activity"
DisplayName: "Kubernetes API Activity from Tor Exit Node"
Enabled: true
Filename: k8s_ioc_activity.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Command and Control
- Encrypted Channel
- Unified Detection
Severity: Medium
Description: >
This detection monitors for Kubernetes API requests originating from known Indicators
of Compromise, specifically Tor exit nodes. Tor usage may indicate attempts to hide
the true source of malicious activity or unauthorized access attempts. This detection
works across AWS EKS, Azure AKS, and GCP GKE clusters.
Runbook: |
1. Find all API operations performed by username through the Tor exit node IP in the 6 hours before and after the alert
2. Compare the operations and resources accessed against normal baseline activity for this user in the past 30 days to identify anomalous behavior
3. Check if the Tor exit node IP has accessed other clusters or sensitive resources in the past 24 hours to assess campaign scope
Reference: https://medium.com/snowflake/from-logs-to-detection-using-snowflake-and-panther-to-detect-k8s-threats-d72f70a504d7
Reports:
MITRE ATT&CK:
- TA0011:T1573.002 # Command and Control: Encrypted Channel - Asymmetric Cryptography
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- p_any_ip_addresses
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
p_log_typeis one ofAmazon.EKS.Audit,Azure.MonitorActivity,GCP.AuditLogany of:
all of:
p_log_typeisGCP.AuditLogprotoPayload.serviceNameisk8s.io
all of:
p_log_typeis notGCP.AuditLogp_log_typeisAzure.MonitorActivitycategoryis one ofkube-audit,kube-audit-admin
p_enrichment.tor_exit_nodesis present
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
category | in |
| field:"category" kind:in |
p_enrichment.tor_exit_nodes | is_not_null | field:"p_enrichment.tor_exit_nodes" kind:is_not_null | |
p_log_type | eq |
| field:"p_log_type" kind:eq |
p_log_type | in |
| field:"p_log_type" kind:in |
p_log_type | ne |
| field:"p_log_type" kind:ne value:"GCP.AuditLog" |
protoPayload.serviceName | eq |
| field:"gcp::service_name" kind:eq value:"k8s.io" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Find all API operations performed by username through the Tor exit node IP in the 6 hours before and after the alert
2. Compare the operations and resources accessed against normal baseline activity for this user in the past 30 days to identify anomalous behavior
3. Check if the Tor exit node IP has accessed other clusters or sensitive resources in the past 24 hours to assess campaign scope
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"namespace": "default",
"resource": "pods"
},
"p_enrichment": {
"tor_exit_nodes": [
"1.2.3.4"
]
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestURI": "/api/v1/namespaces/default/pods",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "list"
}
Kubernetes API Multiple 403 Responses from Single Public IP
#This detection identifies when a public source IP generates multiple 403 (Forbidden) responses from the Kubernetes API server. This pattern may indicate reconnaissance attempts, permission enumeration, brute force attacks, or misconfigured access. Private IPs are excluded as they typically represent legitimate internal traffic.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Detection logic
from ipaddress import ip_address
from panther_kubernetes_helpers import k8s_alert_context
def rule(event):
response_status = event.udm("responseStatus") or {}
source_ips = event.udm("sourceIPs") or []
# Check for 403 response (HTTP) or 7 (gRPC PERMISSION_DENIED)
status_code = response_status.get("code")
if status_code not in (403, 7):
return False
if not source_ips:
return False
try:
source_ip = source_ips[0]
ip_obj = ip_address(source_ip)
# alert on public IPs
# Check if is_global attribute exists (Python 3.4+)
if hasattr(ip_obj, "is_global") and ip_obj.is_global:
return True
except (ValueError, IndexError):
return False
return False
def title(event):
source_ips = event.udm("sourceIPs") or []
source_ip = source_ips[0] if source_ips else "<UNKNOWN_IP>"
return f"Multiple 403 responses from public IP [{source_ip}]"
def dedup(event):
source_ips = event.udm("sourceIPs") or []
source_ip = source_ips[0] if source_ips else "<UNKNOWN_IP>"
return f"k8s_403_{source_ip}"
def alert_context(event):
return k8s_alert_context(event)
Rule specification
AnalysisType: rule
Filename: k8s_multiple_403_public_ip.py
RuleID: "Kubernetes.API.Multiple403.PublicIP"
DisplayName: "Kubernetes API Multiple 403 Responses from Single Public IP"
Enabled: true
Status: Experimental
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Container and Resource Discovery
- Unified Detection
Reports:
MITRE ATT&CK:
- TA0007:T1613 # Container and Resource Discovery
Reference: https://aws.github.io/aws-eks-best-practices/security/docs/detective/
Severity: Info
Description: >
This detection identifies when a public source IP generates multiple 403 (Forbidden)
responses from the Kubernetes API server. This pattern may indicate reconnaissance
attempts, permission enumeration, brute force attacks, or misconfigured access.
Private IPs are excluded as they typically represent legitimate internal traffic.
Runbook: |
1. Find all API requests from the sourceIPs address in the 1 hour before and after the alert to identify attempted operations
2. Identify which API resources or operations the source IP attempted to access to determine if this is systematic probing or random scanning
3. Check if the same source IP has generated 403 errors against other Kubernetes clusters in the past 24 hours to assess campaign scope
DedupPeriodMinutes: 30
Threshold: 10
SummaryAttributes:
- username
- p_any_ip_addresses
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
responseStatus.codeis one of403,7sourceIPsis present
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
responseStatus.code | in |
| field:"responseStatus.code" kind:in |
sourceIPs | is_not_null | field:"sourceIPs" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Find all API requests from the sourceIPs address in the 1 hour before and after the alert to identify attempted operations
2. Identify which API resources or operations the source IP attempted to access to determine if this is systematic probing or random scanning
3. Check if the same source IP has generated 403 errors against other Kubernetes clusters in the past 24 hours to assess campaign scope
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiGroup": "discovery.k8s.io",
"apiVersion": "v1",
"resource": "endpointslices"
},
"p_any_ip_addresses": [
"5.5.5.5"
],
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"responseStatus": {
"code": 403
},
"sourceIPs": [
"5.5.5.5"
],
"stage": "ResponseComplete",
"user": {
"username": "system:serviceaccount:kube-system:coredns"
},
"userAgent": "Go-http-client/2.0",
"verb": "watch"
}
Kubernetes Client Certificate Credential Created
#Detects the creation of client certificate signing requests (CSRs) for Kubernetes API authentication. Attackers with appropriate RBAC permissions can create and approve client certificates to establish persistent access to the cluster. Client certificates provide long-term authentication that bypasses service account token expiration and can be harder to revoke. This technique is documented by Stratus Red Team as a persistence mechanism.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-certificatesigningrequests: create certificatesigningrequests |
Rules detecting the same action
These rules filter on the same operation.
- Azure AKS Certificate Signing Request Created or Approved (Elastic)
- GKE Certificate Signing Request API Client Signer Requested (Elastic)
- GKE Certificate Signing Request for Privileged Identity (Elastic)
- GKE Certificate Signing Request Self-Approved (Elastic)
- GKE Client Certificate Signing Request Created or Approved (Elastic)
- Kubernetes Client Certificate Signing Request Created or Approved (Elastic)
Detection logic
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 CertificateSigningRequest creation
if verb != "create" or resource != "certificatesigningrequests":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system principals
if is_system_principal(username):
return False
# Exclude node bootstrap processes that legitimately create CSRs during cluster operations
if username == "kubelet-nodepool-bootstrap":
return False
# Check if this is for client authentication
request_object = event.udm("requestObject") or {}
spec = request_object.get("spec", {})
usages = spec.get("usages", [])
signer_name = spec.get("signerName", "")
# Look for client auth certificates
if "client auth" in usages or "kubernetes.io/kube-apiserver-client" in signer_name:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
name = event.udm("name") or "<UNKNOWN_CSR>"
return f"[{username}] created client certificate signing request [{name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
name = event.udm("name") or "<UNKNOWN_CSR>"
return f"k8s_client_cert_{username}_{name}"
def alert_context(event):
request_object = event.udm("requestObject") or {}
spec = request_object.get("spec", {})
return k8s_alert_context(
event,
extra_fields={
"csr_name": event.udm("name"),
"signer_name": spec.get("signerName"),
"usages": spec.get("usages"),
"groups": spec.get("groups"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.ClientCertificate.Created"
DisplayName: "Kubernetes Client Certificate Credential Created"
Enabled: true
Filename: k8s_client_certificate_created.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Persistence
- Credential Access
- Unified Detection
Severity: Info
Description: >
Detects the creation of client certificate signing requests (CSRs) for Kubernetes API authentication. Attackers with
appropriate RBAC permissions can create and approve client certificates to establish persistent access to the cluster.
Client certificates provide long-term authentication that bypasses service account token expiration and can be harder
to revoke. This technique is documented by Stratus Red Team as a persistence mechanism.
Runbook: |
1. Immediately investigate the user creating the certificate and verify if they are authorized to create client auth credentials
2. Check if the CSR was approved and review who approved it within the last 10 minutes by searching audit logs for certificate approval events
3. Search for API activity using the newly created certificate in the past 24 hours to identify any unauthorized access
Reports:
Stratus Red Team:
- k8s.persistence.create-client-certificate
MITRE ATT&CK:
- TA0003:T1098 # Persistence: Account Manipulation
- TA0006:T1552 # Credential Access: Unsecured Credentials
Reference: https://stratus-red-team.cloud/attack-techniques/kubernetes/k8s.persistence.create-client-certificate/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceiscertificatesigningrequestsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
usernameis notkubelet-nodepool-bootstrapany of:
requestObject.spec.usagescontainsclient authrequestObject.spec.signerNamecontainskubernetes.io/kube-apiserver-client
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | certificatesigningrequests | excludes:resource field:"resource" value:"certificatesigningrequests" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
requestObject.spec.signerName | contains |
| field:"requestObject.spec.signerName" kind:contains value:"kubernetes.io/kube-apiserver-client" |
requestObject.spec.usages | contains |
| field:"requestObject.spec.usages" kind:contains value:"client auth" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
username | ne |
| field:"username" kind:ne value:"kubelet-nodepool-bootstrap" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Immediately investigate the user creating the certificate and verify if they are authorized to create client auth credentials
2. Check if the CSR was approved and review who approved it within the last 10 minutes by searching audit logs for certificate approval events
3. Search for API activity using the newly created certificate in the past 24 hours to identify any unauthorized access
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "certificates.k8s.io/v1",
"name": "malicious-csr",
"resource": "certificatesigningrequests"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"apiVersion": "certificates.k8s.io/v1",
"kind": "CertificateSigningRequest",
"metadata": {
"name": "malicious-csr"
},
"spec": {
"groups": [
"system:authenticated"
],
"request": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0=",
"signerName": "kubernetes.io/kube-apiserver-client",
"usages": [
"client auth"
]
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes ClusterRoleBinding to Privileged Role
#This detection monitors for ClusterRoleBindings being created that grant privileged cluster roles like cluster-admin or system:masters. Attackers who gain initial cluster access often create ClusterRoleBindings to escalate privileges and gain full control over all cluster resources and namespaces. While some bindings to privileged roles are legitimate for cluster operators, unexpected bindings should be investigated immediately as they may indicate compromise or insider threat.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-clusterrolebindings: create clusterrolebindings |
Rules detecting the same action
These rules filter on the same operation.
- Attach to cluster-admin Role (Falco)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Cluster-Admin Role Binding Created or Modified (Elastic)
- GKE Creation of a RoleBinding Referencing a ServiceAccount (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- Google Cloud Kubernetes RoleBinding (Sigma)
- K8s ClusterRoleBinding Created (Falco)
- Kubernetes Cluster-Admin Role Binding Created (Elastic)
Detection logic
from panther_base_helpers import deep_get
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
PRIVILEGED_CLUSTER_ROLES = {
"cluster-admin", # Full cluster control
"system:masters", # Superuser group
"admin", # Namespace admin capabilities
"system:kube-controller-manager", # Controller manager privileges
"system:kube-scheduler", # Scheduler privileges
}
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check ClusterRoleBinding creation events
if verb != "create" or resource != "clusterrolebindings":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system principals to reduce false positives from legitimate operators
if is_system_principal(username):
return False
# Check if binding references a privileged cluster role
request_object = event.udm("requestObject") or {}
role_name = deep_get(request_object, "roleRef", "name", default="")
if role_name in PRIVILEGED_CLUSTER_ROLES:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
name = event.udm("name") or "<UNKNOWN_BINDING>"
request_object = event.udm("requestObject") or {}
role_ref = request_object.get("roleRef", {})
role_name = role_ref.get("name", "<UNKNOWN_ROLE>")
# Extract subject information
subjects = request_object.get("subjects", [])
subject_names = []
for subject in subjects:
subject_type = subject.get("kind", "")
subject_name = subject.get("name", "")
if subject_type and subject_name:
subject_names.append(f"{subject_type}:{subject_name}")
subjects_str = ", ".join(subject_names) if subject_names else "<UNKNOWN_SUBJECTS>"
return (
f"[{username}] created ClusterRoleBinding [{name}] granting [{role_name}] "
f"to [{subjects_str}]"
)
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
name = event.udm("name") or "<UNKNOWN_BINDING>"
return f"k8s_clusterrolebinding_{username}_{name}"
def severity(event):
"""Increase severity for cluster-admin and system:masters roles."""
request_object = event.udm("requestObject") or {}
role_ref = request_object.get("roleRef", {})
role_name = role_ref.get("name", "")
# High for cluster-admin and system:masters (full cluster control)
if role_name in {"cluster-admin", "system:masters"}:
return "HIGH"
# Medium for other privileged system roles
return "DEFAULT"
def alert_context(event):
request_object = event.udm("requestObject") or {}
role_ref = request_object.get("roleRef", {})
subjects = request_object.get("subjects", [])
return k8s_alert_context(
event,
extra_fields={
"binding_name": event.udm("name"),
"role_name": role_ref.get("name"),
"role_kind": role_ref.get("kind"),
"subjects": subjects,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.ClusterRoleBinding.Privileged"
DisplayName: "Kubernetes ClusterRoleBinding to Privileged Role"
Enabled: true
Filename: k8s_clusterrolebinding_privileged.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Privilege Escalation
- Persistence
- RBAC
- Unified Detection
Severity: Medium
Description: >
This detection monitors for ClusterRoleBindings being created that grant privileged cluster
roles like cluster-admin or system:masters. Attackers who gain initial cluster access often create
ClusterRoleBindings to escalate privileges and gain full control over all cluster resources and namespaces.
While some bindings to privileged roles are legitimate for cluster operators, unexpected bindings should be
investigated immediately as they may indicate compromise or insider threat.
Runbook: |
1. Review the subjects being granted the privileged role and immediately delete the ClusterRoleBinding if unauthorized
2. Identify all API operations by the creating user in the 2 hours before and after the alert and audit all actions by the subjects that were granted privileges
3. Review all ClusterRoleBindings across all clusters and search for other RBAC changes in the past 7 days
Reports:
Stratus Red Team:
- k8s.persistence.create-admin-clusterrole
- k8s.privilege-escalation.create-admin-clusterrole
MITRE ATT&CK:
- TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
- TA0003:T1098 # Persistence: Account Manipulation
Reference: >
- https://kubernetes.io/docs/reference/access-authn-authz/rbac/
- https://seifrajhi.github.io/blog/kubernetes-rbac-privilege-escalation-mitigation/#%EF%B8%8F-rolebinding-permissions-in-kubernetes-implications-and-safeguards
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceisclusterrolebindingsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
requestObject.roleRef.nameis one ofcluster-admin,system:masters,admin,system:kube-controller-manager,system:kube-scheduler
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | clusterrolebindings | excludes:resource field:"resource" value:"clusterrolebindings" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
requestObject.roleRef.name | in |
| field:"requestObject.roleRef.name" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name | |
name | requestObject.roleRef.name |
Response runbook
1. Review the subjects being granted the privileged role and immediately delete the ClusterRoleBinding if unauthorized
2. Identify all API operations by the creating user in the 2 hours before and after the alert and audit all actions by the subjects that were granted privileges
3. Review all ClusterRoleBindings across all clusters and search for other RBAC changes in the past 7 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "backdoor-admin",
"resource": "clusterrolebindings"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRoleBinding",
"metadata": {
"name": "backdoor-admin"
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "ClusterRole",
"name": "cluster-admin"
},
"subjects": [
{
"apiGroup": "rbac.authorization.k8s.io",
"kind": "User",
"name": "attacker@example.com"
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes CronJob Created or Modified
#This detection monitors for creation or modification of CronJobs in Kubernetes clusters. Attackers may create or modify scheduled jobs to achieve cluster persistence, execute malicious code on a schedule, or maintain backdoor access to compromised clusters. This detection works across AWS EKS, Azure AKS, and GCP GKE clusters.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Unusual Sensitive Workload Modification (Elastic)
- Kubernetes Cron Job Created or Modified (Panther)
- Kubernetes Cron Job Created or Modified (Panther)
- Kubernetes Cron Job Creation (Splunk)
- Kubernetes CronJob/Job Modification (Sigma)
- Kubernetes Sensitive RBAC Change Followed by Workload Modification (Elastic)
Detection logic
from panther_kubernetes_helpers import (
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
subresource = event.udm("subresource")
response_status = event.udm("responseStatus")
namespace = event.udm("namespace")
username = event.udm("username")
# Check for CronJob create/update/patch operations
if verb in ("create", "update", "patch") and resource == "cronjobs":
# Only alert on successful operations
if is_failed_request(response_status):
return False
# Exclude status updates (routine execution tracking, not spec changes)
if subresource == "status":
return False
# Exclude system controllers creating/modifying CronJobs in system namespaces
if is_system_namespace(namespace) and is_system_principal(username):
return False
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
cronjob_name = event.udm("name") or "<UNKNOWN>"
verb = event.udm("verb")
action = "created" if verb == "create" else "modified"
return f"[{username}] {action} CronJob " f"[{namespace}/{cronjob_name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_cronjob_{username}_{namespace}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"cronjob_name": event.udm("name"),
"requestObject": event.udm("requestObject"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.CronJob.CreatedOrModified"
DisplayName: "Kubernetes CronJob Created or Modified"
Enabled: true
Status: Experimental
Filename: k8s_cronjob_created_or_modified.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Persistence
- Scheduled Task
- Unified Detection
Severity: Info
Description: >
This detection monitors for creation or modification of CronJobs in Kubernetes clusters.
Attackers may create or modify scheduled jobs to achieve cluster persistence, execute
malicious code on a schedule, or maintain backdoor access to compromised clusters.
This detection works across AWS EKS, Azure AKS, and GCP GKE clusters.
Runbook: |
1. Query all CronJob creation and modification events by username in the 24 hours before the alert to establish deployment patterns
2. Analyze the CronJob schedule specification and container image from requestObject to identify suspicious commands or known malicious patterns
3. Search for other CronJobs created by this user in the past 30 days to determine if this represents established activity or anomalous behavior
Reference: https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/
Reports:
MITRE ATT&CK:
- TA0003:T1053.003 # Persistence: Scheduled Task/Job - Cron
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbis one ofcreate,update,patchresourceiscronjobsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
subresourceis notstatusany of:
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-publicusernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | eq |
| field:"resource" kind:eq value:"cronjobs" |
subresource | ne |
| field:"subresource" kind:ne value:"status" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | in |
| field:"verb" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Query all CronJob creation and modification events by username in the 24 hours before the alert to establish deployment patterns
2. Analyze the CronJob schedule specification and container image from requestObject to identify suspicious commands or known malicious patterns
3. Search for other CronJobs created by this user in the past 30 days to determine if this represents established activity or anomalous behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "batch/v1",
"name": "backup-job",
"namespace": "default",
"resource": "cronjobs"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "CronJob",
"metadata": {
"name": "backup-job",
"namespace": "default"
},
"spec": {
"jobTemplate": {
"spec": {
"template": {
"spec": {
"containers": [
{
"image": "backup:latest",
"name": "backup"
}
]
}
}
}
},
"schedule": "0 2 * * *"
}
},
"requestURI": "/apis/batch/v1/namespaces/default/cronjobs",
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes DaemonSet Created
#This detection monitors for creation of DaemonSets in Kubernetes clusters. DaemonSets ensure that a copy of a pod runs on all (or selected) nodes in the cluster. Attackers may abuse DaemonSets to deploy malicious containers across all nodes for cluster-wide persistence, credential harvesting, cryptomining, or lateral movement. This detection works across AWS EKS, Azure AKS, and GCP GKE clusters.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Persistence |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-daemonsets: create daemonsets |
Rules detecting the same action
These rules filter on the same operation.
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Unusual Sensitive Workload Modification (Elastic)
- Kubernetes DaemonSet Deployed (Splunk)
- Kubernetes Sensitive RBAC Change Followed by Workload Modification (Elastic)
- New DaemonSet Deployed to Kubernetes (Panther)
- New DaemonSet Deployed to Kubernetes (Panther)
- Unusual Kubernetes Sensitive Workload Modification (Elastic)
Detection logic
from panther_kubernetes_helpers import (
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
response_status = event.udm("responseStatus")
namespace = event.udm("namespace")
username = event.udm("username")
# Check for DaemonSet create operation
if verb == "create" and resource == "daemonsets":
# Only alert on successful operations
if is_failed_request(response_status):
return False
# Exclude system controllers creating DaemonSets in system namespaces
if is_system_namespace(namespace) and is_system_principal(username):
return False
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
daemonset_name = event.udm("name") or "<UNKNOWN>"
return f"[{username}] created DaemonSet [{namespace}/{daemonset_name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_daemonset_{username}_{namespace}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"daemonset_name": event.udm("name"),
"requestObject": event.udm("requestObject"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.DaemonSet.Created"
DisplayName: "Kubernetes DaemonSet Created"
Enabled: true
Filename: k8s_daemonset_created.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Persistence
- Deploy Container
- Unified Detection
Severity: Info
Description: >
This detection monitors for creation of DaemonSets in Kubernetes clusters.
DaemonSets ensure that a copy of a pod runs on all (or selected) nodes in the cluster.
Attackers may abuse DaemonSets to deploy malicious containers across all nodes for
cluster-wide persistence, credential harvesting, cryptomining, or lateral movement.
This detection works across AWS EKS, Azure AKS, and GCP GKE clusters.
Runbook: |
1. Review all DaemonSet and Deployment creation events by username in the 24 hours before the alert to establish if this is routine activity
2. Analyze the container image, hostPath volumes, and privileged security context from the requestObject specification to identify risk factors
3. Search for similar DaemonSet deployments by this user in the past 90 days and compare images, namespaces, and security settings to identify anomalies
Reference: https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/
Reports:
MITRE ATT&CK:
- TA0002:T1610 # Execution: Deploy Container
- TA0003:T1543 # Persistence: Create or Modify System Process
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceisdaemonsetsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-publicusernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | eq |
| field:"resource" kind:eq value:"daemonsets" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | eq |
| field:"verb" kind:eq value:"create" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review all DaemonSet and Deployment creation events by username in the 24 hours before the alert to establish if this is routine activity
2. Analyze the container image, hostPath volumes, and privileged security context from the requestObject specification to identify risk factors
3. Search for similar DaemonSet deployments by this user in the past 90 days and compare images, namespaces, and security settings to identify anomalies
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "apps/v1",
"name": "monitoring-agent",
"namespace": "kube-system",
"resource": "daemonsets"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "DaemonSet",
"metadata": {
"name": "monitoring-agent",
"namespace": "kube-system"
},
"spec": {
"selector": {
"matchLabels": {
"app": "monitoring"
}
},
"template": {
"metadata": {
"labels": {
"app": "monitoring"
}
},
"spec": {
"containers": [
{
"image": "monitoring-agent:latest",
"name": "agent",
"volumeMounts": [
{
"mountPath": "/var/run/docker.sock",
"name": "docker-sock"
}
]
}
],
"volumes": [
{
"hostPath": {
"path": "/var/run/docker.sock"
},
"name": "docker-sock"
}
]
}
}
}
},
"requestURI": "/apis/apps/v1/namespaces/kube-system/daemonsets",
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Data Copy via kubectl cp
#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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Collection |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | get-pods-exec: get pods/exec |
| Kubernetes | create-pods-exec: create pods/exec |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
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"),
},
)
Rule specification
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
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbis one ofcreate,getresourceispodssubresourceisexecany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | in |
| field:"verb" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response 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
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "webapp-pod",
"namespace": "production",
"resource": "pods",
"subresource": "exec"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": null,
"requestURI": "/api/v1/namespaces/production/pods/webapp-pod/exec?command=tar&command=cf&command=-&command=/app/secrets/credentials.json&container=webapp&stdout=true",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Exec Into Pod
#Detects when users exec into pods across Kubernetes clusters. Execing into pods should be monitored as it can be used for unauthorized access, privilege escalation, or persistent access to workloads. This detection is disabled by default and should be configured with inline filters in the Panther UI to exclude legitimate use cases (e.g., specific service accounts, namespaces, or authorized users).
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | get-pods-exec: get pods/exec |
| Kubernetes | create-pods-exec: create pods/exec |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import k8s_alert_context
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
subresource = event.udm("subresource")
# Check for exec action on pods
if verb in ("create", "get") and resource == "pods" and subresource == "exec":
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
pod_name = event.udm("name") or "<UNKNOWN>"
return f"[{username}] executed into pod [{namespace}/{pod_name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
pod_name = event.udm("name") or "<UNKNOWN_POD>"
return f"k8s_exec_{username}_{namespace}_{pod_name}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={"pod_name": event.udm("name")},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Exec"
DisplayName: "Kubernetes Exec Into Pod"
Enabled: false
Status: Experimental
Filename: k8s_exec_into_pod.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Configuration Required
- Unified Detection
Severity: Medium
Description: >
Detects when users exec into pods across Kubernetes clusters. Execing into pods should be
monitored as it can be used for unauthorized access, privilege escalation, or persistent
access to workloads. This detection is disabled by default and should be configured with
inline filters in the Panther UI to exclude legitimate use cases (e.g., specific service
accounts, namespaces, or authorized users).
Runbook: |
1. Query all exec operations by the username in the 24 hours before and after the alert to identify scope of pod access
2. Review the namespace and pod name from the alert context to determine if accessing sensitive workloads or data
3. Cross-reference the username against authorized administrator or DevOps groups in the past 30 days to verify authorization
Reference: https://kubernetes.io/docs/tasks/debug/debug-application/get-shell-running-container/
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbis one ofcreate,getresourceispodssubresourceisexec
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | eq |
| field:"resource" kind:eq value:"pods" |
subresource | eq |
| field:"subresource" kind:eq value:"exec" |
verb | in |
| field:"verb" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Query all exec operations by the username in the 24 hours before and after the alert to identify scope of pod access
2. Review the namespace and pod name from the alert context to determine if accessing sensitive workloads or data
3. Cross-reference the username against authorized administrator or DevOps groups in the past 30 days to verify authorization
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "nginx-pod",
"namespace": "default",
"resource": "pods",
"subresource": "exec"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestURI": "/api/v1/namespaces/default/pods/nginx-pod/exec",
"responseStatus": {
"code": 101
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"groups": [
"system:authenticated"
],
"username": "john.doe@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Ingress Created Without TLS
#This detection monitors for Ingress objects being created without TLS certificates configured. Ingresses without TLS expose services over unencrypted HTTP, allowing sensitive data like passwords, tokens, and PII to be transmitted in cleartext. This violates security best practices and compliance requirements like PCI-DSS and HIPAA, and enables man-in-the-middle attacks.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-ingresses: create ingresses |
Rules detecting the same action
These rules filter on the same operation.
Detection logic
from panther_base_helpers import deep_get
from panther_kubernetes_helpers import (
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check ingress creation events
if verb != "create" or resource != "ingresses":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system namespaces and system principals to reduce false positives
if is_system_namespace(namespace) or is_system_principal(username):
return False
# Check if ingress has TLS configuration
tls = deep_get(event.udm("requestObject"), "spec", "tls")
# Alert if TLS is not configured (missing or empty)
if not tls:
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_INGRESS>"
return f"[{username}] created Ingress [{namespace}/{name}] without TLS certificate"
def dedup(event):
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = event.udm("name") or "<UNKNOWN_INGRESS>"
return f"k8s_ingress_no_tls_{namespace}_{name}"
def severity(event):
"""Increase severity based on ingress annotations and rules."""
request_object = event.udm("requestObject") or {}
metadata = request_object.get("metadata", {})
annotations = metadata.get("annotations", {})
# Check if this is an external-facing ingress (has external annotations)
external_annotations = [
"kubernetes.io/ingress.class",
"cert-manager.io/cluster-issuer",
"external-dns.alpha.kubernetes.io/hostname",
]
if any(key in annotations for key in external_annotations):
return "MEDIUM"
return "DEFAULT"
def alert_context(event):
request_object = event.udm("requestObject") or {}
spec = request_object.get("spec", {})
rules = spec.get("rules", [])
metadata = request_object.get("metadata", {})
annotations = metadata.get("annotations", {})
# Extract hosts from ingress rules
hosts = []
for rule_entry in rules:
host = rule_entry.get("host")
if host:
hosts.append(host)
return k8s_alert_context(
event,
extra_fields={
"ingress_name": event.udm("name"),
"ingress_hosts": hosts,
"annotations": annotations,
"has_tls": False,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Ingress.NoTLS"
DisplayName: "Kubernetes Ingress Created Without TLS"
Enabled: true
Filename: k8s_ingress_without_tls.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Network Security
- Encryption
- Compliance
- Unified Detection
Severity: Medium
Description: >
This detection monitors for Ingress objects being created without TLS certificates configured. Ingresses
without TLS expose services over unencrypted HTTP, allowing sensitive data like passwords, tokens, and PII
to be transmitted in cleartext. This violates security best practices and compliance requirements like
PCI-DSS and HIPAA, and enables man-in-the-middle attacks.
Runbook: |
1. Review the ingress specification to determine if this is an internal-only service or if TLS is terminated at an external load balancer
2. If TLS is required, work with the team to configure TLS certificates using cert-manager or manual certificate creation
3. Search for other ingresses without TLS in the past 7 days to identify if this is a systemic configuration issue
Reports:
MITRE ATT&CK:
- TA0009:T1040 # Collection: Network Sniffing
- TA0006:T1552.004 # Credential Access: Unsecured Credentials - Private Keys
Reference: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceisingressesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
requestObject.spec.tlsis empty
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | ingresses | excludes:resource field:"resource" value:"ingresses" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
requestObject.spec.tls | is_null | field:"requestObject.spec.tls" kind:is_null | |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review the ingress specification to determine if this is an internal-only service or if TLS is terminated at an external load balancer
2. If TLS is required, work with the team to configure TLS certificates using cert-manager or manual certificate creation
3. Search for other ingresses without TLS in the past 7 days to identify if this is a systemic configuration issue
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "networking.k8s.io",
"apiVersion": "v1",
"name": "api-ingress",
"namespace": "production",
"resource": "ingresses"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "Ingress",
"metadata": {
"annotations": {
"kubernetes.io/ingress.class": "nginx"
},
"name": "api-ingress",
"namespace": "production"
},
"spec": {
"rules": [
{
"host": "api.example.com",
"http": {
"paths": [
{
"backend": {
"serviceName": "api-service",
"servicePort": 80
},
"path": "/"
}
]
}
}
]
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "developer@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Long-Lived Service Account Token Created
#Detects the creation of long-lived service account tokens via the serviceaccounts/token subresource. Kubernetes 1.24+ deprecated automatic token creation, but users with appropriate permissions can still manually create non-expiring tokens for service accounts. Attackers can abuse this to establish persistent access credentials that don't expire automatically. This technique is documented by Stratus Red Team as a persistence mechanism. Note: GCP GKE does not log TokenRequest API operations in Kubernetes audit logs.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-serviceaccounts-token: create serviceaccounts/token |
Rules detecting the same action
These rules filter on the same operation.
- Azure AKS Service Account Token Created via TokenRequest API (Elastic)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Service Account Token Created via TokenRequest API (Elastic)
- K8s Serviceaccount Created (Falco)
- Kubernetes Service Account Token Created via TokenRequest API (Elastic)
- New Kubernetes Service Account Created (Sigma)
- Service Account Created in Kube Namespace (Falco)
Detection logic
from panther_kubernetes_helpers import (
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
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 serviceaccount token subresource creation
if verb != "create" or resource != "serviceaccounts" or subresource != "token":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system principals in system namespaces (legitimate operations)
# but alert on system principals in user namespaces (malicious controllers)
# and alert on users creating tokens in system namespaces (privilege escalation)
if is_system_principal(username) and is_system_namespace(namespace):
return False
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = event.udm("name") or "<UNKNOWN_SA>"
return f"[{username}] created long-lived token for service account " 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_SA>"
return f"k8s_token_created_{username}_{namespace}_{name}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"service_account": event.udm("name"),
"namespace": event.udm("namespace"),
"subresource": "token",
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.ServiceAccount.TokenCreated"
DisplayName: "Kubernetes Long-Lived Service Account Token Created"
Enabled: true
Status: Experimental
Filename: k8s_serviceaccount_token_created.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
Tags:
- Kubernetes
- Persistence
- Credential Access
Severity: Info
Description: >
Detects the creation of long-lived service account tokens via the serviceaccounts/token subresource. Kubernetes 1.24+
deprecated automatic token creation, but users with appropriate permissions can still manually create non-expiring tokens
for service accounts. Attackers can abuse this to establish persistent access credentials that don't expire automatically.
This technique is documented by Stratus Red Team as a persistence mechanism.
Note: GCP GKE does not log TokenRequest API operations in Kubernetes audit logs.
Runbook: |
1. Immediately investigate the user creating the token and determine if they have a legitimate need for long-lived credentials
2. Review the service account permissions to assess what access the token provides by checking RoleBindings and ClusterRoleBindings
3. Search for API activity using this service account in the past 24 hours to identify any unauthorized operations
Reports:
Stratus Red Team:
- k8s.persistence.create-token
MITRE ATT&CK:
- TA0003:T1098 # Persistence: Account Manipulation
- TA0006:T1552 # Credential Access: Unsecured Credentials
Reference: https://stratus-red-team.cloud/attack-techniques/kubernetes/k8s.persistence.create-token/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity events when all of the conditions below hold.
Condition
verbiscreateresourceisserviceaccountssubresourceistokenany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Immediately investigate the user creating the token and determine if they have a legitimate need for long-lived credentials
2. Review the service account permissions to assess what access the token provides by checking RoleBindings and ClusterRoleBindings
3. Search for API activity using this service account in the past 24 hours to identify any unauthorized operations
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "persistence-sa",
"namespace": "production",
"resource": "serviceaccounts",
"subresource": "token"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes NodePort Service Deployed
#This detection monitors for any Kubernetes service deployed with type NodePort. A NodePort service allows an attacker to expose a set of pods hosting the service to the internet by opening their port and redirecting traffic here. This can be used to bypass network controls and intercept traffic, creating a direct line to the outside network.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-services: create services |
Rules detecting the same action
These rules filter on the same operation.
- Create NodePort Service (Falco)
- GCP K8S Service Type NodePort Deployed (Panther)
- GKE Exposed Service Created With Type NodePort (Elastic)
- K8s Service Created (Falco)
- Kubernetes Exposed Service Created With Type NodePort (Elastic)
- Kubernetes Node Port Creation (Splunk)
- Kubernetes Service with Type Node Port Deployed (Panther)
- Kubernetes Service with Type Node Port Deployed (Panther)
Detection logic
from panther_kubernetes_helpers import is_failed_request, k8s_alert_context
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
response_status = event.udm("responseStatus")
# Only check service creation events
if verb != "create" or resource != "services":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Check if service type is NodePort
service_type = event.udm("serviceType") or ""
if service_type == "NodePort":
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>"
return f"[{username}] deployed NodePort service [{namespace}/{name}]"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"service_name": event.udm("name"),
"service_type": event.udm("serviceType"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Service.NodePort.Deployed"
DisplayName: "Kubernetes NodePort Service Deployed"
Enabled: true
Filename: k8s_service_nodeport.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Severity: High
Description: >
This detection monitors for any Kubernetes service deployed with type NodePort. A NodePort
service allows an attacker to expose a set of pods hosting the service to the internet by
opening their port and redirecting traffic here. This can be used to bypass network controls
and intercept traffic, creating a direct line to the outside network.
Runbook: |
1. Identify all services created by the username in the 48 hours before the alert to understand deployment patterns
2. Check the NodePort service specification to determine if it exposes cluster-critical services or standard application workloads
3. Review exposed ports and compare against documented internal services in the past 30 days to verify if this is authorized
Reference: https://kubernetes.io/docs/tutorials/kubernetes-basics/expose/expose-intro/
Tags:
- Kubernetes
- Exploit Public-Facing Application
- Initial Access
- Unified Detection
Reports:
MITRE ATT&CK:
- TA0001:T1190 # Exploit Public-Facing Application
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceisservicesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
serviceTypeisNodePort
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | services | excludes:resource field:"resource" value:"services" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
serviceType | eq |
| field:"serviceType" kind:eq value:"NodePort" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Identify all services created by the username in the 48 hours before the alert to understand deployment patterns
2. Check the NodePort service specification to determine if it exposes cluster-critical services or standard application workloads
3. Review exposed ports and compare against documented internal services in the past 30 days to verify if this is authorized
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "test-ns",
"namespace": "default",
"resource": "services"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"apiVersion": "v1",
"kind": "Service",
"spec": {
"ports": [
{
"port": 5678,
"protocol": "TCP",
"targetPort": 8080
}
],
"type": "NodePort"
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "user@example.com"
},
"userAgent": "kubectl/v1.28.2",
"verb": "create"
}
Kubernetes Pod Attached To Host Network
#This detection monitors for the creation of pods which are attached to the host's network. This allows a pod to listen to all network traffic for all deployed compute on that particular node and communicate with other compute on the network namespace. Attackers can use this to capture secrets passed in arguments or connections.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
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
# Check if hostNetwork is set to true in the request
host_network = event.udm("hostNetwork")
if host_network is True:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"[{username}] created pod [{namespace}/{name}] with host network access "
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_host_network_{username}_{namespace}"
def alert_context(event):
return k8s_alert_context(event, extra_fields=get_pod_context_fields(event))
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Host.Network"
DisplayName: "Kubernetes Pod Attached To Host Network"
Enabled: true
Filename: k8s_pod_host_network.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Escape to Host
- Unified Detection
Severity: Medium
Description:
This detection monitors for the creation of pods which are attached to the host's network.
This allows a pod to listen to all network traffic for all deployed compute on that particular
node and communicate with other compute on the network namespace. Attackers can use this to
capture secrets passed in arguments or connections.
Reports:
MITRE ATT&CK:
- TA0004:T1611 # Escape to Host
Runbook: |
1. Find all pod creation events by the username in the 24 hours before the alert to establish normal deployment behavior
2. Check if the pod namespace indicates system infrastructure purpose (kube-system, kube-public) which may be legitimate
3. Review all pods with hostNetwork by this user in the past 30 days to identify if this is an established pattern or anomalous activity
Reference: >
- https://kubernetes.io/docs/concepts/security/pod-security-standards/#host-namespaces
- https://securitylabs.datadoghq.com/articles/kubernetes-security-fundamentals-part-6/
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
hostNetworkistrue
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
hostNetwork | eq |
| field:"hostNetwork" kind:eq value:"true" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Find all pod creation events by the username in the 24 hours before the alert to establish normal deployment behavior
2. Check if the pod namespace indicates system infrastructure purpose (kube-system, kube-public) which may be legitimate
3. Review all pods with hostNetwork by this user in the past 30 days to identify if this is an established pattern or anomalous activity
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "nginx-test",
"namespace": "default",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "nginx-test",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "nginx",
"name": "nginx"
}
],
"hostNetwork": true
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Pod Created in System Namespace
#This detection monitors for pods being created in system namespaces like kube-system, kube-public, gke-system, or kube-node-lease. These namespaces are reserved for Kubernetes control plane components and cluster infrastructure. Attackers who gain cluster access may create malicious pods in system namespaces to hide among legitimate system workloads, gain elevated privileges, or establish persistence mechanisms that are less likely to be noticed.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
SYSTEM_NAMESPACES,
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_system_principal,
k8s_alert_context,
)
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 pod creation events
if verb != "create" or resource != "pods":
return False
# Only check direct pod creation, not subresources like eviction
if subresource:
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system principals (legitimate operators/controllers)
if is_system_principal(username):
return False
# Alert if pod is created in a system namespace
if namespace in SYSTEM_NAMESPACES:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"[{username}] created pod [{namespace}/{name}] in system namespace"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"k8s_pod_system_ns_{username}_{namespace}_{name}"
def alert_context(event):
return k8s_alert_context(event, extra_fields=get_pod_context_fields(event))
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.SystemNamespace"
DisplayName: "Kubernetes Pod Created in System Namespace"
Enabled: true
Status: Experimental
Filename: k8s_pod_created_in_system_namespace.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Privilege Escalation
- Persistence
- Defense Evasion
- Unified Detection
Severity: Medium
Description: >
This detection monitors for pods being created in system namespaces like kube-system, kube-public, gke-system,
or kube-node-lease. These namespaces are reserved for Kubernetes control plane components and cluster infrastructure.
Attackers who gain cluster access may create malicious pods in system namespaces to hide among legitimate system
workloads, gain elevated privileges, or establish persistence mechanisms that are less likely to be noticed.
Runbook: |
1. Review the pod specification to identify the container images being deployed and determine if this is a legitimate system component or malicious workload
2. Identify all API operations by the creating user in the 2 hours before and after the alert to establish intent
3. If unauthorized, immediately delete the pod and search for other suspicious pod creations or RBAC changes across all clusters in the past 7 days
Reports:
MITRE ATT&CK:
- TA0004:T1611 # Privilege Escalation: Escape to Host
- TA0003:T1525 # Persistence: Implant Internal Image
- TA0005:T1578.002 # Defense Evasion: Modify Cloud Compute Infrastructure
Reference: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodssubresourceis emptyany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis one ofkube-system,gke-system,kube-node-lease,kube-public
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
namespace | in |
| field:"namespace" kind:in |
subresource | is_null | field:"subresource" kind:is_null | |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Review the pod specification to identify the container images being deployed and determine if this is a legitimate system component or malicious workload
2. Identify all API operations by the creating user in the 2 hours before and after the alert to establish intent
3. If unauthorized, immediately delete the pod and search for other suspicious pod creations or RBAC changes across all clusters in the past 7 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "malicious-pod",
"namespace": "kube-system",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "Pod",
"metadata": {
"name": "malicious-pod",
"namespace": "kube-system"
},
"spec": {
"containers": [
{
"image": "attacker/backdoor:latest",
"name": "backdoor"
}
]
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Pod Using Host IPC Namespace
#This detection monitors for pods created with hostIPC set to true, which allows the pod to use the host's IPC namespace. This breaks isolation between the pod and the host system, giving the pod direct access to shared memory segments, semaphores, and message queues on the host. Attackers can abuse this to communicate with or interfere with processes on the host system or other containers using the same IPC namespace.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
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
# Check if hostIPC is enabled
host_ipc = event.udm("hostIPC")
if host_ipc is True:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"[{username}] created pod [{namespace}/{name}] with host IPC enabled "
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_host_ipc_{username}_{namespace}"
def alert_context(event):
return k8s_alert_context(event, extra_fields=get_pod_context_fields(event))
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Host.IPC"
DisplayName: "Kubernetes Pod Using Host IPC Namespace"
Enabled: true
Filename: k8s_pod_host_ipc.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Privilege Escalation
- Container Escape
- Unified Detection
Severity: Medium
Description: >
This detection monitors for pods created with hostIPC set to true, which allows the pod to
use the host's IPC namespace. This breaks isolation between the pod and the host system,
giving the pod direct access to shared memory segments, semaphores, and message queues on
the host. Attackers can abuse this to communicate with or interfere with processes on the
host system or other containers using the same IPC namespace.
Runbook: |
1. Review all pod creation events by the username in the 24 hours before the alert to establish normal deployment patterns
2. Determine if the hostIPC setting is required for legitimate inter-process communication needs for this specific workload
3. Search for other pods with hostIPC enabled deployed by this user across all clusters in the past 30 days
Reports:
MITRE ATT&CK:
- TA0004:T1611 # Privilege Escalation: Escape to Host
- TA0005:T1562 # Defense Evasion: Impair Defenses
Reference: >
- https://kubernetes.io/docs/concepts/security/pod-security-standards/
- https://www.fairwinds.com/blog/kubernetes-basics-tutorial-host-ipc-should-not-be-configured
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
hostIPCistrue
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
hostIPC | eq |
| field:"hostIPC" kind:eq value:"true" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Review all pod creation events by the username in the 24 hours before the alert to establish normal deployment patterns
2. Determine if the hostIPC setting is required for legitimate inter-process communication needs for this specific workload
3. Search for other pods with hostIPC enabled deployed by this user across all clusters in the past 30 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "debug-pod",
"namespace": "default",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "Pod",
"metadata": {
"name": "debug-pod",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "busybox",
"name": "debug"
}
],
"hostIPC": true
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "developer@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Pod Using Host PID Namespace
#This detection monitors for any pod creation or modification using the host PID namespace. The Host PID namespace enables a pod and its containers to have direct access and share the same view as the host's processes. This can offer a powerful escape hatch to the underlying host.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
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
# Check if hostPID is set to true in the request
host_pid = event.udm("hostPID")
if host_pid is True:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"[{username}] created pod [{namespace}/{name}] using host PID namespace"
def alert_context(event):
return k8s_alert_context(event, extra_fields=get_pod_context_fields(event))
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Host.PID"
DisplayName: "Kubernetes Pod Using Host PID Namespace"
Enabled: true
Filename: k8s_pod_host_pid.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Escape to Host
- Deploy Container
- Unified Detection
Severity: Medium
Description:
This detection monitors for any pod creation or modification using the host PID namespace.
The Host PID namespace enables a pod and its containers to have direct access and share the
same view as the host's processes. This can offer a powerful escape hatch to the underlying host.
Runbook: |
1. Query API calls by the username in the 24 hours before and after the alert to understand the deployment context and related activity
2. Check if the pod namespace indicates system or infrastructure purpose (kube-system, kube-monitoring) versus user workloads
3. Search for other host PID pod creations by this user in the past 30 days to determine if this is an established pattern or new behavior
Reports:
MITRE ATT&CK:
- TA0004:T1611 # Escape to Host
- TA0002:T1610 # Deploy Container
Reference: >
- https://kubernetes.io/docs/concepts/security/pod-security-standards/#host-namespaces
- https://medium.com/@chrispisano/limiting-pod-privileges-hostpid-57ce07b05896
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
hostPIDistrue
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
hostPID | eq |
| field:"hostPID" kind:eq value:"true" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Query API calls by the username in the 24 hours before and after the alert to understand the deployment context and related activity
2. Check if the pod namespace indicates system or infrastructure purpose (kube-system, kube-monitoring) versus user workloads
3. Search for other host PID pod creations by this user in the past 30 days to determine if this is an established pattern or new behavior
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "nginx-test",
"namespace": "default",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "Pod",
"spec": {
"containers": [
{
"image": "nginx",
"name": "nginx"
}
],
"hostPID": true
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"verb": "create"
}
Kubernetes Pod with Dangerous Linux Capabilities
#This detection monitors for pods created with dangerous Linux capabilities such as SYS_ADMIN, NET_ADMIN, or BPF. These capabilities can enable privilege escalation, container escape, or unauthorized access to host resources. Attackers often add these capabilities to containers to bypass security restrictions and gain elevated privileges on the underlying host.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_base_helpers import deep_get
from panther_kubernetes_helpers import (
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
# Dangerous Linux capabilities that enable privilege escalation or container escape
DANGEROUS_CAPABILITIES = {
"SYS_ADMIN", # Most powerful
"NET_ADMIN", # Network manipulation
"BPF", # eBPF programs
"SYS_PTRACE", # Process tracing
"SYS_MODULE", # Load kernel modules
"DAC_READ_SEARCH", # Bypass file read permission checks
"DAC_OVERRIDE", # Bypass file permission checks
}
def has_dangerous_capabilities(containers):
"""Check if any container has dangerous Linux capabilities."""
if not containers:
return []
dangerous_caps_found = []
for container in containers:
added_caps = deep_get(container, "securityContext", "capabilities", "add", default=[])
if added_caps:
# Check for intersection with dangerous capabilities
dangerous = set(added_caps) & DANGEROUS_CAPABILITIES
if dangerous:
dangerous_caps_found.extend(list(dangerous))
return dangerous_caps_found
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
return False
# Skip failed requests
if is_failed_request(response_status):
return False
# Exclude system namespaces and system principals to reduce false positives
if is_system_namespace(namespace) or is_system_principal(username):
return False
# Check for dangerous capabilities
containers = event.udm("containers") or []
dangerous_caps = has_dangerous_capabilities(containers)
if dangerous_caps:
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
containers = event.udm("containers") or []
dangerous_caps = has_dangerous_capabilities(containers)
caps_str = ", ".join(sorted(set(dangerous_caps)))
return (
f"[{username}] created pod [{namespace}/{name}] with dangerous capabilities "
f"[{caps_str}]"
)
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_dangerous_caps_{username}_{namespace}"
def alert_context(event):
containers = event.udm("containers") or []
dangerous_caps = has_dangerous_capabilities(containers)
context_fields = get_pod_context_fields(event)
context_fields["dangerous_capabilities"] = sorted(set(dangerous_caps))
return k8s_alert_context(event, extra_fields=context_fields)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Dangerous.Capabilities"
DisplayName: "Kubernetes Pod with Dangerous Linux Capabilities"
Enabled: true
Filename: k8s_pod_dangerous_capabilities.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Security Control
- Privilege Escalation
- Container Escape
- Unified Detection
Severity: High
Description: >
This detection monitors for pods created with dangerous Linux capabilities such as SYS_ADMIN,
NET_ADMIN, or BPF. These capabilities can enable privilege escalation, container escape, or
unauthorized access to host resources. Attackers often add these capabilities to containers
to bypass security restrictions and gain elevated privileges on the underlying host.
Runbook: |
1. Review all pod creation events by the username in the 24 hours before the alert to determine if this is routine deployment activity
2. Analyze the specific capabilities granted and their legitimate business justification for the workload
3. Search for other pods with dangerous capabilities deployed by this user across all clusters in the past 30 days
Reports:
MITRE ATT&CK:
- TA0004:T1611 # Privilege Escalation: Escape to Host
- TA0005:T1068 # Defense Evasion: Exploitation for Privilege Escalation
Reference: >
- https://kubernetes.io/docs/tasks/configure-pod-container/security-context/
- https://www.dynatrace.com/news/blog/kubernetes-security-essentials-container-misconfigurations-from-theory-to-exploitation/
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- namespace
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Review all pod creation events by the username in the 24 hours before the alert to determine if this is routine deployment activity
2. Analyze the specific capabilities granted and their legitimate business justification for the workload
3. Search for other pods with dangerous capabilities deployed by this user across all clusters in the past 30 days
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "test-pod",
"namespace": "default",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "Pod",
"spec": {
"containers": [
{
"image": "nginx",
"name": "test",
"securityContext": {
"capabilities": {
"add": [
"SYS_ADMIN",
"NET_RAW"
]
}
}
}
]
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Pod With HostPath Volume Mount
#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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Exfiltration |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
get_hostpath_paths,
get_pod_context_fields,
get_pod_name,
has_hostpath_volume,
is_failed_request,
is_sensitive_hostpath,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
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
# Check for hostPath volumes
volumes = event.udm("volumes") or []
if has_hostpath_volume(volumes):
return True
return False
def severity(event):
volumes = event.udm("volumes") or []
paths = get_hostpath_paths(volumes)
for path in paths:
if is_sensitive_hostpath(path):
return "HIGH"
return "DEFAULT"
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
volumes = event.udm("volumes") or []
paths = get_hostpath_paths(volumes)
paths_str = ", ".join(paths) if paths else "unknown"
return (
f"[{username}] created pod [{namespace}/{name}] with hostPath volume mount "
f"[{paths_str}]"
)
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"hostpath_volume_{username}_{namespace}"
def alert_context(event):
volumes = event.udm("volumes") or []
pod_context = get_pod_context_fields(event)
return k8s_alert_context(
event,
extra_fields={
**pod_context,
"hostpath_paths": get_hostpath_paths(volumes),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.HostPath.Volume"
DisplayName: "Kubernetes Pod With HostPath Volume Mount"
Enabled: true
Filename: k8s_pod_hostpath_volume.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Severity: Medium
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: |
1. Review all pod creation events by the username in the 24 hours before the alert to establish baseline deployment behavior
2. Check if the hostPath volume is mounted from sensitive paths (/, /var, /sys, /proc, /etc) which pose higher security risk
3. Search for other hostPath volume mounts by this user in the past 30 days and compare the paths being accessed to identify patterns
Reference: >
- https://kubernetes.io/docs/concepts/security/pod-security-standards/#host-namespaces
- https://medium.com/@vincn.ledan/understanding-the-risks-injecting-malicious-pods-via-hostpath-in-kubernetes-due-to-83f54a1bef31
Reports:
Stratus Red Team:
- k8s.privilege-escalation.hostpath-volume
MITRE ATT&CK:
- TA0010:T1041 # Exfiltration Over C2 Channel
- TA0004:T1611 # Escape to Host
DedupPeriodMinutes: 360
Tags:
- Kubernetes
- Security Control
- Privilege Escalation
- Data Exfiltration
- Unified Detection
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
volumesis presentany element of
volumesmatches:volumescontainshostPath
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
volumes | is_not_null | field:"volumes" kind:is_not_null |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Review all pod creation events by the username in the 24 hours before the alert to establish baseline deployment behavior
2. Check if the hostPath volume is mounted from sensitive paths (/, /var, /sys, /proc, /etc) which pose higher security risk
3. Search for other hostPath volume mounts by this user in the past 30 days and compare the paths being accessed to identify patterns
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "test",
"namespace": "default",
"resource": "pods"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "nginx",
"name": "test",
"volumeMounts": [
{
"mountPath": "/test",
"name": "test-volume"
}
]
}
],
"volumes": [
{
"hostPath": {
"path": "/var/lib/kubelet",
"type": "DirectoryOrCreate"
},
"name": "test-volume"
}
]
}
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"user": {
"username": "user@company.com"
},
"userAgent": "kubectl/v1.28.2",
"verb": "create"
}
Kubernetes Privileged Pod Created
#Detects creation of privileged pods across Kubernetes clusters. Privileged pods have full access to the host's namespace and devices, have the ability to exploit the kernel, have dangerous linux capabilities, and can be a powerful launching point for further attacks. In the event of a successful container escape where a user is operating with root privileges, the attacker retains this role on the node.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods: create pods |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
from panther_kubernetes_helpers import (
get_pod_context_fields,
get_pod_name,
is_failed_request,
is_privileged_container,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check pod creation events
if verb != "create" or resource != "pods":
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
# Check request object for privileged containers
containers = event.udm("containers") or []
if is_privileged_container(containers):
return True
return False
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = get_pod_name(event)
return f"[{username}] created a privileged pod [{namespace}/{name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
return f"privileged_pod_{username}"
def alert_context(event):
return k8s_alert_context(event, extra_fields=get_pod_context_fields(event))
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Pod.Privileged.Created"
DisplayName: "Kubernetes Privileged Pod Created"
Enabled: true
Filename: k8s_privileged_pod_created.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Severity: High
Description: >
Detects creation of privileged pods across Kubernetes clusters. Privileged pods have full access
to the host's namespace and devices, have the ability to exploit the kernel, have dangerous linux
capabilities, and can be a powerful launching point for further attacks. In the event of a
successful container escape where a user is operating with root privileges, the attacker retains
this role on the node.
Runbook: |
1. Check if the username who created the privileged pod has a history of deploying system infrastructure in the past 90 days
2. Review all API calls by this username in the 6 hours before the alert to understand context and related activity
3. Query for other privileged pods created by the same user in the past 30 days to identify if this is an established pattern
Reference: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
Reports:
Stratus Red Team:
- k8s.privilege-escalation.privileged-pod
MITRE ATT&CK:
- TA0004:T1548.003 # Abuse Elevation Control Mechanism: Sudo and Sudo Caching
DedupPeriodMinutes: 360
Tags:
- Kubernetes
- Security Control
- Privilege Escalation
- Unified Detection
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodsany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
containersis present
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
containers | is_not_null | field:"containers" kind:is_not_null | |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Check if the username who created the privileged pod has a history of deploying system infrastructure in the past 90 days
2. Review all API calls by this username in the 6 hours before the alert to understand context and related activity
3. Query for other privileged pods created by the same user in the past 30 days to identify if this is an established pattern
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"auditID": "abc-123",
"kind": "Event",
"level": "RequestResponse",
"objectRef": {
"apiVersion": "v1",
"name": "test-privileged-pod",
"namespace": "default",
"resource": "pods"
},
"p_event_time": "2024-02-13 12:45:06.073",
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test-privileged-pod",
"namespace": "default"
},
"spec": {
"containers": [
{
"image": "nginx",
"name": "nginx",
"securityContext": {
"privileged": true
}
}
]
}
},
"requestURI": "/api/v1/namespaces/default/pods",
"responseStatus": {
"code": 201
},
"sourceIPs": [
"1.2.3.4"
],
"stage": "ResponseComplete",
"user": {
"groups": [
"system:authenticated"
],
"username": "john.doe@company.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Role With Node Proxy Permissions Created
#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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Stealth | |
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-clusterroles: create clusterroles |
| Kubernetes | create-roles: create roles |
Rules detecting the same action
These rules filter on the same operation.
- ClusterRole With Pod Exec Created (Falco)
- ClusterRole With Wildcard Created (Falco)
- ClusterRole With Write Privileges Created (Falco)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Creation or Modification of Sensitive Role (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- K8s ClusterRole Created (Falco)
Detection logic
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,
},
)
Rule specification
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
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceis one ofroles,clusterrolesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | in | clusterroles, roles | excludes:resource field:"resource" value:"clusterroles" field:"resource" value:"roles" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response 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
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "node-accessor",
"resource": "clusterroles"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRole",
"metadata": {
"name": "node-accessor"
},
"rules": [
{
"apiGroups": [
""
],
"resources": [
"nodes/proxy"
],
"verbs": [
"get",
"list",
"create"
]
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Role With Pod Exec Permissions Created
#This detection monitors for Roles or ClusterRoles being created that grant permissions to exec into pods. The pods/exec subresource allows executing arbitrary commands inside containers, which can be abused for lateral movement, credential theft, or container escape. Attackers who gain RBAC modification permissions often create roles with pods/exec to establish backdoor access for executing commands across the cluster.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Lateral Movement |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-clusterroles: create clusterroles |
| Kubernetes | create-roles: create roles |
Rules detecting the same action
These rules filter on the same operation.
- ClusterRole With Pod Exec Created (Falco)
- ClusterRole With Wildcard Created (Falco)
- ClusterRole With Write Privileges Created (Falco)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Creation or Modification of Sensitive Role (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- K8s ClusterRole Created (Falco)
Detection logic
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 from legitimate operators
if is_system_principal(username):
return False
# Check if role grants pods/exec permissions
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
for rule_entry in rules:
resources = rule_entry.get("resources") or []
# Check for pods/exec subresource
if "pods/exec" 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>"
namespace = event.udm("namespace") or "<CLUSTER_SCOPED>"
role_type = "ClusterRole" if resource == "clusterroles" else "Role"
if namespace != "<CLUSTER_SCOPED>":
return f"[{username}] created {role_type} [{namespace}/{name}] with pods/exec permissions"
return f"[{username}] created {role_type} [{name}] with pods/exec permissions "
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
name = event.udm("name") or "<UNKNOWN_ROLE>"
return f"k8s_role_podexec_{username}_{resource}_{name}"
def severity(event):
"""ClusterRoles are more dangerous than namespaced Roles."""
resource = event.udm("resource") or ""
# Critical for ClusterRole (cluster-wide exec permissions)
if resource == "clusterroles":
return "CRITICAL"
# High for namespaced Role (namespace-scoped exec permissions)
return "HIGH"
def alert_context(event):
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
# Extract only the rules that grant pods/exec
exec_rules = []
for rule_entry in rules:
if "pods/exec" in (rule_entry.get("resources") or []):
exec_rules.append(rule_entry)
return k8s_alert_context(
event,
extra_fields={
"role_name": event.udm("name"),
"role_type": event.udm("resource"),
"exec_rules": exec_rules,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Role.PodExec"
DisplayName: "Kubernetes Role With Pod Exec Permissions Created"
Enabled: true
Filename: k8s_role_pod_exec.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Privilege Escalation
- Lateral Movement
- RBAC
- Unified Detection
Severity: High
Description: >
This detection monitors for Roles or ClusterRoles being created that grant permissions to exec into pods.
The pods/exec subresource allows executing arbitrary commands inside containers, which can be abused for
lateral movement, credential theft, or container escape. Attackers who gain RBAC modification permissions
often create roles with pods/exec to establish backdoor access for executing commands across the cluster.
Runbook: |
1. Review the role rules to identify what verbs and API groups were granted alongside pods/exec and determine if this role creation is expected
2. Identify all API operations by the creating user in the 2 hours before and after the alert and search for any RoleBindings or ClusterRoleBindings that reference this role
3. Search for all exec operations across the cluster in the past 24 hours to identify if this permission has already been abused
Reports:
MITRE ATT&CK:
- TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
- TA0008:T1021 # Lateral Movement: Remote Services
Reference: >
- https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources
- https://seifrajhi.github.io/blog/kubernetes-rbac-privilege-escalation-mitigation/#%EF%B8%8F-rolebinding-permissions-in-kubernetes-implications-and-safeguards
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- resource
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceis one ofroles,clusterrolesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
any element of
requestObject.rulesmatches:requestObject.rules.resourcescontainspods/exec
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | in | clusterroles, roles | excludes:resource field:"resource" value:"clusterroles" field:"resource" value:"roles" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review the role rules to identify what verbs and API groups were granted alongside pods/exec and determine if this role creation is expected
2. Identify all API operations by the creating user in the 2 hours before and after the alert and search for any RoleBindings or ClusterRoleBindings that reference this role
3. Search for all exec operations across the cluster in the past 24 hours to identify if this permission has already been abused
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "pod-exec-role",
"resource": "clusterroles"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRole",
"metadata": {
"name": "pod-exec-role"
},
"rules": [
{
"apiGroups": [
""
],
"resources": [
"pods/exec"
],
"verbs": [
"create",
"get"
]
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Role With Wildcard Permissions Created
#This detection monitors for Roles or ClusterRoles being created with wildcard (*) permissions in resources or verbs. Wildcard permissions grant overly broad access, such as all operations on all resources, which violates the principle of least privilege. Attackers who gain RBAC modification permissions often create wildcard roles to maximize their access across the cluster without knowing specific resource names or API operations.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Stealth |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-clusterroles: create clusterroles |
| Kubernetes | create-roles: create roles |
Rules detecting the same action
These rules filter on the same operation.
- ClusterRole With Pod Exec Created (Falco)
- ClusterRole With Wildcard Created (Falco)
- ClusterRole With Write Privileges Created (Falco)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Creation or Modification of Sensitive Role (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- K8s ClusterRole Created (Falco)
Detection logic
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 from legitimate operators
if is_system_principal(username):
return False
# Check if role grants wildcard permissions
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
for rule_entry in rules:
resources = rule_entry.get("resources") or []
verbs = rule_entry.get("verbs") or []
# Check for wildcard in resources or verbs
if "*" in resources or "*" in verbs:
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>"
namespace = event.udm("namespace") or "<CLUSTER_SCOPED>"
role_type = "ClusterRole" if resource == "clusterroles" else "Role"
if namespace != "<CLUSTER_SCOPED>":
return f"[{username}] created {role_type} [{namespace}/{name}] with wildcard permissions"
return f"[{username}] created {role_type} [{name}] with wildcard permissions "
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
name = event.udm("name") or "<UNKNOWN_ROLE>"
return f"k8s_role_wildcard_{username}_{resource}_{name}"
def severity(event):
"""ClusterRoles with wildcards are more dangerous than namespaced Roles."""
resource = event.udm("resource") or ""
# Critical for ClusterRole (cluster-wide wildcard permissions)
if resource == "clusterroles":
return "CRITICAL"
# High for namespaced Role (namespace-scoped wildcard permissions)
return "HIGH"
def alert_context(event):
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
# Extract only the rules that contain wildcards
wildcard_rules = []
for rule_entry in rules:
resources = rule_entry.get("resources") or []
verbs = rule_entry.get("verbs") or []
if "*" in resources or "*" in verbs:
wildcard_rules.append(rule_entry)
return k8s_alert_context(
event,
extra_fields={
"role_name": event.udm("name"),
"role_type": event.udm("resource"),
"wildcard_rules": wildcard_rules,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Role.Wildcard"
DisplayName: "Kubernetes Role With Wildcard Permissions Created"
Enabled: true
Status: Experimental
Filename: k8s_role_wildcard.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Privilege Escalation
- Excessive Permissions
- RBAC
- Unified Detection
Severity: High
Description: >
This detection monitors for Roles or ClusterRoles being created with wildcard (*) permissions in resources
or verbs. Wildcard permissions grant overly broad access, such as all operations on all resources, which
violates the principle of least privilege. Attackers who gain RBAC modification permissions often create
wildcard roles to maximize their access across the cluster without knowing specific resource names or API operations.
Runbook: |
1. Review the role rules to identify the scope of wildcard permissions and determine if this role creation is expected
2. Identify all API operations by the creating user in the 2 hours before and after the alert and search for RoleBindings or ClusterRoleBindings that reference this role
3. If unauthorized, immediately delete the role and any bindings, then audit all API activity in the cluster in the past 24 hours to assess impact
Reports:
MITRE ATT&CK:
- TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
- TA0005:T1078.004 # Defense Evasion: Valid Accounts - Cloud Accounts
Reference: >
- https://kubernetes.io/docs/reference/access-authn-authz/rbac/#privilege-escalation-prevention-and-bootstrapping
- https://hub.datree.io/built-in-rules/prevent-wildcards-role-clusterrole
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- resource
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceis one ofroles,clusterrolesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
any element of
requestObject.rulesmatches:any of:
requestObject.rules.resourcescontains*requestObject.rules.verbscontains*
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | in | clusterroles, roles | excludes:resource field:"resource" value:"clusterroles" field:"resource" value:"roles" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review the role rules to identify the scope of wildcard permissions and determine if this role creation is expected
2. Identify all API operations by the creating user in the 2 hours before and after the alert and search for RoleBindings or ClusterRoleBindings that reference this role
3. If unauthorized, immediately delete the role and any bindings, then audit all API activity in the cluster in the past 24 hours to assess impact
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "super-admin",
"resource": "clusterroles"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRole",
"metadata": {
"name": "super-admin"
},
"rules": [
{
"apiGroups": [
"*"
],
"resources": [
"*"
],
"verbs": [
"*"
]
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Role With Write Permissions Created
#This detection monitors for Roles or ClusterRoles being created with write permissions (create, update, patch, delete, deletecollection). While write permissions are common and often necessary for application operations, tracking role creation helps establish RBAC baselines and identify overly permissive configurations. Severity escalates for write access to sensitive resources like secrets or RBAC objects.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-clusterroles: create clusterroles |
| Kubernetes | create-roles: create roles |
Rules detecting the same action
These rules filter on the same operation.
- ClusterRole With Pod Exec Created (Falco)
- ClusterRole With Wildcard Created (Falco)
- ClusterRole With Write Privileges Created (Falco)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Creation or Modification of Sensitive Role (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- K8s ClusterRole Created (Falco)
Detection logic
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
# Write-related verbs that modify cluster state
WRITE_VERBS = {
"create",
"update",
"patch",
"delete",
"deletecollection",
}
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 noise from legitimate operators
if is_system_principal(username):
return False
# Check if role grants write permissions
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
for rule_entry in rules:
verbs = rule_entry.get("verbs") or []
# Check if any write verb is present
if any(verb in WRITE_VERBS for verb in verbs):
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>"
namespace = event.udm("namespace") or "<CLUSTER_SCOPED>"
role_type = "ClusterRole" if resource == "clusterroles" else "Role"
if namespace != "<CLUSTER_SCOPED>":
return f"[{username}] created {role_type} [{namespace}/{name}] with write " f"permissions"
return f"[{username}] created {role_type} [{name}] with write permissions " f""
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
name = event.udm("name") or "<UNKNOWN_ROLE>"
return f"k8s_role_write_{username}_{resource}_{name}"
def severity(event):
"""Increase severity for dangerous combinations of write permissions."""
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
resource = event.udm("resource") or ""
# Check for high-risk resource + write verb combinations
for rule_entry in rules:
resources_list = rule_entry.get("resources") or []
verbs = rule_entry.get("verbs") or []
# Critical: Write access to secrets or RBAC resources
sensitive_resources = {
"secrets",
"clusterroles",
"clusterrolebindings",
"roles",
"rolebindings",
}
if any(res in sensitive_resources for res in resources_list) and any(
v in {"create", "update", "patch", "delete"} for v in verbs
):
return "CRITICAL"
# High: ClusterRole with write to pods or nodes
if resource == "clusterroles" and any(
res in {"pods", "nodes", "persistentvolumes"} for res in resources_list
):
if any(v in {"create", "update", "patch", "delete"} for v in verbs):
return "HIGH"
# Medium: ClusterRole with general write permissions
if resource == "clusterroles":
return "MEDIUM"
# Low: Namespaced Role with write permissions (common/expected)
return "LOW"
def alert_context(event):
request_object = event.udm("requestObject") or {}
rules = request_object.get("rules") or []
# Extract only the rules that contain write verbs
write_rules = []
for rule_entry in rules:
verbs = rule_entry.get("verbs") or []
if any(verb in WRITE_VERBS for verb in verbs):
write_rules.append(rule_entry)
return k8s_alert_context(
event,
extra_fields={
"role_name": event.udm("name"),
"role_type": event.udm("resource"),
"write_rules": write_rules,
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Role.WritePermissions"
DisplayName: "Kubernetes Role With Write Permissions Created"
Enabled: true
Status: Experimental
Filename: k8s_role_write_permissions.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- RBAC
- Configuration Management
- Unified Detection
Severity: Low
Description: >
This detection monitors for Roles or ClusterRoles being created with write permissions (create, update, patch,
delete, deletecollection). While write permissions are common and often necessary for application operations,
tracking role creation helps establish RBAC baselines and identify overly permissive configurations. Severity
escalates for write access to sensitive resources like secrets or RBAC objects.
Runbook: |
1. Review the role rules to identify what resources have write permissions and determine if this role creation is expected for the user or application
2. Search for RoleBindings or ClusterRoleBindings that reference this role in the past 24 hours to identify who will receive these permissions
3. If the role grants excessive permissions, work with the team to implement least-privilege access and replace with a more restrictive role
Reports:
MITRE ATT&CK:
- TA0005:T1222 # Defense Evasion: File and Directory Permissions Modification
Reference: >
- https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole
- https://www.paloaltonetworks.ca/cyberpedia/kubernetes-rbac#roles
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- resource
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceis one ofroles,clusterrolesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | in | clusterroles, roles | excludes:resource field:"resource" value:"clusterroles" field:"resource" value:"roles" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Review the role rules to identify what resources have write permissions and determine if this role creation is expected for the user or application
2. Search for RoleBindings or ClusterRoleBindings that reference this role in the past 24 hours to identify who will receive these permissions
3. If the role grants excessive permissions, work with the team to implement least-privilege access and replace with a more restrictive role
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "secret-manager",
"resource": "clusterroles"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRole",
"metadata": {
"name": "secret-manager"
},
"rules": [
{
"apiGroups": [
""
],
"resources": [
"secrets"
],
"verbs": [
"get",
"list",
"delete"
]
}
]
},
"responseStatus": {
"code": 201
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "admin@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes Secret Access Denied
#This detection monitors for failed attempts to read Kubernetes secrets. While occasional failed access attempts may indicate RBAC misconfigurations, repeated failures suggest enumeration or brute-force attempts by compromised accounts. With 15-minute deduplication, 20 or more failed attempts within this window indicates active secret enumeration and should be investigated immediately.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Discovery |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | get-secrets: get secrets |
Rules detecting the same action
These rules filter on the same operation.
- Azure AKS Secret get or list with Suspicious User Agent (Elastic)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Rapid Secret GET Activity Against Multiple Objects (Elastic)
- GKE Secret Access from Node or Denied Service Account (Elastic)
- GKE Secret Access via Unusual User Agent (Elastic)
- GKE Secret get or list with Suspicious User Agent (Elastic)
- GKE Unusual Service Account Secret Access via New User Agent (Elastic)
- K8s Secret Get Successfully (Falco)
Detection logic
from panther_kubernetes_helpers import (
is_failed_request,
is_system_namespace,
is_system_principal,
k8s_alert_context,
)
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
namespace = event.udm("namespace")
username = event.udm("username")
response_status = event.udm("responseStatus")
# Only check secret get operations
if verb != "get" or resource != "secrets":
return False
# Only alert on failed requests (access denied)
if not is_failed_request(response_status):
return False
# Exclude system namespaces and system principals
if is_system_namespace(namespace) or is_system_principal(username):
return False
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
name = event.udm("name") or "<UNKNOWN_SECRET>"
response_status = event.udm("responseStatus") or {}
status_code = response_status.get("code", "UNKNOWN")
return (
f"[{username}] failed secret enumeration attempt "
f"in [{namespace}/{name}] "
f"(response: {status_code})"
)
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
namespace = event.udm("namespace") or "<UNKNOWN_NAMESPACE>"
return f"k8s_secret_denied_{username}_{namespace}"
def alert_context(event):
response_status = event.udm("responseStatus") or {}
return k8s_alert_context(
event,
extra_fields={
"secret_name": event.udm("name"),
"response_code": response_status.get("code"),
"response_message": response_status.get("message"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.Secret.AccessDenied"
DisplayName: "Kubernetes Secret Access Denied"
Enabled: true
Status: Experimental
Filename: k8s_secret_access_denied.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Credential Access
- Enumeration
- Secrets Management
- Unified Detection
Severity: Medium
Description: >
This detection monitors for failed attempts to read Kubernetes secrets. While occasional failed access attempts
may indicate RBAC misconfigurations, repeated failures suggest enumeration or brute-force attempts by compromised
accounts. With 15-minute deduplication, 20 or more failed attempts within this window indicates active secret
enumeration and should be investigated immediately.
Runbook: |
1. Review the total count of failed secret access attempts for this user in the 15-minute dedup window and identify all secrets they attempted to access
2. Determine if the user or service account should have access to any secrets and check if this represents a misconfiguration or malicious enumeration
3. Search for successful secret access by this user in the past 24 hours and review all other API operations to identify if the account is compromised
Reports:
MITRE ATT&CK:
- TA0006:T1552.007 # Credential Access: Unsecured Credentials - Container API
- TA0007:T1613 # Discovery: Container and Resource Discovery
Reference: https://kubernetes.io/docs/concepts/configuration/secret/
DedupPeriodMinutes: 15
Threshold: 20
SummaryAttributes:
- username
- namespace
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbisgetresourceissecretsresponseStatusis presentany of:
responseStatus.codeis at least400all of:
responseStatus.codeis at least1responseStatus.codeis at most16
any of:
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username | |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
resource | ne | secrets | excludes:resource field:"resource" value:"secrets" |
verb | ne | get | excludes:verb field:"verb" value:"get" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
responseStatus | is_not_null | field:"responseStatus" kind:is_not_null | |
responseStatus.code | ge |
| field:"responseStatus.code" kind:ge |
responseStatus.code | le |
| field:"responseStatus.code" kind:le value:"16" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name | |
code | responseStatus.code |
Response runbook
1. Review the total count of failed secret access attempts for this user in the 15-minute dedup window and identify all secrets they attempted to access
2. Determine if the user or service account should have access to any secrets and check if this represents a misconfiguration or malicious enumeration
3. Search for successful secret access by this user in the past 24 hours and review all other API operations to identify if the account is compromised
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "db-password",
"namespace": "production",
"resource": "secrets"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"responseStatus": {
"code": 403,
"message": "Forbidden: User cannot get resource secrets in namespace production"
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "get"
}
Kubernetes Secret Enumeration by a User
#Detects when a single user accesses 15 or more distinct secrets within 30 minutes using list, get, or watch verbs. This may indicate secret enumeration to enable lateral or vertical movement and unauthorized access to critical resources. The threshold should be tuned to your environment.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | get-secrets: get secrets |
| Kubernetes | list-secrets: list secrets |
| Kubernetes | watch-secrets: watch secrets |
Rules detecting the same action
These rules filter on the same operation.
- Azure AKS Secret get or list with Suspicious User Agent (Elastic)
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Rapid Secret GET Activity Against Multiple Objects (Elastic)
- GKE Secret Access from Node or Denied Service Account (Elastic)
- GKE Secret Access via Unusual User Agent (Elastic)
- GKE Secret get or list with Suspicious User Agent (Elastic)
- GKE Secrets List from Unusual Source AS Organization (Elastic)
- GKE Unusual Service Account Secret Access via New User Agent (Elastic)
Detection logic
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
SECRET_VERBS = {"list", "get", "watch"}
def rule(event):
if event.udm("verb") not in SECRET_VERBS:
return False
if event.udm("resource") != "secrets":
return False
if is_system_principal(event.udm("username") or ""):
return False
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
return f"Kubernetes Secret Enumeration by [{username}]"
def dedup(event):
return event.udm("username") or "<UNKNOWN_USER>"
def unique(event):
secret_name = event.udm("name")
if secret_name:
return secret_name
# List/watch requests target secret collections and often omit objectRef.name.
verb = event.udm("verb")
if verb in ("list", "watch"):
namespace = event.udm("namespace")
if namespace:
return f"{verb}:{namespace}"
request_uri = event.udm("requestURI")
if request_uri:
return f"{verb}:{request_uri}"
return verb
return None
def severity(event):
if not is_failed_request(event.udm("responseStatus")):
return "HIGH"
return "DEFAULT"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"secret_name": event.udm("name"),
"verb": event.udm("verb"),
"user_agent": event.udm("userAgent"),
},
)
Rule specification
AnalysisType: rule
Filename: k8s_secret_enumeration.py
RuleID: "Kubernetes.BulkSecretAccess"
DisplayName: "Kubernetes Secret Enumeration by a User"
Status: Experimental
Enabled: false
Severity: Medium
DedupPeriodMinutes: 30
Threshold: 15
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Credential Access
Description: >
Detects when a single user accesses 15 or more distinct secrets within 30 minutes
using list, get, or watch verbs. This may indicate secret enumeration to enable
lateral or vertical movement and unauthorized access to critical resources.
The threshold should be tuned to your environment.
Reports:
MITRE ATT&CK:
- TA0006:T1552.007
Runbook: |
1. Query Amazon.EKS.Audit for all secret access events by the username and userAgent in the 30 minutes around this alert to identify the full list of secrets accessed and the verbs used
2. Determine if the user or service account has a legitimate reason to access this volume of secrets, and check if the responseStatus codes indicate successful reads or denied attempts
3. Search for other suspicious API activity by this username in the past 24 hours, including privilege escalation attempts, role or clusterrole binding changes, or unusual resource access patterns
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbis one oflist,get,watchresourceissecretsany of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
username | is_not_null | excludes:username |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
resource | eq |
| field:"resource" kind:eq value:"secrets" |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | in |
| field:"verb" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Query Amazon.EKS.Audit for all secret access events by the username and userAgent in the 30 minutes around this alert to identify the full list of secrets accessed and the verbs used
2. Determine if the user or service account has a legitimate reason to access this volume of secrets, and check if the responseStatus codes indicate successful reads or denied attempts
3. Search for other suspicious API activity by this username in the past 24 hours, including privilege escalation attempts, role or clusterrole binding changes, or unusual resource access patterns
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "db-credentials",
"namespace": "production",
"resource": "secrets"
},
"p_log_type": "Amazon.EKS.Audit",
"responseStatus": {
"code": 200
},
"stage": "ResponseComplete",
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "get"
}
Kubernetes Service Account Token Theft from Pod
#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.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Credential Access | |
| Lateral Movement |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Kubernetes | create-pods-exec: create pods/exec |
Rules detecting the same action
These rules filter on the same operation.
- Attach/Exec Pod (Falco)
- Azure AKS Attempted User Exec into Pod (Elastic)
- Container With A hostPath Mount Created (Sigma)
- Create Disallowed Pod (Falco)
- Create HostIPC Pod (Falco)
- Create HostNetwork Pod (Falco)
- Create HostPid Pod (Falco)
- Create Privileged Pod (Falco)
Detection logic
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"),
},
)
Rule specification
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
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbiscreateresourceispodssubresourceisexecany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
namespaceis emptynamespaceis not one ofkube-system,gke-system,kube-node-lease,kube-public
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
username | contains | serviceaccount | excludes:username field:"username" value:"serviceaccount" |
username | starts_with | system: | excludes:username field:"username" value:"system:" |
username | in | aksService, masterclient | excludes:username field:"username" value:"aksService" field:"username" value:"masterclient" |
namespace | in | gke-system, kube-node-lease, kube-public, kube-system | excludes:namespace |
namespace | is_not_null | excludes:namespace | |
username | is_not_null | excludes:username | |
responseStatus.code | ge | 1 | excludes:responseStatus.code field:"responseStatus.code" value:"1" |
responseStatus.code | le | 16 | excludes:responseStatus.code field:"responseStatus.code" value:"16" |
responseStatus.code | ge | 400 | excludes:responseStatus.code field:"responseStatus.code" value:"400" |
responseStatus | is_not_null | excludes:responseStatus | |
resource | ne | pods | excludes:resource field:"resource" value:"pods" |
subresource | ne | exec | excludes:subresource field:"subresource" value:"exec" |
verb | ne | create | excludes:verb field:"verb" value:"create" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response 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
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"name": "webapp-pod",
"namespace": "production",
"resource": "pods",
"subresource": "exec"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"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",
"responseStatus": {
"code": 101
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "create"
}
Kubernetes System Principal Accessed from Non-Cloud Public IP
#This detection identifies when Kubernetes system principals (service accounts with usernames starting with "system:", "eks:", or "aks:") are accessed from non-cloud provider public IP addresses. System principals should only operate from within the cluster (private IPs) or from legitimate cloud infrastructure. Access from external public IPs indicates potential service account token theft or compromise, often following initial access to a cluster.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access | |
| Lateral Movement |
Detection logic
from ipaddress import ip_address
from panther_ipinfo_helpers import get_ipinfo_asn
from panther_kubernetes_helpers import k8s_alert_context
# AWS-managed services that run as Lambdas and legitimately originate from public IPs
AWS_MANAGED_PRINCIPALS = {"eks:addon-manager", "eks:node-manager"}
# AKS-managed service accounts that legitimately make requests from public IPs
# These are CSI drivers and system controllers that run on nodes with public IPs
AKS_MANAGED_SERVICE_ACCOUNTS = {
"system:serviceaccount:kube-system:csi-azuredisk-node-sa",
"system:serviceaccount:kube-system:csi-azurefile-node-sa",
"system:serviceaccount:kube-system:csi-secrets-store-provider-azure",
"system:serviceaccount:kube-system:cloud-node-manager",
}
# Cloud provider ASN mappings for infrastructure IP detection
CLOUD_PROVIDER_ASNS = {
"aws": ["AS16509"],
"azure": ["AS8075"],
"gcp": ["AS15169", "AS396982"],
}
# GKE control plane user agents that legitimately make anonymous requests from Google IPs
GKE_SYSTEM_USER_AGENTS = {
"GoogleKubernetesEngineFrontend",
"GoogleHC/1.0",
}
# GKE health check endpoints accessed by control plane
GKE_HEALTH_CHECK_ENDPOINTS = {"readyz", "livez", "healthz"}
def is_cloud_infrastructure_ip(event, cloud_provider):
"""Check if source IP is from cloud provider infrastructure using IPInfo ASN."""
ipinfo_asn_data = get_ipinfo_asn(event)
if not ipinfo_asn_data:
return False
# Get ASN from appropriate field based on log type
log_type = event.get("p_log_type", "")
if "GCP" in log_type:
asn_value = ipinfo_asn_data.asn("callerIp")
else:
asn_value = ipinfo_asn_data.asn("sourceIPs")
if (
asn_value
and len(asn_value) > 0
and asn_value[0] in CLOUD_PROVIDER_ASNS.get(cloud_provider, [])
):
return True
return False
def _is_legitimate_eks_node(event):
"""Check if this is a legitimate EKS node based on username and user groups."""
username = event.udm("username") or ""
# Check if it's a system node
if username.startswith("system:node:"):
user_groups = event.deep_get("user", "groups", default=[])
# Legitimate EKS nodes should be in system:nodes and system:authenticated groups
return "system:nodes" in user_groups and "system:authenticated" in user_groups
return False
def is_aws_managed_service(event):
"""Check if this is an AWS-managed EKS service like addon-manager or node-manager."""
username = event.udm("username") or ""
if username not in AWS_MANAGED_PRINCIPALS:
return False
# Verify it's actually from AWS Lambda (AWSWesleyClusterManagerLambda role)
arn = event.deep_get("user", "extra", "arn", default=[""])[0]
return ":assumed-role/AWSWesleyClusterManagerLambda" in arn
def _is_legitimate_eks_request(event):
"""Check if this is a legitimate EKS request."""
if is_aws_managed_service(event):
return True
return _is_legitimate_eks_node(event) and is_cloud_infrastructure_ip(event, "aws")
def _is_legitimate_aks_request(event, username):
"""Check if this is a legitimate AKS request."""
if username in AKS_MANAGED_SERVICE_ACCOUNTS:
return True
return username.startswith("system:node:") and is_cloud_infrastructure_ip(event, "azure")
def _is_legitimate_gke_request(event, username):
"""Check if this is a legitimate GKE request."""
if username.startswith("system:node:") and is_cloud_infrastructure_ip(event, "gcp"):
return True
# GKE control plane makes anonymous health check requests from Google IPs
if username == "system:anonymous":
user_agent = event.deep_get(
"protoPayload", "requestMetadata", "callerSuppliedUserAgent", default=""
)
if user_agent not in GKE_SYSTEM_USER_AGENTS:
return False
# Prefer ASN-based verification
if is_cloud_infrastructure_ip(event, "gcp"):
return True
# Fallback: Health check endpoints are legitimate without ASN verification
resource_name = event.deep_get("protoPayload", "resourceName", default="")
request_uri = event.udm("requestURI") or ""
# Use path-based matching to avoid false positives
return any(
f"/{endpoint}" in resource_name
or resource_name.endswith(endpoint)
or f"/{endpoint}" in request_uri
or request_uri.endswith(endpoint)
for endpoint in GKE_HEALTH_CHECK_ENDPOINTS
)
return False
def _is_legitimate_cloud_node(event, username, log_type):
"""Check if this is a legitimate cloud provider node."""
if "Amazon.EKS" in log_type:
return _is_legitimate_eks_request(event)
if "Azure.MonitorActivity" in log_type:
return _is_legitimate_aks_request(event, username)
if "GCP.AuditLog" in log_type:
return _is_legitimate_gke_request(event, username)
return False
def rule(event): # pylint: disable=too-many-return-statements
username = event.udm("username") or ""
source_ips = event.udm("sourceIPs") or []
response_status = event.udm("responseStatus") or {}
log_type = event.get("p_log_type", "")
# Only check ResponseComplete stage (EKS/AKS have this field)
stage = event.get("stage")
if stage and stage != "ResponseComplete":
return False
# Exclude 403 responses (handled by k8s_multiple_403_public_ip rule)
if response_status.get("code") == 403:
return False
# Check if this is a REAL system principal (service accounts, nodes, cloud-managed)
# Excludes system:anonymous and system:unauthenticated (unauthenticated API access)
if not (
username.startswith("system:serviceaccount:")
or username.startswith("system:node:")
or username.startswith("eks:")
or username.startswith("aks:")
):
return False
# Check if source IP is public
if not source_ips:
return False
# If any source IP is private, this is a pod running on a node (which has both
# public and private interfaces). Real external attackers only have public IPs.
for ip_str in source_ips:
try:
ip_obj = ip_address(ip_str)
if not ip_obj.is_global:
return False
except ValueError:
continue # Skip invalid IPs
# Exclude legitimate cloud provider nodes
if _is_legitimate_cloud_node(event, username, log_type):
return False
# Alert: system principal accessed from non-cloud-provider public IP
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
verb = event.udm("verb") or "<UNKNOWN_VERB>"
resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
namespace = event.udm("namespace")
source_ips = event.udm("sourceIPs") or ["<UNKNOWN_IP>"]
source_ip = source_ips[0] if source_ips else "<UNKNOWN_IP>"
# Handle cluster-scoped resources (no namespace)
if namespace:
namespace_str = f"in namespace [{namespace}] "
else:
namespace_str = "(cluster-scoped) "
return (
f"System principal [{username}] executed [{verb}] for resource [{resource}] "
f"{namespace_str}from non-cloud public IP [{source_ip}]"
)
def dedup(event):
source_ips = event.udm("sourceIPs") or ["<UNKNOWN_IP>"]
source_ip = source_ips[0] if source_ips else "<UNKNOWN_IP>"
return f"k8s_system_principal_{source_ip}"
def alert_context(event):
source_ips = event.udm("sourceIPs") or []
ipinfo_asn_data = get_ipinfo_asn(event)
extra_fields = {
"source_ip": source_ips[0] if source_ips else None,
"asn_info": None,
}
# Add ASN information if available
if ipinfo_asn_data:
log_type = event.get("p_log_type", "")
if "GCP" in log_type:
asn_value = ipinfo_asn_data.asn("callerIp")
domain_value = ipinfo_asn_data.domain("callerIp")
else:
asn_value = ipinfo_asn_data.asn("sourceIPs")
domain_value = ipinfo_asn_data.domain("sourceIPs")
if asn_value and asn_value[0]:
extra_fields["asn_info"] = {
"asn": asn_value[0],
"domain": domain_value[0] if domain_value else None,
}
return k8s_alert_context(event, extra_fields=extra_fields)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.System.Principal.PublicIP"
DisplayName: "Kubernetes System Principal Accessed from Non-Cloud Public IP"
Enabled: true
Status: Experimental
Filename: k8s_system_principal_public_ip.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Initial Access
- Lateral Movement
- Credential Access
- Unified Detection
Severity: High
Description: >
This detection identifies when Kubernetes system principals (service accounts with usernames
starting with "system:", "eks:", or "aks:") are accessed from non-cloud provider public IP
addresses. System principals should only operate from within the cluster (private IPs) or
from legitimate cloud infrastructure. Access from external public IPs indicates potential
service account token theft or compromise, often following initial access to a cluster.
Runbook: |
1. Find all Kubernetes API requests from the sourceIPs address in the 24 hours before and after the alert to identify targeted resources and operations
2. Query for authentication and service account token events by this username in the 48 hours before the alert to determine if the token was recently compromised
3. Search for other system principal alerts from the same sourceIPs address across all clusters in the past 7 days to assess campaign scope
Reference: https://kubernetes.io/docs/concepts/security/rbac-good-practices/
Reports:
MITRE ATT&CK:
- TA0001:T1190 # Initial Access: Exploit Public-Facing Application
- TA0006:T1528 # Credential Access: Steal Application Access Token
- TA0008:T1021.007 # Lateral Movement: Remote Services: Cloud Services
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- p_any_ip_addresses
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
any of:
stageis emptystageisResponseComplete
responseStatus.codeis not403any of:
usernamestarts withsystem:serviceaccount:usernamestarts withsystem:node:usernamestarts witheks:usernamestarts withaks:
sourceIPsis presentany of:
p_log_typedoes not containAmazon.EKSall of:
usernameis not one ofeks:addon-manager,eks:node-managerany of:
usernamedoes not start withsystem:node:user.groupsdoes not containsystem:nodesuser.groupsdoes not containsystem:authenticated
any of:
p_log_typecontainsAmazon.EKSp_log_typedoes not containAzure.MonitorActivityall of:
usernameis not one ofsystem:serviceaccount:kube-system:csi-azuredisk-node-sa,system:serviceaccount:kube-system:csi-azurefile-node-sa,system:serviceaccount:kube-system:csi-secrets-store-provider-azure,system:serviceaccount:kube-system:cloud-node-managerusernamedoes not start withsystem:node:
any of:
p_log_typecontainsAmazon.EKSp_log_typecontainsAzure.MonitorActivityp_log_typedoes not containGCP.AuditLogall of:
usernamedoes not start withsystem:node:any of:
usernameis notsystem:anonymousprotoPayload.requestMetadata.callerSuppliedUserAgentis not one ofGoogleKubernetesEngineFrontend,GoogleHC/1.0all of:
protoPayload.resourceNamedoes not end withreadyzrequestURIdoes not end withreadyzprotoPayload.resourceNamedoes not end withlivezrequestURIdoes not end withlivezprotoPayload.resourceNamedoes not end withhealthzrequestURIdoes not end withhealthz
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
p_log_type | contains |
| field:"p_log_type" kind:contains |
responseStatus.code | ne |
| field:"responseStatus.code" kind:ne value:"403" |
sourceIPs | is_not_null | field:"sourceIPs" kind:is_not_null | |
username | starts_with |
| field:"username" kind:starts_with |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
Response runbook
1. Find all Kubernetes API requests from the sourceIPs address in the 24 hours before and after the alert to identify targeted resources and operations
2. Query for authentication and service account token events by this username in the 48 hours before the alert to determine if the token was recently compromised
3. Search for other system principal alerts from the same sourceIPs address across all clusters in the past 7 days to assess campaign scope
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiVersion": "v1",
"resource": "endpointslices"
},
"p_enrichment": {
"ipinfo_asn": {
"sourceIPs": [
{
"asn": "AS12345",
"domain": "example-isp.com",
"name": "Example ISP",
"p_match": "1.2.3.4",
"type": "isp"
}
]
}
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"responseStatus": {
"code": 200
},
"sourceIPs": [
"1.2.3.4"
],
"stage": "ResponseComplete",
"user": {
"groups": [
"system:serviceaccounts",
"system:authenticated"
],
"username": "system:serviceaccount:kube-system:coredns"
},
"verb": "get"
}
Kubernetes System Role Modified or Deleted
#This detection monitors for modifications or deletions of system ClusterRoles/Roles (those starting with "system:"). These are built-in Kubernetes roles for control plane components like kube-scheduler, kube-controller-manager, and system:admin. Tampering with system roles can break cluster functionality, create privilege escalation backdoors, or disable security controls. Legitimate modifications to system roles are extremely rare outside of cluster upgrades.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Stealth | |
| Defense Impairment |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- Direct Interactive Kubernetes API Request by Unusual Utilities (Elastic)
- GKE Creation or Modification of Sensitive Role (Elastic)
- GKE RBAC Wildcard Elevation on Existing Role (Elastic)
- GKE Sensitive RBAC Change Followed by Workload Modification (Elastic)
- GKE Service Account Modified RBAC Objects (Elastic)
- K8s ClusterRole Deleted (Falco)
- K8s Role Deleted (Falco)
- Kubernetes Creation or Modification of Sensitive Role (Elastic)
Detection logic
from panther_kubernetes_helpers import is_failed_request, is_system_principal, k8s_alert_context
# System roles that are expected to change during normal operations
# Users can extend this list for their environment
ALLOWED_SYSTEM_ROLE_MODIFICATIONS = {
"system:coredns",
"system:managed-certificate-controller",
}
def rule(event):
verb = event.udm("verb")
resource = event.udm("resource")
username = event.udm("username")
response_status = event.udm("responseStatus")
name = event.udm("name") or ""
# Only check role/clusterrole modification/deletion events
if verb not in {"update", "patch", "delete"} 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 if role name starts with "system:" or "eks:" (EKS system roles)
if not (name.startswith("system:") or name.startswith("eks:")):
return False
# Exclude roles that are expected to change
if name in ALLOWED_SYSTEM_ROLE_MODIFICATIONS:
return False
return True
def title(event):
username = event.udm("username") or "<UNKNOWN_USER>"
verb = event.udm("verb") or "<UNKNOWN_VERB>"
resource = event.udm("resource") or "<UNKNOWN_RESOURCE>"
name = event.udm("name") or "<UNKNOWN_ROLE>"
namespace = event.udm("namespace") or "<CLUSTER_SCOPED>"
role_type = "ClusterRole" if resource == "clusterroles" else "Role"
action = "deleted" if verb == "delete" else "modified"
if namespace != "<CLUSTER_SCOPED>":
return f"[{username}] {action} system {role_type} [{namespace}/{name}]"
return f"[{username}] {action} system {role_type} [{name}]"
def dedup(event):
username = event.udm("username") or "<UNKNOWN_USER>"
name = event.udm("name") or "<UNKNOWN_ROLE>"
return f"k8s_system_role_{username}_{name}"
def alert_context(event):
return k8s_alert_context(
event,
extra_fields={
"role_name": event.udm("name"),
"role_type": event.udm("resource"),
"modification_type": event.udm("verb"),
},
)
Rule specification
AnalysisType: rule
RuleID: "Kubernetes.SystemRole.Modified"
DisplayName: "Kubernetes System Role Modified or Deleted"
Enabled: true
Status: Experimental
Filename: k8s_system_role_modified.py
LogTypes:
- Amazon.EKS.Audit
- Azure.MonitorActivity
- GCP.AuditLog
Tags:
- Kubernetes
- Privilege Escalation
- Defense Evasion
- Persistence
- RBAC
- Unified Detection
Severity: Critical
Description: >
This detection monitors for modifications or deletions of system ClusterRoles/Roles (those starting with "system:").
These are built-in Kubernetes roles for control plane components like kube-scheduler, kube-controller-manager, and
system:admin. Tampering with system roles can break cluster functionality, create privilege escalation backdoors,
or disable security controls. Legitimate modifications to system roles are extremely rare outside of cluster upgrades.
Runbook: |
1. Immediately review the changes made to the system role and determine if this represents a cluster upgrade or unauthorized tampering
2. If unauthorized, revert the role to its original state using kubectl or restore from backup, then revoke credentials for the modifying user
3. Search for all RBAC modifications by this user across all clusters in the past 7 days and audit all API operations to identify other malicious changes
Reports:
MITRE ATT&CK:
- TA0004:T1078.004 # Privilege Escalation: Valid Accounts - Cloud Accounts
- TA0005:T1222 # Defense Evasion: File and Directory Permissions Modification
- TA0003:T1098 # Persistence: Account Manipulation
Reference: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#default-roles-and-role-bindings
DedupPeriodMinutes: 60
SummaryAttributes:
- username
- resource
- name
- p_source_label
Stages and Predicates
Fires on Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog events when all of the conditions below hold.
Condition
verbis one ofupdate,patch,deleteresourceis one ofroles,clusterrolesany of:
responseStatusis emptyall of:
responseStatus.codeis less than400any of:
responseStatus.codeis less than1responseStatus.codeis greater than16
any of:
usernameis emptyall of:
usernameis not one ofmasterclient,aksServiceany of:
usernamedoes not start withsystem:usernamecontainsserviceaccount
any of:
namestarts withsystem:namestarts witheks:
nameis not one ofsystem:coredns,system:managed-certificate-controller
This rule also runs imperative logic the parser cannot express as a filter. The conditions above are the structured part it could extract.
Exclusions
The rule actively suppresses these predicates.
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
name | starts_with |
| field:"name" kind:starts_with |
resource | in |
| field:"resource" kind:in |
username | contains |
| field:"username" kind:contains value:"serviceaccount" |
verb | in |
| field:"verb" kind:in |
Output fields
Fields the rule emits when it matches, drawn from the rule's alert_context.
| Field | Source |
|---|---|
username | |
sourceIPs | |
userAgent | |
namespace | |
verb | |
resource | |
requestURI | |
responseStatus | |
cluster | p_source_label |
name |
Response runbook
1. Immediately review the changes made to the system role and determine if this represents a cluster upgrade or unauthorized tampering
2. If unauthorized, revert the role to its original state using kubectl or restore from backup, then revoke credentials for the modifying user
3. Search for all RBAC modifications by this user across all clusters in the past 7 days and audit all API operations to identify other malicious changes
Worked example
A sample event from the rule's unit tests that triggers a match.Sample Test Event
{
"apiVersion": "audit.k8s.io/v1",
"kind": "Event",
"objectRef": {
"apiGroup": "rbac.authorization.k8s.io",
"apiVersion": "v1",
"name": "system:node",
"resource": "clusterroles"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster",
"requestObject": {
"kind": "ClusterRole",
"metadata": {
"name": "system:node"
},
"rules": [
{
"apiGroups": [
"*"
],
"resources": [
"*"
],
"verbs": [
"*"
]
}
]
},
"responseStatus": {
"code": 200
},
"sourceIPs": [
"203.0.113.42"
],
"user": {
"username": "attacker@example.com"
},
"userAgent": "kubectl/v1.28.0",
"verb": "update"
}