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
Tests:
- Name: EKS MutatingWebhookConfiguration Created
ExpectedResult: true
Log:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"verb": "create",
"user": {"username": "admin@example.com"},
"sourceIPs": ["1.2.3.4"],
"userAgent": "kubectl/v1.28.0",
"objectRef": {
"resource": "mutatingwebhookconfigurations",
"name": "custom-mutator",
"apiGroup": "admissionregistration.k8s.io",
"apiVersion": "v1"
},
"responseStatus": {"code": 201},
"requestObject": {
"kind": "MutatingWebhookConfiguration",
"metadata": {"name": "custom-mutator"},
"webhooks": [{
"name": "mutate.example.com",
"clientConfig": {
"url": "https://webhook.example.com/mutate"
},
"rules": [{
"operations": ["CREATE", "UPDATE"],
"apiGroups": ["*"],
"apiVersions": ["*"],
"resources": ["*"]
}]
}]
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster"
}
- Name: AKS ValidatingWebhookConfiguration Created
ExpectedResult: true
Log:
{
"p_log_type": "Azure.MonitorActivity",
"category": "kube-audit",
"operationName": "Microsoft.ContainerService/managedClusters/diagnosticLogs/Read",
"properties": {
"log": "{\"kind\":\"Event\",\"apiVersion\":\"audit.k8s.io/v1\",\"verb\":\"create\",\"user\":{\"username\":\"policy-admin@example.com\"},\"sourceIPs\":[\"10.0.0.1\"],\"objectRef\":{\"resource\":\"validatingwebhookconfigurations\",\"name\":\"policy-validator\",\"apiGroup\":\"admissionregistration.k8s.io\"},\"responseStatus\":{\"code\":201},\"requestObject\":{\"kind\":\"ValidatingWebhookConfiguration\",\"webhooks\":[{\"name\":\"validate.policy.com\"}]}}"
},
"p_source_label": "aks-cluster"
}
- Name: GKE MutatingWebhookConfiguration Created
ExpectedResult: true
Log:
{
"protoPayload": {
"authenticationInfo": {"principalEmail": "user@company.com"},
"authorizationInfo": [{
"granted": true,
"permission": "io.k8s.admissionregistration.v1.mutatingwebhookconfigurations.create",
"resource": "admissionregistration.k8s.io/v1/mutatingwebhookconfigurations/istio-sidecar"
}],
"methodName": "io.k8s.admissionregistration.v1.mutatingwebhookconfigurations.create",
"requestMetadata": {"callerIP": "1.2.3.4"},
"resourceName": "admissionregistration.k8s.io/v1/mutatingwebhookconfigurations/istio-sidecar",
"serviceName": "k8s.io",
"request": {
"kind": "MutatingWebhookConfiguration",
"metadata": {"name": "istio-sidecar"},
"webhooks": [{
"name": "sidecar-injector.istio.io",
"clientConfig": {
"service": {
"name": "istio-sidecar-injector",
"namespace": "istio-system"
}
}
}]
}
},
"resource": {
"type": "k8s_cluster",
"labels": {"project_id": "test-project"}
},
"p_log_type": "GCP.AuditLog",
"p_source_label": "gke-cluster"
}
- Name: Webhook Update (Not Create)
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "update",
"objectRef": {
"resource": "mutatingwebhookconfigurations",
"name": "existing-webhook"
},
"responseStatus": {"code": 200},
"p_log_type": "Amazon.EKS.Audit"
}
- Name: Different Resource Type
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "create",
"objectRef": {
"resource": "configmaps",
"name": "test-config"
},
"responseStatus": {"code": 201},
"p_log_type": "Amazon.EKS.Audit"
}
- Name: Webhook Creation Failed
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "create",
"objectRef": {
"resource": "validatingwebhookconfigurations",
"name": "test-webhook"
},
"responseStatus": {"code": 403},
"p_log_type": "Amazon.EKS.Audit"
}
# ------ paired body: k8s_admission_controller_created.py ------
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,
},
)