1
ADR – 001 Store Images Are Signed and Verified by Cells
Dmitrii Iurco edited this page 2026-06-11 15:47:54 -04:00

Status: Active | Owner: @roof | Applies to: main (2026-06) | Updated: 2026-06-11

ADR – 001 Store Images Are Signed and Verified by Cells

Context

The service store will accept community-submitted services. A cell installing a store service pulls a container image and runs it with access to the cell network. Without provenance guarantees, two attacks are possible:

  1. Malicious image content. An image that does not match its published source can carry malware onto every cell that installs it.
  2. Tag redirection. A manifest that references a mutable tag (:latest) can be made to pull different content after review, even if the reviewed content was clean.

The store needed a way for a cell to decide, at install time, whether an image is exactly what the PIC publish process produced.


Options Considered

Option A — Trust the registry

Restrict images to git.pic.ngo/roof/* and rely on registry access control.

Rejected because: it protects against outsiders but not against a compromised publish path, a stolen registry credential, or tag mutation. The cell has no way to detect that an image changed after review.

Option B — Digest pinning only

Require every manifest image to be a @sha256: digest.

Rejected as insufficient alone: digests guarantee immutability but not origin. A digest written into a manifest by an attacker still installs cleanly. Digest pinning is kept as a necessary part of the solution.

Option C — Digest pinning + cryptographic signing, verified on the cell (chosen)

Every published store image is digest-pinned and signed with cosign. The cell verifies the signature against a bundled public key before starting any store container.


Decision

Store images must be digest-pinned and cosign-signed, and cells verify them at install time:

  • ServiceStoreManager rejects manifests whose image is not a @sha256: digest.
  • ServiceComposer runs cosign verify against the bundled public key (config/cosign/cosign.pub) before bringing a service up (api/service_composer.py).
  • Behaviour is controlled by the image_verification section in cell_config.json: off | warn | enforce. The default is enforce — an undigested, unsigned, or signature-mismatched image refuses to install. If the verification mode cannot be read (corrupt config), the composer falls back to enforce: verification fails closed, never silently weakens.
  • Signing happens in the publish pipeline (images are signed and their digests written back into manifests before they reach the store index); the private key never exists on a cell.

Rollout was staged: verification shipped warn-by-default first, and the default flipped to enforce once every store image was signed.


Consequences

  • Unsigned or undigested images refuse to install on any cell running the default enforce mode.
  • Service developers must downgrade verification locally. To run images that have not been through the publish pipeline, set image_verification.mode to warn or off in config/api/cell_config.json and restart the API container. See Dev – Build a Store Service.
  • Manifests always reference digests, not tags. The image field in a published manifest is a @sha256: digest, which removes tag-redirect attacks entirely.
  • Only the public key ships with PIC. Compromising a cell yields nothing that can sign new images.
  • There is currently no API route or UI for the verification mode — changing it is a config-file edit. This is intentional friction: weakening verification should not be one click away.