LoFP LoFP / platform installers, gitops controllers, and emergency break-glass roles sometimes ship or widen wildcard clusterroles; correlate with change records and narrow by user or service account when baselined.

Techniques

Sample rules

GKE RBAC Wildcard Elevation on Existing Role

Description

Flags an existing GKE Role or ClusterRole being changed (patch or update) so the effective rules become cluster-admin-like: wildcard on every API resource and wildcard on every verb. That is usually a deliberate privilege expansion, not a typo. GKE audit logs with response body capture are required so the detection reads the merged role after apply; loopback source IPs are ignored.

Detection logic

data_stream.dataset:gcp.audit and service.name:"k8s.io" and event.outcome:success and
event.action:(
  "io.k8s.authorization.rbac.v1.roles.update" or
  "io.k8s.authorization.rbac.v1.roles.patch" or
  "io.k8s.authorization.rbac.v1.clusterroles.update" or
  "io.k8s.authorization.rbac.v1.clusterroles.patch"
) and source.ip:(* and not ("127.0.0.1" or "::1")) and not (
  client.user.email:"system:addon-manager" and
  event.action:(
    "io.k8s.authorization.rbac.v1.roles.patch" or
    "io.k8s.authorization.rbac.v1.clusterroles.patch"
  )
) and
gcp.audit.response.rules.verbs:"*" and gcp.audit.response.rules.resources:"*"

Kubernetes RBAC Wildcard Elevation on Existing Role

Description

Flags an existing Role or ClusterRole being changed (patch or update) so the effective rules become cluster-admin-like: wildcard on every API resource and wildcard on every verb. That is usually a deliberate privilege expansion, not a typo. RequestResponse audit and the response body are required so the detection reads the merged role after apply; loopback source IPs are ignored.

Detection logic

from logs-kubernetes.audit_logs-* metadata _id, _index, _version
| where
  kubernetes.audit.objectRef.resource in ("roles", "clusterroles") and
  kubernetes.audit.verb in ("update", "patch") and
  `kubernetes.audit.annotations.authorization_k8s_io/decision` == "allow" and
  kubernetes.audit.level == "RequestResponse" and
  kubernetes.audit.stage == "ResponseComplete" and
  kubernetes.audit.sourceIPs is not null and
  not kubernetes.audit.sourceIPs in ("::1", "127.0.0.1") and
  KQL(""" kubernetes.audit.responseObject.rules.verbs:"*" and kubernetes.audit.responseObject.rules.resources:"*" """)
| keep user.name, user_agent.original, event.action, source.ip, kubernetes.audit.verb, kubernetes.audit.objectRef.resource, kubernetes.audit.objectRef.name, kubernetes.audit.requestURI, kubernetes.audit.user.username, kubernetes.audit.user.groups, `kubernetes.audit.annotations.authorization_k8s_io/decision`, event.original, _id, _version, _index, data_stream.namespace