gcp-gcr-push-can-modify-code
storage.objects.create on the GCR artifacts bucket = push/overwrite a container image - code control over the registry and supply-chain poisoning of every consumer that pulls a mutable tag.
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)
The GCR endpoint (gcr.io/PROJECT) is GCS-backed (legacy GCR active, not yet migrated to Artifact Registry backend). Detect by checking artifacts bucket existence and non-empty state, or absence of an Artifact Registry gcr.io-mirror repo.
emit
| source type | Identity |
|---|---|
| target type | ContainerRegistry |
| source | <principal with effective storage.objects.create on the artifacts bucket> |
| target | <ContainerRegistry node for gcr.io/PROJECT> |
| permissions | storage.objects.create |
| conditions | api_enabled |
| state logic | ACTIVE when the principal holds effective storage.objects.create on artifacts.PROJECT.appspot.com (via bucket-level IAM, project-level role binding, or org-level inheritance) AND the gcr.io endpoint is GCS-backed (legacy GCR active). CONDITIONAL(api_enabled) when the Container Registry API is not confirmed enabled (gcr.io endpoint may not be initialised). BLOCKED when a GCS IAM deny policy or VPC Service Controls perimeter denies storage.objects.create to the principal for this bucket. |
Narrative
{principal.name} can push or overwrite container images in the GCR registry for project {project.name} (storage.objects.create on artifacts.{project.name}.appspot.com). Overwriting a mutable image tag injects attacker code into every workload (GKE pod, Cloud Run service, Cloud Build step, or GCE container-mode instance) that pulls that tag - a supply-chain attack with a blast radius equal to all consumers of mutable tags in this registry. container-chains produces the CanExecuteAs edges to each consumer's runtime service account.
Raw rule rules/derived/gcp/gcr.yaml
id: gcp-gcr-push-can-modify-code
emits: CanModifyCode
description: "storage.objects.create on the GCR artifacts bucket = push/overwrite a container image \u2014\
\ code control over the registry and supply-chain poisoning of every consumer that pulls a mutable tag."
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)
- The GCR endpoint (gcr.io/PROJECT) is GCS-backed (legacy GCR active, not yet migrated to Artifact Registry
backend). Detect by checking artifacts bucket existence and non-empty state, or absence of an Artifact
Registry gcr.io-mirror repo.
emit:
source_type: Identity
target_type: ContainerRegistry
source: <principal with effective storage.objects.create on the artifacts bucket>
target: <ContainerRegistry node for gcr.io/PROJECT>
permissions:
- storage.objects.create
conditions:
- api_enabled
state_logic: ACTIVE when the principal holds effective storage.objects.create on artifacts.PROJECT.appspot.com
(via bucket-level IAM, project-level role binding, or org-level inheritance) AND the gcr.io endpoint
is GCS-backed (legacy GCR active). CONDITIONAL(api_enabled) when the Container Registry API is not
confirmed enabled (gcr.io endpoint may not be initialised). BLOCKED when a GCS IAM deny policy or
VPC Service Controls perimeter denies storage.objects.create to the principal for this bucket.
confidence: 0.95
derived_from:
- effective storage.objects.create on artifacts.<PROJECT>.appspot.com (from storage.buckets.getIamPolicy
+ resourcemanager.projects.getIamPolicy + role expansion)
false_positive_note: "(1) On projects that have migrated gcr.io to Artifact Registry backend, this rule\
\ MUST NOT fire; storage.objects.create on the legacy bucket in those projects does not yield push\
\ capability on the active gcr.io endpoint \u2014 the push goes through Artifact Registry (artifactregistry.repositories.uploadArtifacts).\
\ The collection adapter must check which backend is active. (2) storage.objects.create scoped at\
\ PROJECT level (not bucket level) grants write to ALL GCS buckets in the project, not just the artifacts\
\ bucket. Scope the CanModifyCode to the ContainerRegistry node (artifacts bucket), not all buckets.\
\ The linchpin (container-chains image-push-poisons-consumers) fans out only to consumers of this\
\ registry. (3) The supply-chain reach is CONDITIONAL(image_pulled) for each consumer: only workloads\
\ referencing MUTABLE tags (:latest, moving tags, no @sha256 digest pin) are poisoned on next pull/redeploy.\
\ Digest-pinned consumers are NOT poisoned \u2014 container-chains enforces this gate; do not override\
\ it here. (4) Overwriting a tag requires storage.objects.create but NOT storage.objects.delete (GCS\
\ PUT semantics). A principal with only storage.objects.get or storage.objects.list cannot push; only\
\ storage.objects.create is the push gate."
narrative: "{principal.name} can push or overwrite container images in the GCR registry for project\
\ {project.name} (storage.objects.create on artifacts.{project.name}.appspot.com). Overwriting a mutable\
\ image tag injects attacker code into every workload (GKE pod, Cloud Run service, Cloud Build step,\
\ or GCE container-mode instance) that pulls that tag \u2014 a supply-chain attack with a blast radius\
\ equal to all consumers of mutable tags in this registry. container-chains produces the CanExecuteAs\
\ edges to each consumer's runtime service account."