image-push-poisons-consumers

Pushing/overwriting an image in a registry poisons every workload that pulls it - code control propagates from the registry to each consuming Compute/KubernetesWorkload.

derived any cloud emits CanModifyCode

match

A conjunctive graph pattern. Variables (?x) bind node ids; every clause must hold.

{'principal': None} CanModifyCode {'image_store': None} {'consumer': None} ContainsResourceReference {'image_store': None}

where

node_type(?image_store) in [ContainerRegistry, ArtifactRepository] node_type(?consumer) in [Compute, ContainerService, ContainerTask, KubernetesWorkload, Container, VirtualMachine, ServerlessFunction, BuildWorker] reference_kind(?consumer -> ?image_store) == runs_image

optional

Present → upgrade the emitted state; absent → downgrade (not a hard requirement).

{'consumer': None} ContainsResourceReference {'image_store': None}

emit

source typeIdentity
target typeKubernetesWorkload
source?principal
target?consumer
permissions<registry push, e.g. ecr:PutImage | AcrPush | artifactregistry.repositories.uploadArtifacts>
conditionsimage_pulled
state logicACTIVE only when the consumer will ACTUALLY pull the pushed content: the reference is a MUTABLE, re-pullable tag (matched(optional[0]) — a moving tag like :latest, no @sha256 digest pin, registry tags not immutable) AND a pull/redeploy/restart occurs (imagePullPolicy Always, or the workload is (re)scheduled). CONDITIONAL(image_pulled) when no re-pull is established (the pod already runs the old image and won't restart). BLOCKED / does-not-fire for a DIGEST-PINNED consumer (image referenced by immutable @sha256 digest) or an IMMUTABLE-tag registry — overwriting a tag cannot change what a digest-pinned consumer runs. Inherit weakest with the contributing CanModifyCode over the registry: a CONDITIONAL/BLOCKED push (registry policy deny on PutImage / AcrPush, immutable repo) caps or blocks this fan-out.

Narrative

{principal.name} can push/overwrite an image in {image_store.name} ({perm:CanModifyCode}); {consumer.name} pulls that image and executes its layers, so the push injects attacker code into {consumer.name} - one registry write fans out to every workload that pulls the poisoned image.

Raw rule rules/derived/container-chains.yaml

id: image-push-poisons-consumers
emits: CanModifyCode
description: "Pushing/overwriting an image in a registry poisons every workload that pulls it \u2014 code\
  \ control propagates from the registry to each consuming Compute/KubernetesWorkload."
applies_to:
- '*'
match:
- - principal: null
  - CanModifyCode
  - image_store: null
- - consumer: null
  - ContainsResourceReference
  - image_store: null
where:
- node_type(?image_store) in [ContainerRegistry, ArtifactRepository]
- node_type(?consumer) in [Compute, ContainerService, ContainerTask, KubernetesWorkload, Container, VirtualMachine,
  ServerlessFunction, BuildWorker]
- reference_kind(?consumer -> ?image_store) == runs_image
optional:
- - consumer: null
  - ContainsResourceReference
  - image_store: null
emit:
  source_type: Identity
  target_type: KubernetesWorkload
  source: ?principal
  target: ?consumer
  permissions:
  - <registry push, e.g. ecr:PutImage | AcrPush | artifactregistry.repositories.uploadArtifacts>
  conditions:
  - image_pulled
  state_logic: "ACTIVE only when the consumer will ACTUALLY pull the pushed content: the reference is\
    \ a MUTABLE, re-pullable tag (matched(optional[0]) \u2014 a moving tag like :latest, no @sha256 digest\
    \ pin, registry tags not immutable) AND a pull/redeploy/restart occurs (imagePullPolicy Always, or\
    \ the workload is (re)scheduled). CONDITIONAL(image_pulled) when no re-pull is established (the pod\
    \ already runs the old image and won't restart). BLOCKED / does-not-fire for a DIGEST-PINNED consumer\
    \ (image referenced by immutable @sha256 digest) or an IMMUTABLE-tag registry \u2014 overwriting a\
    \ tag cannot change what a digest-pinned consumer runs. Inherit weakest with the contributing CanModifyCode\
    \ over the registry: a CONDITIONAL/BLOCKED push (registry policy deny on PutImage / AcrPush, immutable\
    \ repo) caps or blocks this fan-out."
  confidence: min(contributing_confidences) * 0.9
  derived_from:
  - ?principal CanModifyCode ?image_store
  - ?consumer ContainsResourceReference ?image_store (runs_image)
  false_positive_note: "The judgment is (a) the reference is a genuine run-image relation, not a recon\
    \ mention, and (b) the consumer will actually PULL the overwritten content. DIGEST PINNING is the\
    \ primary weakener: a workload that pulls `repo@sha256:...` (or a registry that enforces immutable\
    \ tags / content digest) is NOT poisoned by overwriting a tag \u2014 do NOT emit ACTIVE, treat the\
    \ fan-out to that consumer as blocked. A tag pushed but never re-pulled (no redeploy, imagePullPolicy\
    \ IfNotPresent with the image already cached) stays CONDITIONAL(image_pulled). Scope the push to the\
    \ exact repository/tag the consumer resolves \u2014 a write to a DIFFERENT repo/tag does not reach\
    \ it. This edge feeds rule 1b (and can-execute-as execute-as-via-code-modify) on each consumer's ExecutesAs;\
    \ the escalation consequence is produced there, not here. Where the consumer is a CI runner, cicd-chains\
    \ consumes this CanModifyCode \u2014 do not re-derive the buildspec execute-as here."
  narrative: "{principal.name} can push/overwrite an image in {image_store.name} ({perm:CanModifyCode});\
    \ {consumer.name} pulls that image and executes its layers, so the push injects attacker code into\
    \ {consumer.name} \u2014 one registry write fans out to every workload that pulls the poisoned image."
move · open · esc close