gcp-gcr-cross-project-push
GCR artifacts bucket grants storage.objects.create to a principal from another project - cross-project supply-chain trust allows external actors to inject images into this project's registry.
match (effective permission)
{
"action": "storage.objects.create",
"resource_type": "storage.googleapis.com/Bucket"
}
where
resource.name matches 'artifacts.<PROJECT>.appspot.com' (the GCR artifacts backing bucket, not an arbitrary GCS bucket)
principal.project_id != PROJECT (the external principal belongs to a different GCP project, e.g., a cross-project SA: X@OTHER-PROJECT.iam.gserviceaccount.com)
The effective permission includes storage.objects.create on the artifacts bucket.
emit
| source type | ResourcePolicy |
|---|---|
| target type | ExternalIdentity |
| source | <ResourcePolicy node for the artifacts.PROJECT.appspot.com bucket IAM> |
| target | <ExternalIdentity node for the cross-project principal> |
| permissions | storage.objects.create |
| conditions | resource_policy |
| state logic | ACTIVE when the cross-project principal has effective storage.objects.create on the artifacts bucket (binding confirmed in bucket IAM policy) AND the external project is collected (the principal is a real SA, not a deleted member). CONDITIONAL(resource_policy) when the binding exists but the external principal's liveness or project status is unconfirmed. BLOCKED when org policy constraints/iam.allowedPolicyMemberDomains restricts the bucket IAM to project/org members only (the binding is org-policy-denied, not effective). |
Narrative
The GCR artifacts bucket for project {project.name} grants storage.objects.create to {external_principal.name} from another project ({external_project}). This external principal can push or overwrite images in this registry, injecting code into workloads that pull mutable tags - a cross-project supply-chain trust that extends the attack surface to the external project.
Raw rule rules/derived/gcp/gcr.yaml
id: gcp-gcr-cross-project-push
emits: CrossProjectTrust
description: "GCR artifacts bucket grants storage.objects.create to a principal from another project \u2014\
\ cross-project supply-chain trust allows external actors to inject images into this project's registry."
match_effective_permission:
action: storage.objects.create
resource_type: storage.googleapis.com/Bucket
where:
- resource.name matches 'artifacts.<PROJECT>.appspot.com' (the GCR artifacts backing bucket, not an arbitrary
GCS bucket)
- 'principal.project_id != PROJECT (the external principal belongs to a different GCP project, e.g., a
cross-project SA: X@OTHER-PROJECT.iam.gserviceaccount.com)'
- The effective permission includes storage.objects.create on the artifacts bucket.
emit:
source_type: ResourcePolicy
target_type: ExternalIdentity
source: <ResourcePolicy node for the artifacts.PROJECT.appspot.com bucket IAM>
target: <ExternalIdentity node for the cross-project principal>
permissions:
- storage.objects.create
conditions:
- resource_policy
state_logic: ACTIVE when the cross-project principal has effective storage.objects.create on the artifacts
bucket (binding confirmed in bucket IAM policy) AND the external project is collected (the principal
is a real SA, not a deleted member). CONDITIONAL(resource_policy) when the binding exists but the
external principal's liveness or project status is unconfirmed. BLOCKED when org policy constraints/iam.allowedPolicyMemberDomains
restricts the bucket IAM to project/org members only (the binding is org-policy-denied, not effective).
confidence: 0.9
derived_from:
- cross-project member with storage.objects.create in artifacts.<PROJECT>.appspot.com bucket IAM
false_positive_note: "Verify the external principal is live (the service account exists in the other\
\ project, not a dangling reference to a deleted SA \u2014 a deleted SA with retained IAM binding\
\ is a confused-deputy risk, not an active trust). Cross-organization principals (from a different\
\ org) are higher severity than same-org, cross-project ones. Hierarchy-chains enter-project-via-cross-project-trust\
\ rolls this up to CanEnterProject for the consuming project; do not re-derive that here."
narrative: "The GCR artifacts bucket for project {project.name} grants storage.objects.create to {external_principal.name}\
\ from another project ({external_project}). This external principal can push or overwrite images\
\ in this registry, injecting code into workloads that pull mutable tags \u2014 a cross-project supply-chain\
\ trust that extends the attack surface to the external project."