ADR-033 - Unified Registry Access (stella+oci:// masked refs + registry gateway)
- Status: Accepted (binding for the unified registry access implementation stream)
- Date: 2026-06-22
- Owners: Lead architect / ReleaseOrchestrator + Integrations + Scanner + AirGap
- Supersedes: none
- Relates to: ADR-031, ADR-032, ADR-004
Context
Stella Ops currently addresses container images through several overlapping paths: deployment helpers assemble real registry references, registry integrations are catalogued by connector/integration state, the internal OCI metadata registry is reached through Scanner-owned endpoints, and air-gap hosts do not have a single pull mechanism when the target host has no direct registry reachability.
The result is a contract gap:
- No single Stella-owned image reference. UI, audit, release creation, and deployment code can talk about the same image using different strings.
- Real registry hosts leak into identity. External hosts, internal metadata endpoints, and future agent-local transport are routing coordinates, not image identity.
- Digest policy is not represented by the naming scheme. Release deployment must remain digest-pinned even when operators enter or browse tag-shaped refs.
- Credential routing must remain uniform. Registry credentials must flow through the in-house secret-provider/authref model, never inline plaintext.
- No direct-registry hosts need a real pull path. Docker cannot be taught an arbitrary
stella-ops/...namespace; Stella must resolve a masked ref to a Docker-valid reference before compose, Docker.DotNet, or deploy plugins see it.
The unified secret provider work already established the pattern for routing opaque URLs to configured backends. Registry access follows that pattern: a single injected gateway parses a Stella-owned URL and dispatches to the concrete registry backend for the selected scheme segment.
Decision
D1 - Digest is identity; masking is routing and presentation
Stella Ops adopts the opaque masked image reference:
stella+oci://<backend-selector>/<object-path>[:tag][@sha256:<digest>]
The masked reference is the stored, displayed, and audited Stella identity string. It is not Docker-pullable. Server-side deployment code must resolve it to a real Docker-valid reference before compose, Docker.DotNet, agent tasks, or native deploy plugins consume it.
The sha256 digest remains the machine identity. Tags may be accepted for browse or human-entry flows, and a tag may be retained for display, but release deployment is pinned to @sha256:<64 lowercase hex>. If both tag and digest are present, the digest wins for every machine path.
D2 - Four backend selectors are reserved by the URL scheme
The stella+oci:// backend selector is one of:
registry- internal primary image registry System of Record. This selector is reserved for forward-compatible layer storage. It can be described and shown as the canonical internal form, but layer pull is fail-closed until that store is provisioned.meta- internal OCI metadata/referrer registry. This addresses SBOM, reachability, attestation, and deployment-decision referrers by subject digest. It is metadata-only, not an image-layer registry.registries/<slug-or-guid>- an external integrated registry. The token is an immutable integration slug when present, with GUID fallback for existing rows. The registry host is folded out of the object path and resolved from the tenant-scoped integration record.bundle- air-gap or deployment-agent-served content. This resolves to a loopback OCI endpoint on the target host or through a deploy-session tunnel.
object-path is the canonical repository key. Real hosts, metadata gateway prefixes, integration tokens, and loopback agent prefixes are routing coordinates and must not become the referrer subject key.
D3 - IImageRegistryGateway is the registry router (SUPERSEDED — see note below)
WS-A Superseding note (2026-06-23): The
IImageRegistryGatewayrouter (RoutingImageRegistryGateway) and its startup probe (RegistryGatewayStartupProbe) have been deleted. The unified router is deferred until there are ≥ 2 live backends with ≥ 2 active consumers — at that point the router re-introduction trigger is: a second backend kind (beyondInternalMetadata) must be fully implemented AND wired into two distinct production call-sites. Until then the single-backend complexity is unjustified.Current live resolution path:
- Masked-ref resolution is performed by
IMaskedImageRefResolver(implemented byMaskedImageRefResolver), registered viaAddMaskedImageRefResolver.- The metadata backend is a standalone
IMetadataReferrerClient(implemented byMetadataRegistryGateway), registered as a singleton in the same extension.- DI registration:
services.AddMaskedImageRefResolver(configuration)— replaces the formerAddRoutingImageRegistryGateway.The historical design (router + adapters + startup probe) is preserved below for reference; it is not compiled and not deployed.
ReleaseOrchestrator owns the registry access abstraction in StellaOps.ReleaseOrchestrator.Deployment.Registry.
IImageRegistryGateway is implemented as RoutingImageRegistryGateway, a router holding one inner adapter per configured backend kind. It dispatches by the parsed StellaRegistryRef.BackendKind:
- registry -> RegistryBackendKind.InternalPrimary- meta -> RegistryBackendKind.InternalMetadata- registries/<token> -> RegistryBackendKind.ExternalIntegrated- bundle -> RegistryBackendKind.AirgapAgent
The router mirrors the current URL-routed secret-provider architecture: many configured backends can exist at once, and the reference chooses the backend. An unknown selector, malformed reference, unconfigured backend, unreachable backend, or unsupported reserved backend fails closed.
D4 - Credentials compose through the secret provider
External registry credentials are addressed by secret references such as authref://... or builtin://... and resolved through the routing ISecretProvider. Registry credentials must not be stored in masked image refs, deployment bundles, logs, descriptors, or audit payloads as plaintext.
Internal metadata, internal primary, and agent-local flows use service identity or agent trust appropriate to the deployment path rather than per-registry inline secrets.
D5 - Resolve before Docker; never hand Docker a masked ref
Docker determines whether the first path segment is a registry host by syntax such as . or : or localhost. A user-facing string like stella-ops/registry/image:v1.0 would be treated as a Docker Hub namespace, not a Stella registry. The stella+oci:// scheme prevents that ambiguity.
Every transport path must resolve masked refs server-side to real refs before Docker or compose sees them. The real ref must contain a Docker-valid host and a digest pin for deploy flows.
D6 - No-direct-registry hosts pull through a loopback agent registry
For targets flagged for agent registry pull-through or air-gap deployment, the resolved real reference rewrites the host portion to the deployment agent’s local OCI endpoint while preserving the original digest:
127.0.0.1:<agentPort>/<canonical-repo>@sha256:<digest>
For connected pull-through, the agent fetches by digest from the upstream registry using credentials resolved through ISecretProvider, verifies the digest fail-closed, caches the content-addressed object, and streams it to Docker. For air-gap, the agent serves the same /v2/ shape from an imported bundle-backed content store. If the origin integration token is needed for pull-through routing, the loopback path may carry it as a routing prefix, but the content-store key and decision-referrer subject remain the bare canonical object-path@digest.
Loopback is preferred because Docker permits plain HTTP pulls from 127.0.0.0/8 without daemon-wide insecure-registries configuration. Remote SSH deployment uses a reverse tunnel so the target still pulls from its own loopback address. Registry mirrors are rejected for this purpose because Docker mirrors do not rewrite arbitrary non-Docker-Hub hosts.
Consequences
- Release creation, deployment, UI, audit, and offline evidence share one Stella reference shape while preserving Docker-compatible transport strings.
- Digest-pinned release behavior becomes explicit in the URL contract: masked refs may carry tags, but deployed refs are pinned by sha256 digest.
- External registry hostnames no longer define identity. The tenant-scoped integration token and persisted release tuple disambiguate registries sharing a host.
- The internal metadata registry is addressed through the same abstraction as image registries without pretending it stores image layers.
- The
registryandbundleselectors can exist before all transport backends are complete, but reserved operations must fail closed and be visible during startup probing. - The loopback agent-registry path gives no-direct-registry hosts a Docker-native pull surface without daemon mirror assumptions or daemon restart requirements.
- Operators and support can show both URLs: the masked Stella ref as primary identity and the real host/repo digest ref as the actual transport endpoint.
Rejected alternatives
- Use real registry URLs everywhere. Rejected: audit and UI identity would change when routing changes, and external host collisions would remain ambiguous.
- Use a bare
stella-ops/...Docker namespace. Rejected: Docker would route it as a Docker Hub namespace unless a real registry host is present. - Rely on Docker registry mirrors for no-direct-registry targets. Rejected: daemon mirrors only cover Docker Hub behavior and do not rewrite arbitrary hosts such as Harbor, GHCR, Quay, or private registries.
- Select one registry backend globally per host. Rejected: one release can reference internal metadata, external registries, and agent-served content at the same time. The router model matches the accepted secret-reference routing pattern.
- Put tenant IDs in the masked URL path. Rejected: tenancy is enforced during resolution from the caller context. Encoding tenant in the path would create copy/paste hazards and multiple URL identities for the same digest.
- Treat tag-only refs as deployable. Rejected: tags are mutable. Tags must be resolved to a digest and frozen before deployment.
- Make the metadata backend a layer registry. Rejected:
metais for OCI referrers, attestations, and decisions. Image-layer storage belongs toregistryor the agent/bundle content store.
