ADR-040 - Zot Registry Retention and Boundary (keep the third-party lab/bundle registry; registry selector stays reserved)

Superseding note (2026-08-03, ADR-041): the owner ruled that zot is demo/development-lab material only — NOT part of the release installation — and that the Stella Registry, Stella’s own product registry service, is built to back the stella+oci://registry selector and “take care of all” product-side registry roles including the meta-masking facade. Under that ruling: D1 (zot retained as third-party infrastructure) and D3 (registry stays reserved) are superseded; D4’s anonymous-path mechanics survive but their subject becomes the Stella Registry; §R1.3’s role statement is superseded (zot’s end state is demo integration, not Stella-artifact host — the stella-infra zot is only the interim plugin-publish target until the ADR-041 R6 cutover). D2’s boundary invariants, D5’s namespace/plan model, D6, and §R1.2’s destination classification survive and are carried forward by ADR-041. The inventory, measurements, and cost record below remain the authoritative point-in-time evidence base and are not rewritten.

Context

The owner asked (2026-08-03, verbatim intent): “Can we delete zot? What is the zot infrastructure about? If it is for plugins and CLI, then we probably could create some anonymous repo path on the internal registry.” Ledger round 93 had already surfaced the companion architecture question: how — if at all — to provision the reserved stella+oci://registry backend selector (the internal primary image registry, the layer System of Record, which MaskedImageRefResolver fails closed on by design).

Three registry-shaped things exist today, and they are three different roles:

SurfaceWhat it isAuth posture
registry.stella-ops.org + mirrors.stella-ops.orgVendor distribution on separate infrastructure (nginx + registry:3.0.0): the CLI images, the compact vuln-db, and the full release image estate — measured 2026-09-15, 50 repositories including all 45 Stella-owned v1.0.0-RC1 product images, anonymously pullable. Scope ratified then: this row previously read “CLI images, compact vuln-db”, which had stopped describing the surface, and docs/modules/registry/registry-topology.md already documented “customer estates pulling release images by digest” as the intent. The licence obligation follows the surface — every image published here carries the licence it is granted under, enforced by tools/scripts/validate/check-licence-publications.sh and recorded in docs/legal/busl/image-publications.sha256 (SPRINT_20260825_016 LRS-17).anonymous pull live, htpasswd push
registry.stella-ops.local (zot v2.1.3, devops/compose/docker-compose.stella-infra.yml:126)Lab/dev + evaluation-bundle OCI registry — infrastructure alongside PostgreSQL/Valkey/SeaweedFS/Rekorbearer-token via the registry token service (B1, 2026-08-03); no anonymous path
StellaOps.Agent.Registry (in-tree C#, ~2,170 lines)Agent-side pull-through/BundleOnly masker on deploy targets — loopback-only, default OFFagent trust

A full dependency sweep (2026-08-03, excluding frozen QA evidence under docs/qa/feature-checks/runs/** and archives) found 13 live consumers of zot — 7 push, 6 pull — plus the token-service auth plane:

ConsumerUsePush/PullClass
Scanner worker OCI publish (RegistryHostAliasMapper + SbomOciPublishStageExecutor; wired in docker-compose.stella-services.yml:1227)SBOM/reachability referrers attached to scanned imagesPUSHProduct feature, lab-pointed config (PublishOciMetadata defaults false in code)
stella plugin sign-releasePluginBundleOciPublisher; consumed by DeployPluginPullExecutionPluginsigned plugin bundles as digest-pinned OCI artifactsPUSH (publish) / PULL (deploy)Product feature; --registry is a required argument with no default — zot appears only in help text and docs
Registry token service (src/Registry/StellaOps.Registry.TokenService) + gateway realm route ^/api/registry/tokenmints 5-min plan-scoped registry JWTsneither (auth plane)Product service whose only current subject is zot
Release bundle (devops/release/bundle/)evaluation on-ramp: “Push a test image here to scan it without an external registry” (.env.example:71)PUSH+PULLProduct delivery (customer evaluation)
compliance-golden-path CI lane (.gitea/workflows/)manifest/digest preflight + scan legsPULLLive CI, scaffolding
Build co-production proof, reachability-live-go, agents-targets e2e trio, sprint-017 smoke (devops/, tools/scripts/)fixture image pushes + referrers readsPUSHLab/CI scaffolding
EF5 QA proofs, os-analyzer verify, customer script examples, setup.sh health probe (blocking), CLI local-integration bootstrap manifestfixture pulls / probesPULLLab/dev scaffolding

Load-bearing measurements behind this decision:

  1. Nothing in src/ production code resolves registry.stella-ops.local at runtime. Every source hit is help text, OpenAPI example: values, a UI wizard prefill, a lab-bootstrap manifest, or unit-test literals. The product features that touch zot are all registry-agnostic (config- or argument-driven).
  2. The expensive capability is PUSH. Seven consumers use the distribution-spec push protocol (chunked/monolithic blob upload sessions, manifest PUT, tag lists) and three of them additionally depend on the OCI 1.1 Referrers API (build co-production verification, customer attach-referrers scripts, Scanner referrer dual-write).
  3. The in-tree C# registry has no push path. OciRegistryEndpoints maps GET/HEAD /v2/ manifests+blobs only — verified by grep 2026-08-03: zero PUT/POST routes. It is a digest-verified pull-through/bundle cache, not a registry a docker push can target.
  4. registry:2.8 (the second lab registry, docker-registry/oci-registry aliases, docker-compose.integrations.yml:243) is not a substitute: it lacks the Referrers API (GenericOciRegistryContainer.SupportsReferrersApi => false) and has no bearer auth, which would orphan the token service.
  5. A namespace gap exists: the documented plugin publish target registry.stella-ops.local:5000/stellaops/plugins is covered by no plan in devops/etc/registry-token.yaml (stella-ops/public/* and stella-ops/enterprise/* only). Once bearer auth is enabled, plugin pushes and pulls 401 with no plan able to grant them.
  6. The release bundle has drifted from the hardened posture: its registry-token env still carries a wildcard Plans__0__Repositories__0__Pattern: '*' with pull+push, the old api://registry audience shape, and Signing__KeyPath: /app/etc/certs/kestrel-dev.pfx — a dev TLS certificate doing duty as the token-signing key (devops/release/bundle/docker-compose.yml:3185-3192).

Decision

D1 - Zot stays, classified as third-party infrastructure

Refinement R1 (2026-08-03, same day — owner follow-up “zot theoretically is just stella ops and cli, nothing more”): the role list below was too generous. Role (i) “lab/dev registry for fixtures, e2e lanes, and CI” is not a legitimate zot role — the artifact-ownership classification in §R1 shows every fixture/e2e consumer stores a customer-application stand-in, which belongs on the external-registry simulator family, not on Stella’s own registry. Zot’s roles narrow to: (i) Stella’s own artifacts — plugin bundles and the plan-model distribution namespaces (stella-ops/public/*, stella-ops/enterprise/*); (ii) the evaluation-bundle registry, where it legitimately plays a dual role (Stella artifact store + the evaluator’s stand-in customer registry through the integrations path); (iii) the registry-token service’s subject. The keep verdict is unchanged; see §R1 for the evidence, the referrer-plane ruling, and the re-scoped rejected alternatives.

The zot registry is retained deliberately as a third-party infrastructure component of the same class as PostgreSQL, Valkey, SeaweedFS, and Rekor: something the installer provides so the platform has a real OCI distribution-spec registry (push protocol, Referrers API, GC, bearer auth) without Stella owning that conformance surface. Its roles are exactly three: (i) lab/dev registry for fixtures, e2e lanes, and CI (revoked by R1 — reassigned to the external-registry simulator family); (ii) the evaluation registry shipped in the release bundle; (iii) the plan-governed publish/pull target for plugin bundles in lab and evaluation estates. Zot is Apache-2.0 and pinned by digest in the bundle; no license-gate work is triggered.

D2 - Boundary invariants (what keeps zot deletable later)

Under these invariants, replacing or deleting zot is a compose + docs change, never a code change — which is the honest answer to “can we delete zot”: yes, mechanically, at any time; but nothing is gained today and real capability (push, referrers, the bundle evaluation on-ramp) would have to be rebuilt or abandoned.

D3 - stella+oci://registry remains RESERVED and is not backed by zot

The internal-primary selector stays fail-closed. Provisioning it is a product commitment — Stella becoming the layer System of Record — and no consumer in the inventory needs that today; the ADR-033 router re-introduction trigger (≥ 2 live backends × ≥ 2 consumers) is likewise unmet. Backing the reserved selector with the lab zot would silently promote lab infrastructure into product identity. Revisit only when a product requirement demands central layer custody (e.g. per-release masking pulls that agent pull-through cannot serve); the token-service plan model transfers unchanged to whatever backing is chosen then.

D4 - The “anonymous repo path” is a token-service plan, not new infrastructure

The capability the owner asked for already exists, shipped fail-closed: the token service can mint anonymous pull-only tokens (the Docker Hub/GHCR pattern) when anonymousPlan names a plan; the shipped config deliberately leaves it unset, pinned by ShippedRegistryTokenConfigTests. Concretely:

Enabling the path is a config flip on an existing service. No new deployable, no new database, no new conformance surface — which is why deleting zot to get it would have been backwards: the anonymous path never required deleting anything.

D5 - In-flight token-service work is KEPT unchanged; the plugin namespace reconciles before bearer enablement

The B1/B1c work (zot bearer auth, the disabled anonymous capability, registry.token.issue scope catalog + seed, the smoke script, the gateway realm route) is kept as-is: zot is its subject, and the plan/token model is the proving ground for any future product registry (D3). One gap must close before the bearer-auth forcing function flips: the canonical plugin-bundle namespace moves under the single stella-ops/ root — stella-ops/plugins/<id>, token-gated via a dedicated publisher/consumer plan (plugin bundles are product content; signing + digest pinning provide integrity, plans provide distribution control). Plugins that become public distribution move under stella-ops/public/plugins/** and ride D4. Docs and CLI help examples follow. Executed via SPRINT_20260803_002.

D6 - The second lab registry is out of scope and stays

registry:2.8 (docker-registry/oci-registry.stella-ops.local, plus the per-provider htpasswd fixtures and CI mock registries) is test scaffolding for the Integrations connector surface and the auto-SBOM CI lane. It is retained as fixtures, is not part of the product boundary, and gains no auth plane. The stale docs that conflate its address with registry.stella-ops.local (which is zot) are corrected by the hygiene sprint.

R1 - Artifact-ownership refinement (2026-08-03): what zot stores vs what merely stands in

The owner’s model to test: masking customer registries is the internal registry’s job (stella+oci://registries/<slug> + agent pull-through), and zot’s legitimate content is Stella Ops’ own artifacts and the CLI — nothing more. The classification below confirms it: reclassified by what is actually stored, almost every fixture consumer is using zot as a stand-in for a customer’s registry.

R1.1 - The referrer-plane ruling (the sharpest question, settled by code)

Two readings were possible for Scanner’s SBOM/reachability referrer publishing: (i) the external attach targets wherever the subject image lives (zot = lab stand-in for the customer registry), or (ii) the meta plane should own referrers centrally and external publishing is a split-brain metadata plane. Both planes exist by design, and reading (i) is what the external leg means. There is no split-brain defect:

Ruling: the meta plane is the referrer home (reading (ii)'s intent, already true); the external attach is deliberate opt-in interop with the subject’s registry (reading (i)), which makes the lab’s 127.1.1.5 → zot alias-with-publish-on config zot playing a customer registry — the exact misuse the owner named. The fix is relocation of the lab target, not a code change. One adjacent gap for the record: Attestor’s OCI attach path targets external registries only and never writes the internal plane, so ADR-033’s “attestation referrers” listing for meta is aspirational today.

R1.2 - Per-consumer classification (consumer → stored → whose → verdict → destination)

ConsumerWhat is actually storedWhose artifactVerdictDestination
stella plugin sign-releaseDeployPluginPullsigned plugin-bundle tar; manifest+blobs, no referrers, no fallback tag (PluginBundleOciPublisher sets no SubjectDigest; the fetcher does manifest-GET-by-digest + one blob GET)Stella’s ownlegitimatestays on zot
Release bundle zotplugin bundles + the evaluator’s test imagesStella’s own + evaluator’slegitimate dual role — the scan-target half goes through the integrations path (registries/<slug>), i.e. the bundle’s zot is the evaluator’s registered customer registry, exercising the product planestays; document the dual role
Registry token service + smoke + setup.sh probeauth plane / health for zotlegitimate (follows zot)stays
Scanner dual-write external leg (compose alias 127.1.1.5→zot, publish=true)SBOM/reachability referrer manifests attached next to the scanned subjectthe subject image’s owner (customer plane)customer-registry stand-inlab: external-registry simulator; production: the customer’s integrated registry (opt-in); internal SoR stays the meta plane
reachability-live-go fixturedeliberately vulnerable Go app (pinned GO-2022-0969)customer-app stand-inmisplacedsimulator
Build co-production proof (e2e-lab/build-coproduction-*)synthetic multi-language scratch source sample + its referrerscustomer plane (build→push→attach is the customer workflow)misplacedsimulator (referrers-capable)
agents-targets e2e trio (stellaops/deployment-e2e-sample etc.)alpine/openssl sleep-forever sample apps — the stellaops/ namespace is misleading; no Stella code insidecustomer-app stand-insmisplacedsimulator, registered as a registries/<slug> integration (masked-ref path)
sprint-017 build smoke (e2e-lab/build-docker-smoke)digest-pinned alpine marker imagecustomer stand-inmisplacedsimulator
compliance-golden-path CI lanescans the frozen stellaops/deployment-e2e-sample fixture (the trio’s artifact)customer stand-inmisplacedsimulator
preseed-lab-imagepulls stellaops/demo-vuln-nginx / demo-patched-nginx — vulnerable demo fixtures under the stellaops/ namespacecustomer stand-insmisplaced (namespace doubly so)simulator + rename off stellaops/
EF5 QA proofs, os-analyzer verify (atlas/pas-policy, library/vulnnginx)vulnerable fixture imagescustomer stand-insmisplacedsimulator
customer attach-referrers scriptsreferrers onto a customer image; zot only as the default valuecustomer planewrong defaultdefault must require an explicit target; examples name the customer’s registry
CLI local-integration-bootstrap entry local-docker-registryregisters zot as a DockerHub-provider customer registrythe literal misuse, in one line of JSONendpoint → docker-registry.stella-ops.local:5000 (simulator)
.gitea dockerhub-mirror prefix exampleupstream Docker Hub cache namespace on zotupstream’smisplaced examplethe CI proxy cache (registry:2.8.3 proxy)
NEW FIND — compiled-in default: ScannerWorkerOptions.ScanPipelineOptions.DefaultRegistry = "stellaops-registry:5000" (+ AllowInsecure = true)the lab zot’s container host baked into product code as the default registry for port-less refsD2 boundary violation (no compiled-in hosts)config-required / fail-closed default; sprint task

R1.3 - Zot’s reduced role, and the claims that move with it

Once the misplaced consumers relocate, zot holds: plugin bundles + the plan-model distribution namespaces (stella-ops/public/*, stella-ops/enterprise/*) + the evaluation bundle’s dual role. The owner’s theory — “zot is just Stella Ops and the CLI” — is confirmed as the target state; today’s estate deviates only in the enumerated places, and every deviation is a config/default move, not a code change (except the DefaultRegistry compiled-in default).

Three context claims re-scope with this:

  1. The Referrers API is no longer load-bearing on zot itself — the plugin path is manifest+blobs+digest only. Referrers become load-bearing on the simulator role (co-production proof, dual-write forcing, attach-referrers), so the rejection of registry:2.8 re-scopes: unfit as the simulator for referrer lanes; for the Stella-artifact store the blocker is bearer auth, not referrers. The cheapest referrers-capable simulator is another zot instance under a fixture alias (e.g. customer-registry.stella-ops.local, open/dev auth, own volume) — zot-the-image stays in the estate either way; what narrows is what THE Stella instance stores.
  2. The 7-of-13-pushers argument recomputes but the verdict holds: after relocation zot’s own push surface is plugin publish + bundle evaluation pushes — still the real distribution-spec push protocol, still not something the pull-only agent registry or plain CAS can serve, and the simulator family still needs a push+referrers registry. Nothing about the reduction revives options (a) or ©.
  3. The anonymous path is strengthened: reduced to Stella’s own artifacts, zot’s content is exactly the class eligible for stella-ops/public/** anonymous pull (D4). And relocation collapses the bearer-enablement credential matrix from thirteen consumers to roughly three (plugin publisher, plugin-pulling deploy agents, bundle evaluation flows) — fixture lanes move to the open simulator and need no zot credentials at all — which makes relocation the single best pre-flip simplifier for the SPRINT_20260722_024 S7 forcing function.

Consequences

Rejected alternatives