Skip to content

Mirror ingest (Path B)

Bring a third-party image into the internal registry and re-attest it under the enterprise's own key, so that at admission there is exactly one trust root to check regardless of where an image came from.

Flow

$ gapsign ingest -config config.yaml docker.io/library/nginx:1.25 mirror.internal/docker.io/nginx:1.25
ingested mirror.internal/docker.io/nginx@sha256:...
  from: docker.io/library/nginx:1.25
  signed by: mirror-x509
  signature: mirror.internal/docker.io/nginx@sha256:...

gapsign ingest:

  1. Authorizes the destination against the policy (AuthorizeSigner, default-deny) and checks the source registry host against the required allowlist — both before any network call.
  2. Pins the source to a digest, bounds its footprint, and mirrors that exact digest into the destination (content-addressed, digest-verified copy).
  3. Confirms the destination now serves that digest, then signs it and pushes the signature.

Configuration

Ingest is governed and fails closed without it — a policy, a signer identity, and a source allowlist are all required:

backend: cosign-key
signer_name: mirror-x509        # must match a policy rule's signer
backend_opts:
  key: ~/.config/gapsign/mirror.key
policy: ~/.config/gapsign/policy.yaml
sources:                        # SSRF guard — hosts ingest may pull from
  - docker.io
  - quay.io
# max_ingest_bytes / max_ingest_layers cap the mirrored footprint (defaults 2GiB / 256)
# policy.yaml
policies:
  - match: "mirror.internal/docker.io/*"
    signer: mirror-x509
    require: signed

Security posture

  • Digest-pinned. The source is pinned before the copy and that exact digest is what gets signed — the signature binds the content actually mirrored, not whatever a mutable source tag later points to.
  • SSRF-bounded. The source host must be on the sources allowlist (host-boundary matched, so docker.io.evil.com never matches docker.io); enforced before any network call, and an empty allowlist fails closed.
  • Footprint-bounded. A compromised-but-allowlisted upstream cannot exhaust the internal registry: child-manifest count and index depth are capped from the manifest before any child is fetched, and an index child of an unaccountable media type is refused.
  • No shell. All registry I/O is via Go libraries — there is no skopeo subprocess and therefore no argument/option-injection surface. (gapsign fits the same mirror flow; it just does the copy itself.)