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.
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 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]) — 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."