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
Tests:
- Name: EKS failed secret get (403 Forbidden)
ExpectedResult: true
Log:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"verb": "get",
"user": {"username": "attacker@example.com"},
"sourceIPs": ["203.0.113.42"],
"userAgent": "kubectl/v1.28.0",
"objectRef": {
"resource": "secrets",
"namespace": "production",
"name": "db-password",
"apiVersion": "v1"
},
"responseStatus": {
"code": 403,
"message": "Forbidden: User cannot get resource secrets in namespace production"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster"
}
- Name: EKS failed secret get (404 Not Found)
ExpectedResult: true
Log:
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"verb": "get",
"user": {"username": "attacker@example.com"},
"sourceIPs": ["203.0.113.42"],
"userAgent": "kubectl/v1.28.0",
"objectRef": {
"resource": "secrets",
"namespace": "production",
"name": "nonexistent-secret",
"apiVersion": "v1"
},
"responseStatus": {
"code": 404,
"message": "secrets \"nonexistent-secret\" not found"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster"
}
- Name: AKS failed secret get (401 Unauthorized)
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\":\"get\",\"user\":{\"username\":\"compromised-user@example.com\"},\"sourceIPs\":[\"1.2.3.4\"],\"objectRef\":{\"resource\":\"secrets\",\"namespace\":\"default\",\"name\":\"api-token\"},\"responseStatus\":{\"code\":401,\"message\":\"Unauthorized\"}}"
},
"p_source_label": "aks-cluster"
}
- Name: GKE failed secret get
ExpectedResult: true
Log:
{
"protoPayload": {
"authenticationInfo": {"principalEmail": "user@company.com"},
"authorizationInfo": [{
"granted": false,
"permission": "io.k8s.core.v1.secrets.get",
"resource": "core/v1/namespaces/production/secrets/tls-cert"
}],
"methodName": "io.k8s.core.v1.secrets.get",
"requestMetadata": {"callerIP": "8.8.8.8"},
"resourceName": "core/v1/namespaces/production/secrets/tls-cert",
"serviceName": "k8s.io",
"status": {
"code": 7,
"message": "PERMISSION_DENIED"
}
},
"resource": {
"type": "k8s_cluster",
"labels": {"project_id": "test-project"}
},
"p_log_type": "GCP.AuditLog",
"p_source_label": "gke-cluster"
}
- Name: Service account failed to get secret
ExpectedResult: true
Log:
{
"kind": "Event",
"verb": "get",
"user": {"username": "system:serviceaccount:default:app-sa"},
"objectRef": {
"resource": "secrets",
"namespace": "production",
"name": "admin-token"
},
"responseStatus": {
"code": 403,
"message": "Forbidden"
},
"p_log_type": "Amazon.EKS.Audit",
"p_source_label": "eks-cluster"
}
- Name: Successful secret get
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "get",
"user": {"username": "admin@example.com"},
"objectRef": {
"resource": "secrets",
"namespace": "default",
"name": "app-config"
},
"responseStatus": {"code": 200},
"p_log_type": "Amazon.EKS.Audit"
}
- Name: System service account failed get (excluded)
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "get",
"user": {"username": "system:serviceaccount:kube-system:controller"},
"objectRef": {
"resource": "secrets",
"namespace": "kube-system",
"name": "token"
},
"responseStatus": {"code": 403},
"p_log_type": "Amazon.EKS.Audit"
}
- Name: Failed get on non-secret resource
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "get",
"user": {"username": "user@example.com"},
"objectRef": {
"resource": "configmaps",
"namespace": "default",
"name": "app-config"
},
"responseStatus": {"code": 403},
"p_log_type": "Amazon.EKS.Audit"
}
- Name: List secrets operation (not get)
ExpectedResult: false
Log:
{
"kind": "Event",
"verb": "list",
"user": {"username": "attacker@example.com"},
"objectRef": {
"resource": "secrets",
"namespace": "production"
},
"responseStatus": {"code": 403},
"p_log_type": "Amazon.EKS.Audit"
}
# ------ paired body: k8s_secret_access_denied.py ------
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"),
},
)