GKE Multi-Resource Discovery
editGKE Multi-Resource Discovery
editAdversaries who land credentials in a GKE cluster—or abuse an over-privileged token, often map the environment before exfiltration or privilege escalation. A practical first pass is to learn where workloads run, how the cluster is partitioned, and what RBAC exists at namespace vs cluster scope. Rapid get/list traffic across many distinct API resource kinds that answer those questions (namespaces, workloads, roles, cluster-wide roles) is a common setup and orientation pattern for both interactive attackers and automated recon scripts. This rule highlights that cross-resource burst from a single client fingerprint within a one-minute bucket when both cluster-layout and RBAC resource kinds are touched, so analysts can separate routine automation from potential discovery ahead of follow-on actions.
Rule type: esql
Rule indices: None
Severity: medium
Risk score: 47
Runs every: 5m
Searches indices from: now-6m (Date Math format, see also Additional look-back time)
Maximum alerts per execution: 100
References:
Tags:
- Domain: Cloud
- Domain: Kubernetes
- Data Source: GCP
- Data Source: Google Cloud Platform
- Use Case: Threat Detection
- Tactic: Discovery
- Resources: Investigation Guide
Version: 1
Rule authors:
- Elastic
Rule license: Elastic License v2
Investigation guide
editTriage and analysis
Investigating GKE Multi-Resource Discovery
The rule groups GKE audit get/list events on namespaces, nodes, pods, configmaps, serviceaccounts, roles,
rolebindings, clusterroles, and clusterrolebindings into one-minute windows per client.user.email, source.ip,
and user_agent.original. It alerts when five or more distinct resource kinds appear and the burst includes both
cluster-layout kinds (namespaces, pods, or nodes) and RBAC kinds. Allowed and denied authorizations are both
included: failures still signal probing.
Possible investigation steps
-
Review
Esql.enumerated_resources,Esql.enumerated_namespaces, andEsql.enumerated_resource_namesfor ordering and targeted APIs. -
Confirm whether
source.ipanduser_agent.originalmatch expected admin or automation clients. - Correlate with follow-on secret reads, RoleBinding changes, pod exec, or unusual user agents from the same actor.
False positive analysis
- Documented platform sync jobs that read layout and RBAC together; exclude known service accounts after validation.
- Upgrade or install windows that briefly query many resource kinds; correlate with change records.
Response and remediation
- If malicious, revoke or rotate the implicated credentials, tighten RBAC, and inspect for data access or persistence established after the burst.
Setup
editThe GCP Fleet integration with GKE audit logs enabled is required to be compatible with this rule.
Rule query
editfrom logs-gcp.audit-* metadata _id, _index, _version
| where data_stream.dataset == "gcp.audit"
and service.name == "k8s.io"
and event.action in (
"io.k8s.core.v1.namespaces.get",
"io.k8s.core.v1.namespaces.list",
"io.k8s.core.v1.nodes.get",
"io.k8s.core.v1.nodes.list",
"io.k8s.core.v1.pods.get",
"io.k8s.core.v1.pods.list",
"io.k8s.core.v1.configmaps.get",
"io.k8s.core.v1.configmaps.list",
"io.k8s.core.v1.serviceaccounts.get",
"io.k8s.core.v1.serviceaccounts.list",
"io.k8s.authorization.rbac.v1.roles.get",
"io.k8s.authorization.rbac.v1.roles.list",
"io.k8s.authorization.rbac.v1.rolebindings.get",
"io.k8s.authorization.rbac.v1.rolebindings.list",
"io.k8s.authorization.rbac.v1.clusterroles.get",
"io.k8s.authorization.rbac.v1.clusterroles.list",
"io.k8s.authorization.rbac.v1.clusterrolebindings.get",
"io.k8s.authorization.rbac.v1.clusterrolebindings.list"
)
and source.ip is not null
and client.user.email is not null
and not to_string(source.ip) in ("127.0.0.1", "::1")
and not client.user.email like "system:kube-*"
and not client.user.email like "system:gke-*"
and not client.user.email like "system:node:*"
and not client.user.email like "system:serviceaccount:kube-system:*"
and not client.user.email like "system:serviceaccount:gke-managed*"
and not client.user.email in (
"system:apiserver",
"system:addon-manager",
"system:kubestore-collector",
"gcp:kube-bootstrap",
"system:serviceaccount:security:trivy-operator"
)
and not client.user.email like "system:serviceaccount:flux-system:*"
and not client.user.email like "system:serviceaccount:argocd:*"
and not client.user.email like "system:serviceaccount:argocd-system:*"
and not client.user.email like "system:serviceaccount:cattle-turtles-system:*"
and not client.user.email like "system:serviceaccount:*:palette-manager"
| eval Esql.time_interval = date_trunc(1 minute, @timestamp),
Esql.resource_kind = case(
event.action in ("io.k8s.core.v1.namespaces.get", "io.k8s.core.v1.namespaces.list"), "namespaces",
event.action in ("io.k8s.core.v1.nodes.get", "io.k8s.core.v1.nodes.list"), "nodes",
event.action in ("io.k8s.core.v1.pods.get", "io.k8s.core.v1.pods.list"), "pods",
event.action in ("io.k8s.core.v1.configmaps.get", "io.k8s.core.v1.configmaps.list"), "configmaps",
event.action in ("io.k8s.core.v1.serviceaccounts.get", "io.k8s.core.v1.serviceaccounts.list"), "serviceaccounts",
event.action in ("io.k8s.authorization.rbac.v1.roles.get", "io.k8s.authorization.rbac.v1.roles.list"), "roles",
event.action in ("io.k8s.authorization.rbac.v1.rolebindings.get", "io.k8s.authorization.rbac.v1.rolebindings.list"), "rolebindings",
event.action in ("io.k8s.authorization.rbac.v1.clusterroles.get", "io.k8s.authorization.rbac.v1.clusterroles.list"), "clusterroles",
event.action in ("io.k8s.authorization.rbac.v1.clusterrolebindings.get", "io.k8s.authorization.rbac.v1.clusterrolebindings.list"), "clusterrolebindings",
null
),
Esql.is_rbac = case(
event.action in (
"io.k8s.authorization.rbac.v1.roles.get",
"io.k8s.authorization.rbac.v1.roles.list",
"io.k8s.authorization.rbac.v1.rolebindings.get",
"io.k8s.authorization.rbac.v1.rolebindings.list",
"io.k8s.authorization.rbac.v1.clusterroles.get",
"io.k8s.authorization.rbac.v1.clusterroles.list",
"io.k8s.authorization.rbac.v1.clusterrolebindings.get",
"io.k8s.authorization.rbac.v1.clusterrolebindings.list"
),
1,
0
),
Esql.is_layout = case(
event.action in (
"io.k8s.core.v1.namespaces.get",
"io.k8s.core.v1.namespaces.list",
"io.k8s.core.v1.pods.get",
"io.k8s.core.v1.pods.list",
"io.k8s.core.v1.nodes.get",
"io.k8s.core.v1.nodes.list"
),
1,
0
)
| stats
Esql.unique_resources = count_distinct(Esql.resource_kind),
Esql.rbac_event_count = sum(Esql.is_rbac),
Esql.layout_event_count = sum(Esql.is_layout),
Esql.enumerated_resources = values(Esql.resource_kind),
Esql.enumerated_namespaces = values(orchestrator.namespace),
Esql.enumerated_resource_names = values(gcp.audit.resource_name),
Esql.event_outcome_values = values(event.outcome)
by client.user.email, source.ip, user_agent.original, Esql.time_interval
| where Esql.unique_resources >= 5
and Esql.rbac_event_count > 0
and Esql.layout_event_count > 0
| keep Esql.*, client.user.email, source.ip, user_agent.original
Framework: MITRE ATT&CKTM
-
Tactic:
- Name: Discovery
- ID: TA0007
- Reference URL: https://attack.mitre.org/tactics/TA0007/
-
Technique:
- Name: Container and Resource Discovery
- ID: T1613
- Reference URL: https://attack.mitre.org/techniques/T1613/