gcp-gcs-enter-project-via-secrets
A cross-project principal that can read a bucket containing credentials for a principal privileged in the bucket's project effectively enters that project.
match
A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.
{'principal': None}
CanReadData {'bucket': None}
{'bucket': None} ContainsSecret {'secret': None}
{'secret': None} CredentialsFor {'identity': None}
where
?principal.project != ?bucket.project # cross-project read
?identity has bindings granting privilege in ?bucket.project
emit
| source type | Identity |
|---|---|
| target type | Project |
| source | ?principal |
| target | <bucket.project> |
| permissions | storage.objects.get |
| conditions | iam_permission resource_policy key_permission |
| state logic | Inherits weakest state from CanReadData (ACTIVE or CONDITIONAL(key_permission) for CMEK), ContainsSecret, and CredentialsFor contributors. ACTIVE when all three contributing edges are ACTIVE and the credentialed identity is confirmed as privileged in the target project; CONDITIONAL(key_permission) when the bucket is CMEK and service-agent grant is unconfirmed. |
Narrative
{principal.name} (project {projectB.name}) can read objects in bucket {bucket.name} (project {projectA.name}), which is classified as containing credentials for {identity.name} that is privileged in {projectA.name} - reading the object yields a pivot into project {projectA.name}.
Raw rule rules/derived/gcp/gcs.yaml
id: gcp-gcs-enter-project-via-secrets
emits: CanEnterProject
description: A cross-project principal that can read a bucket containing credentials for a principal privileged
in the bucket's project effectively enters that project.
match:
- - principal: null
- CanReadData
- bucket: null
- - bucket: null
- ContainsSecret
- secret: null
- - secret: null
- CredentialsFor
- identity: null
where:
- '?principal.project != ?bucket.project # cross-project read'
- ?identity has bindings granting privilege in ?bucket.project
emit:
source_type: Identity
target_type: Project
source: ?principal
target: <bucket.project>
permissions:
- storage.objects.get
conditions:
- iam_permission
- resource_policy
- key_permission
state_logic: Inherits weakest state from CanReadData (ACTIVE or CONDITIONAL(key_permission) for CMEK),
ContainsSecret, and CredentialsFor contributors. ACTIVE when all three contributing edges are ACTIVE
and the credentialed identity is confirmed as privileged in the target project; CONDITIONAL(key_permission)
when the bucket is CMEK and service-agent grant is unconfirmed.
confidence: min(contributing_confidences) * 0.8
derived_from:
- ?principal CanReadData ?bucket
- ?bucket ContainsSecret ?secret
- ?secret CredentialsFor ?identity
false_positive_note: "Low-confidence derivation: ContainsSecret is asserted from object-name heuristics\
\ (e.g. `.tfstate`, `serviceaccount`, `credentials`, `.env`, `kubeconfig`) \u2014 NEVER from reading\
\ object content. Emit with confidence 0.6-0.7 when based on name alone; raise to 0.85+ when object\
\ metadata (content-type, label, annotation) corroborates. The CredentialsFor edge must also be evidenced\
\ or asserted. Do NOT emit when the principal and bucket are in the same project. The credentialed\
\ identity must be privileged in the BUCKET's project, not the reading principal's project."
narrative: "{principal.name} (project {projectB.name}) can read objects in bucket {bucket.name} (project\
\ {projectA.name}), which is classified as containing credentials for {identity.name} that is privileged\
\ in {projectA.name} \u2014 reading the object yields a pivot into project {projectA.name}."