Detection rules › Panther

Kubernetes Secret Access Denied

Status
Experimental
Severity
medium
Group by
namespace, username
Log types
Amazon.EKS.Audit, Azure.MonitorActivity, GCP.AuditLog
Tags
Kubernetes, Credential Access, Enumeration, Secrets Management, Unified Detection
Reference
https://kubernetes.io/docs/concepts/configuration/secret/
Source
github.com/panther-labs/panther-analysis

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

Rule body yaml

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"
      }

Detection logic

Condition

not (verb ne "get" or resource ne "secrets")
responseStatus is_not_null
responseStatus.code ge "400" or (responseStatus.code ge "1" and responseStatus.code le "16")
not ((namespace is_not_null and namespace in ["kube-system", "gke-system", "kube-node-lease", "kube-public"]) or (username is_not_null and (username in ["masterclient", "aksService"] or (username starts_with "system:" and username not contains "serviceaccount"))))

This rule also runs imperative logic the parser cannot express as a filter; the conditions above are the structured part it could extract.

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
usernamecontainsserviceaccount
usernamestarts_withsystem:
usernameinaksService, masterclient
usernameis_not_null(no value, null check)
namespaceingke-system, kube-node-lease, kube-public, kube-system
namespaceis_not_null(no value, null check)
resourcenesecrets
verbneget

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
responseStatusis_not_null
  • (no value, null check)
responseStatus.codege
  • 1
  • 400
responseStatus.codele
  • 16
usernamecontains
  • serviceaccount

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
username
sourceIPs
userAgent
namespace
verb
resource
requestURI
responseStatus
clusterp_source_label
name
coderesponseStatus.code