How it works#
imagegate is a standalone Go process. dockerd calls it over
/run/docker/plugins/imagegate.sock, using Docker's authorization plugin
protocol, before and after every API request.
Components#
flowchart TB
cli["docker CLI / API client"] --> dockerd
dockerd -- "AuthZReq / AuthZRes" --> plugin["imagegate · authz handler"]
plugin --> gate["decision engine"]
gate -- "image + container inspect" --> dockerd
gate --> cache[("verdict cache · LRU + bbolt")]
gate --> audit["audit log · metrics"]
gate --> verifier["verifier · sigstore-go"]
verifier -- "referrers, .sig / .att tags" --> registry[("OCI registry")]
verifier -. "trusted root (TUF)" .-> sigstore[("Sigstore")]
Only the verifier talks to the network. imagegate's own inspect calls to
dockerd come back through the plugin as GET requests and are always
allowed.
What is gated#
| Request | What imagegate does |
|---|---|
POST /containers/create |
Full verification of the image digest |
POST /containers/{id}/start, /restart |
Re-checks the container's actual image ID; a cache hit after a create |
| Everything else, and every response | Allowed, never verified |
Classification matches dockerd's router: any /v[0-9.]+ version prefix,
and container names that contain /. A POST that looks like a create,
start or restart but does not parse is denied rather than guessed.
A create, step by step#
sequenceDiagram
autonumber
participant C as docker run
participant D as dockerd
participant G as imagegate
participant R as registry
C->>D: POST /containers/create {Image}
D->>G: AuthZReq
G->>D: GET /images/{ref}/json
D-->>G: Id, RepoDigests
Note over G: normalize ref, pick digest<br/>override? cache hit?
G->>R: referrers + cosign tags for digest
R-->>G: bundles, signatures, attestations
Note over G: verify signature, then signed SBOM<br/>cache verdict
G-->>D: Allow, or deny with reason
D-->>C: container ID, or the denial message
- Parse. Read
Imagefrom the request body. A malformed body is denied withBAD_REQUEST. - Normalize.
nginxbecomesdocker.io/library/nginx:latest. Registry hostnames are lower-cased,:443is dropped (kept on dotless hosts such asregistry:443, which would otherwise read as Docker Hub) and Docker Hub aliases fold intodocker.io. - Resolve the digest. Ask dockerd for the image. A reference pinned
with
@sha256:uses that digest; otherwise the RepoDigests entry for the same repository. If dockerd does not have the image yet, the create is allowed sodocker runcan pull and retry; the retried create is checked. - Overrides. A valid break-glass entry for the digest allows it, logged at high severity.
- Cache. Key is (repository, digest, policy hash). A hit returns the stored verdict.
- Rule. The first rule whose
imagesglob matches the repository. No match denies withNO_RULE. - Signature. Fetch Sigstore bundles from OCI 1.1 referrers (cosign v3),
then legacy
sha256-<digest>.sigtags (cosign v2). Verify with sigstore-go against the rule's identities. - SBOM. The same for in-toto attestations: a DSSE envelope from an
allowed identity, an allowed
predicateType, and a subject equal to the image digest. - Decide. Cache the verdict, write the audit record, answer dockerd.
Verification detail#
flowchart TD
start(["digest + rule"]) --> fetch["fetch referrers,<br/>.sig and .att tags"]
fetch -->|registry unreachable| up["UPSTREAM_UNAVAILABLE"]
fetch -->|digest not in registry| local["LOCAL_IMAGE"]
fetch --> sig{"signature from an<br/>allowed identity?"}
sig -->|none found| nosig["NO_SIGNATURE"]
sig -->|valid, wrong signer| mism["IDENTITY_MISMATCH"]
sig -->|yes| need{"rule requires SBOM?"}
need -->|no| ok["ALLOWED"]
need -->|yes| sbom{"signed SBOM from an<br/>allowed identity?"}
sbom -->|yes| ok
sbom -->|unsigned or untrusted| untr["SBOM_UNTRUSTED"]
sbom -->|none| nosbom["NO_SBOM"]
- Keyless identities need a Fulcio certificate chain to the trusted
root, an embedded SCT (unless
trust.requireSCT: false), a Rekor inclusion proof or signed entry timestamp, and a matching issuer and subject. - Keyed identities verify against a listed public key. A transparency log entry is checked when present; keyed signatures without one (CI without Rekor) verify by key alone.
- Only the digest dockerd resolved counts. On the containerd image store that is the index of a multi-platform image, so sign and attest the index.
Caching and performance#
- One verification per (repository, digest, policy hash). Allows last 24 h and denies 60 s by default; transient failures are never cached.
- Memory LRU (10,000 entries) backed by
/var/lib/imagegate/cache.db, so a restart does not cause a burst of cold verifications. - Concurrent requests for the same digest share one verification. In the tests, 200 parallel creates trigger exactly one.
- The TUF trusted root refreshes in the background, never inline.
| Path | Measured |
|---|---|
| imagegate's own work on a cached create | ~15 µs |
| Cached start, end to end through dockerd | 1–2 ms |
| dockerd's image inspect (containerd store, test VM) | ~20 ms |
| Cold verification, local registry | 20–60 ms |
The image inspect dominates a cached create on the containerd store and is above the spec's 5 ms p99 target there. Measure on your own hosts.