Zone Retriever and cascade
A zone is driven by one document: its Retriever. It lists, by name
and version constraint, the recipes the zone should hold, and names the
cookbook to resolve them from. The instance re-reads it at every
synchronization; changing what a zone contains means changing this
document — or publishing a new recipe version that an existing
constraint already covers. The document format is normative on the
recipe-spec site: see the
Retriever specification
(the Retriever kind, recipe.tobby.dev/v1alpha1). A complete
commented example ships in the repository as
examples/retriever.yaml.
Three sources
Section titled “Three sources”retriever.source accepts three forms (FR-010):
| Form | Example | When |
|---|---|---|
| Local file | /etc/tobby/retriever.yaml |
The document is managed with the instance’s configuration. |
| HTTP(S) URL | https://git.example.com/platform/retriever.yaml |
The document lives in a Git repository or any web server — the common GitOps arrangement. |
| OCI reference | oci://registry.example.com/config/retriever:v1 |
The document travels like the content it describes — including across zones, carried by Tobby itself. |
The configured source is reported as configured on the Retriever
administration screen (/admin/retriever, admin role) and its API
mirror GET /api/v1/retriever — alongside the declared relaxed trust
scopes and the effective synchronization interval. The interval override
lives on the same screen (PUT /api/v1/retriever/interval, and DELETE
to return to the configured value); it persists in the state directory,
survives restarts, wins over sync.interval, and is audited as a
sensitive configuration change (FR-094).

At each cycle the instance resolves every listed recipe from the cookbook, verifies its signature against the configured trust roots (FR-033), and reconciles. Version constraints are resolved at each pass; if no published version satisfies a constraint, that entry fails and says so — the other entries carry on. One unpublishable recipe never blocks the zone.
The cascade: connected → restricted → more restricted
Section titled “The cascade: connected → restricted → more restricted”Real topologies chain zones. The upstream zone promotes into its
registry; the downstream zone’s Tobby fetches from that registry
even though the recipes — immutable, signed, bit-exact — keep naming the
origin hosts (docker.io/...). The bridge is source substitution
(FR-036):
# Downstream instanceregistries: substitutions: docker.io: registry.upstream.example ghcr.io: registry.upstream.example
retriever: source: oci://registry.upstream.example/cookbook/retriever:v1Substitution changes only the network endpoint contacted — never the
computed destination path (FR-035). That invariance is what makes the
cascade compose: docker.io/bitnami/wordpress relocates to
<registry>/docker.io/bitnami/wordpress in every zone, however many
hops it crossed, and never degenerates into
reg.zone2/reg.zone1/docker.io/.... Each zone’s registry holds the same
relocated paths under its own host, and each zone’s cookbook — populated
by recipe propagation (FR-034) — is what the next zone’s Retriever
points at. The rule and its rationale are
ADR-0013; the normative grammar (host
canonicalization, port encoding, substitution semantics) is
RECIPE-SPEC §11.5.
What the substitute registry must hold
Section titled “What the substitute registry must hold”A substitution maps an origin host to a base. The instance then appends the full canonical path — origin host included:
effective = <base>/<canonical-host>[_<port>]/<repository-path>
substitutions: docker.io: registry.upstream.example
docker.io/library/nginx → registry.upstream.example/docker.io/library/nginxghcr.io/dexidp/dex → ghcr.io/dexidp/dex (not substituted: as written)The origin host stays in the path on purpose: that is where an upstream Tobby holds the content, so the downstream instance finds it without any translation. The value is therefore a host — plus, at most, the base path the upstream promoted under — and never repeats the origin host.
What to write depends on what fills the upstream registry:
| Upstream registry | Filled by | Substitution value | What the operator does |
|---|---|---|---|
The embedded registry (/v2/) of an upstream Tobby |
That instance’s synchronizations | <upstream host>, or <upstream host>/<storage.basePrefix> when one is set |
Nothing: it already holds the canonical layout. |
| A zone registry an upstream Tobby promotes into | destination.registry |
<registry host>, or <registry host>/<destination.basePath> when one is set |
Nothing beyond the promotion itself: Tobby pushes docker.io/library/nginx to <registry>/docker.io/library/nginx. On Harbor, the top-level project named after the origin host must exist (ADR-0013). |
| A pull-through proxy: Harbor proxy-cache project, Artifactory or Nexus remote repository | The proxy itself, on demand, from the origin | <proxy host> |
Create one proxy per origin host, named exactly after the canonical host (below). |
The three cases converge on the same layout. A zone registry fed by an upstream Tobby gets it for free, because Tobby fetches from the origin without substitution and pushes under the origin prefix. A proxy gets it from its name.
Pull-through proxies: one project per origin host
Section titled “Pull-through proxies: one project per origin host”A pull-through proxy rewrites the other way round: its project name
replaces the origin host. A Harbor proxy-cache project named
dockerhub serves harbor.example/dockerhub/library/nginx from Docker
Hub’s library/nginx. A substitution pointed at that project would ask
Docker Hub for docker.io/library/nginx, which does not exist.
The fix is the proxy maintainer’s, and it is only a matter of naming. Name the project after the canonical origin host. The path Tobby asks for is then exactly the one the proxy expects:
Tobby asks harbor.example/docker.io/library/nginxHarbor reads project "docker.io", repository "library/nginx"Harbor fetches library/nginx from Docker HubThe canonical host is at once the substitution key and the project
name, and the path asked of the proxy is always <proxy>/ followed by
the recipe’s ref (a port written _<port>):
| Origin registry | Substitution key / proxy project | Recipe ref |
|---|---|---|
| Docker Hub | docker.io |
docker.io/library/nginx |
| GitHub Container Registry | ghcr.io |
ghcr.io/dexidp/dex |
| Quay | quay.io |
quay.io/jetstack/cert-manager-controller |
| Kubernetes | registry.k8s.io |
registry.k8s.io/kube-state-metrics/kube-state-metrics |
| Google Container Registry | gcr.io |
gcr.io/distroless/static |
| Google Artifact Registry | europe-docker.pkg.dev |
europe-docker.pkg.dev/acme/apps/api |
| Microsoft Artifact Registry | mcr.microsoft.com |
mcr.microsoft.com/dotnet/runtime |
| Amazon ECR Public | public.ecr.aws |
public.ecr.aws/docker/library/alpine |
| Azure Container Registry | acme.azurecr.io |
acme.azurecr.io/team/app |
| GitLab.com | registry.gitlab.com |
registry.gitlab.com/acme/app/api |
| Self-hosted forge on a custom port | key forge.example.com:5050, project forge.example.com_5050 |
forge.example.com:5050/acme/app |
- Docker Hub is the only special case, and only for clients.
docker pull nginxmeansdocker.io/library/nginx. A recipe always writes the host, andlibrary/for official images.index.docker.ioandregistry-1.docker.iofold intodocker.io(a closed list, ADR-0013), so the single projectdocker.iocovers all three spellings. - A custom port. This is common on self-hosted forges such as
GitLab or Froggit, whose registry often listens on 5050. The key keeps
the
:(forge.example.com:5050). The path encodes it as_(forge.example.com_5050), because a repository name cannot contain:. The proxy project is thereforeforge.example.com_5050. - One host, one project. This holds even for the same vendor:
gcr.ioandus.gcr.io, or two Artifact Registry regions, are two projects. - Check the names. They contain dots, and an underscore when a port is involved. Check that your proxy accepts them before building on this layout.
- Trust does not move. The proxy authenticates to the origin with its own credentials. Tobby authenticates to the proxy: the credentials file and the allowlist name the proxy host. The proxy is no trust anchor either: signatures and pinned digests are verified exactly as if the bytes came from the origin.
Two policies deliberately read different references:
- The registry allowlist (FR-030) and credential lookup apply to the effective host actually contacted — the substitute. That is where the bytes come from, so that is what network policy must name.
- Trust-root scopes (FR-033) match the nominal
refwritten in the recipe. Signed provenance does not change because content was fetched from a closer copy.
Logs record the nominal→effective mapping on every substituted fetch, so an audit can always answer both “what was this content” and “where did these bytes come from”.
Credentials between instances
Section titled “Credentials between instances”A downstream instance authenticates to its upstream like any registry client. Create a dedicated account (or token) on the upstream instance — see authentication and RBAC — with read as its only need, and hand it to the downstream instance through its credentials file (FR-004):
registries: credentialsFile: /etc/tobby-credentials/config.jsonThe file is a standard dockerconfigjson payload; entries are looked up
by the effective host, so the entry names the upstream registry —
registry.upstream.example — not docker.io. It must live outside the
store (secrets never travel on transportable media), and on Kubernetes
it is a mounted Secret the chart wires for you. Write-scoped credentials
follow the same path: pushing to the zone’s destination registry uses
the same credentials file, keyed by the destination host.
The destination side of the same instance is its own configuration
section — destination.registry, plus destination.basePath and
destination.cookbook — deliberately separate from substitutions: one
answers “where do I read from”, the other “where do I promote to”, and
applying a read-side rewrite to a write would publish to a registry
nobody named.
Next: your clusters and hosts consume what landed — connect your clients.