- docs
- Settings and trust
- Route traffic through a corporate mirror
Route traffic through a corporate mirror
Send OCX registry traffic through your organization’s artifact manager, without changing any package identity or digest.
Many corporate and air-gapped networks firewall-block external registries such as ghcr.io, docker.io, quay.io and ocx.sh. The organization runs an artifact manager (JFrog Artifactory, Sonatype Nexus or Harbor) with proxy repositories that cache those upstreams. OCX routes its registry traffic to the mirror, and the canonical package identity and the content-addressed digest stay unchanged.
Map a host to its mirror
Section titled “Map a host to its mirror”The [mirrors] config table maps each host to its mirror endpoint. OCX appends the upstream repository path after the mirror’s repo-key prefix and contacts only the mirror. There is no origin fallback: in a firewall-controlled network, falling through to the internet is the opposite of intent.
# ~/.ocx/config.toml (or $XDG_CONFIG_HOME/ocx/config.toml)[mirrors]"ghcr.io" = "https://artifactory.example.com/ghcr-remote""docker.io" = "https://artifactory.example.com/dockerhub-remote"With this config, ocx package install ghcr.io/owner/tool:1.2 fetches the manifest and blobs from artifactory.example.com/ghcr-remote/owner/tool. The canonical identifier ghcr.io/owner/tool:1.2 never changes.
To confirm the traffic goes through the mirror, block ghcr.io on the machine, or watch the mirror’s access log, and run the install again. It succeeds only when the mirror answers.
A plain string, as above, redirects every kind of traffic OCX sends that host. A host that also serves an ocx-index can be split per role instead. See Route index traffic through a mirror.
Relation to the default registry
Section titled “Relation to the default registry”[registry] default and [mirrors] are independent and compose. Default injection expands a registry-less identifier (for example kitware/cmake:3.28 to ocx.sh/kitware/cmake:3.28) at parse time, before any mirror rewrite. If you also configure a [mirrors] entry for ocx.sh, OCX mirrors that default-injected identifier. A fully air-gapped setup can mirror every registry the project uses, including the default one.
Pinning ocx.sh at a mirror’s registry endpoint has one deliberate side effect. It suppresses the compiled-in index for that namespace, which then resolves as a plain OCI registry through the mirror. A site that routes ocx.sh to its own artifact manager should not start dialling index.ocx.sh, a host it never allow-listed. OCX logs a warning naming the namespace it dropped, because the index’s digest verification and yank gate go with it.
Two limits keep the suppression from firing where it was not meant to:
- It applies only to a config file you control: the compiled defaults, the discovered chain,
--configandOCX_CONFIG. A[mirrors]entry arriving through the[managed]tier orOCX_MIRRORSredirects traffic but cannot suppress the index. Neither a remotely published payload nor an inherited variable may drop a namespace off the verified resolution path and its yank gate. - It applies only to the
registryrole. Theindexrole is keyed on the index endpoint’s own host, so"ocx.sh" = { index = ... }redirects nothing for theocx.shnamespace.
To keep the verified index path and pin the physical registry, name the index explicitly. A written index outranks the compiled-in one and is never suppressed. Redirect the index endpoint itself with the index role on its own host:
[registries."ocx.sh"]index = "https://index.ocx.sh" # explicit: survives the mirror entry
[mirrors]"ocx.sh" = "https://artifactory.corp/ocx-remote""index.ocx.sh" = { index = "https://artifactory.corp/ocx-index" }Lockfile portability
Section titled “Lockfile portability”OCX stores the canonical upstream host and digest in ocx.lock, never the mirror host. A lock file produced behind a corporate mirror is valid on a machine with direct internet egress, and the reverse holds too. The mirror is a transport detail, and the identity of the content is unchanged.
Unpinned tags and the trust model
Section titled “Unpinned tags and the trust model”Installing with an unpinned tag such as kitware/cmake:3.28 makes OCX trust the mirror’s tag to digest resolution. It extends the same trust it gives the origin registry. The mirror could, in principle, map the tag to different content. After resolution, OCX verifies the blob digest against the manifest, so it rejects a tampered blob. The manifest itself still came from the mirror’s tag resolution.
For tamper-proof installs, pin with ocx lock. Once a digest is recorded in ocx.lock, OCX never re-resolves the tag, and the mirror cannot substitute a different manifest undetected.
Publisher-signature verification validates the publisher’s identity independent of the mirror. See Verify what you install.
Auth for the mirror
Section titled “Auth for the mirror”Authenticate against the mirror host, not the upstream. Set OCX_AUTH_<mirror_slug>_*, replacing non-alphanumeric characters with underscores, or run ocx login <mirror-host>. OCX never uses the upstream’s credentials on the read path. Authenticate with a private registry covers both methods.
export OCX_AUTH_artifactory_example_com_TYPE=bearerexport OCX_AUTH_artifactory_example_com_TOKEN="<artifactory-token>"Set mirrors in CI via OCX_MIRRORS
Section titled “Set mirrors in CI via OCX_MIRRORS”For CI or container setups where the command line is not controlled, set mirrors via OCX_MIRRORS instead of a config file. The value is a JSON object:
export OCX_MIRRORS='{"ghcr.io":"https://artifactory.example.com/ghcr-remote"}'Each key is a host being redirected. Each value takes the same union as [mirrors] in the config file. A plain string redirects both the registry and index roles for that host. An object {"registry"?: "...", "index"?: "..."} splits them. A role’s value has the form scheme://host[/repo-key-prefix].
export OCX_MIRRORS='{"ghcr.io":"https://artifactory.example.com/ghcr-remote","index.ocx.sh":{"index":"https://artifactory.corp/ocx-index"}}'OCX_MIRRORS wins over [mirrors] on a per-host, per-role basis. A role present in a host’s entry replaces the config entry for that role only. Roles and hosts it omits still come from [mirrors]. OCX forwards the variable to every subprocess it spawns, so nested invocations such as generated launchers and ocx exec see the same mirror map.
A malformed value is a hard startup error, never an empty mirror map. Falling back to no mirrors would silently route reads to the firewall-blocked origin, which is the failure replace semantics exist to prevent. OCX aborts and names the problem when:
- the value is not valid JSON,
- a per-host value is neither a string URL nor an object with only
registryandindexstring fields, or - a mirror value starts with
http://and the mirror host is not plaintext-eligible, as described under plain HTTP.
Mirror values
Section titled “Mirror values”A [mirrors] value is a string, which redirects both roles for the host, or an object with optional registry and index string fields. A role left out of the object means no redirect for that role, with no fallthrough to the other form. Each role’s value is scheme://host[/repo-key-prefix], and OCX builds the registry pull path as <mirror-host>/<prefix>/<upstream-repo>:
# Artifactory path-based routing (repository-path method):# ghcr.io/owner/tool:1.2 → artifactory.example.com/ghcr-remote/owner/tool:1.2[mirrors]"ghcr.io" = "https://artifactory.example.com/ghcr-remote"
# Subdomain / host-only form (empty prefix):# ghcr.io/owner/tool:1.2 → ghcr-remote.artifactory.example.com/owner/tool:1.2[mirrors]"ghcr.io" = "https://ghcr-remote.artifactory.example.com"- Artifactory. The registry-role value is the Docker/OCI pull path,
<host>/<repo-key>. It is not the admin REST path (/artifactory/api/docker/<repo-key>), which is not a valid pull URL. Use what you would givedocker pullororas pull. - Nexus 3.83+ path routing uses the same
<host>/<repo-key>shape with the repo key alone:"ghcr.io" = "https://nexus.corp/docker-proxy". The legacy/repository/<name>form is not used. Deployments that expose each repository on its own port use the host-only form (https://nexus.corp:8082). - Harbor follows
<host>/<project-name>/<image>for its project-level proxy caches. - Docker Hub
library/images. OCX appends the repository path verbatim and does not expand short names. Use the fully qualified form (docker.io/library/alpine) so the mirror URL resolves to<mirror>/<prefix>/library/alpine. - Index role. The same
scheme://host[/path-prefix]shape applies toindex, which covers every root, index-object and catalog fetch of a namespace resolved through the ocx-index protocol. A project resolving packages through index.ocx.sh sets it on that host:"index.ocx.sh" = { index = "https://artifactory.corp/ocx-index" }. The two roles are path-disjoint, so one object entry can point both at the same host. See Route index traffic through a mirror. - Scheme default. A value with no
scheme://prefix ("nexus.corp/docker-proxy") defaults tohttps. - Plain HTTP. A role value starting with
http://needs the mirror host to be plaintext-eligible. Setinsecure = trueon its own[registries.<name>]entry, or list the host inOCX_INSECURE_REGISTRIES. The gate applies to both roles, and OCX never silently downgrades TLS. If the host is in neither, OCX exits before any network activity, and the error names both ways to allow it. - Malformed values. An empty string, or an object whose present fields are all empty, is a hard error when the mirror map is resolved. A role with a non-string value (
{ registry = 5 }) is a parse error naming the host and field. An entry left with no recognized role contributes no mirror.
Tiers merge [mirrors] field-wise. OCX normalizes every value to its two roles first. A higher tier that sets only index for a host leaves a lower tier’s registry role untouched, and a plain string sets both. OCX_MIRRORS overrides on the same per-host, per-role basis. A [mirrors] entry at the system scope locks per role (see system locks).
| Concern | Behavior |
|---|---|
--offline | No network activity at all, and mirrors are not consulted |
--remote | Mutable lookups (tag list, tag to digest resolution) hit the mirror, not the origin |
push | Not mirror-redirected. The canonical upstream host is contacted, because proxy repositories are read-only and redirecting a push would fail confusingly |
ocx index catalog, ocx index update | For a namespace resolved through the ocx-index protocol, every fetch honors that host’s index role only. Against a plain OCI registry mirror, the catalog lists only repositories a proxy-type mirror has cached, which is a registry-side constraint |
A fetch routed through a mirror that fails names both the physical reference it fetched and the upstream host the mirror is configured for. You can diagnose the error without cross-referencing [mirrors] by hand.
OCX refuses any mirror answer to a manifest request that is not a manifest. Examples are an HTML portal page and a login redirect from a dead proxy repository. It does so before digest verification, with a data error naming the response’s content type. Without that check, the HTML body would reach the digest comparison and report a digest mismatch for a problem that was never about digests.
Per-registry settings
Section titled “Per-registry settings”Two more tables shape how a registry is reached. [registry] default (default ocx.sh, overridden by OCX_DEFAULT_REGISTRY) is the literal identifier prefix prepended to bare identifiers: kitware/cmake:3.28 becomes <default>/kitware/cmake:3.28. It is a literal prefix, the same string used as a [registries.<name>] key, and OCX never dereferences it through another field.
[registries.<name>] holds per-registry settings. The key is the registry’s identifier prefix, matched exactly. It is the same host[:port] string package identifiers carry, such as ocx.sh, ghcr.io or registry.corp:5000. The plural form mirrors Cargo’s convention and avoids a TOML collision with the singular [registry]. Three fields are defined (index, trusted_hosts and insecure), and OCX ignores unknown fields in an entry.
index selects the resolution protocol for the namespace (reference). An entry that sets it resolves through the ocx-index protocol against that base URL. An entry without it, or no entry, resolves as a plain OCI registry.
- There is exactly one protocol per namespace, and the index is authoritative for the whole registry. OCX never falls back from the index protocol to plain OCI tags, or the reverse.
- A name the index has no root for fails with an error naming that index. An
index.ocx.shoutage blocksocx.sh/...resolution, while other namespaces are unaffected. - The compiled defaults ship
[registries."ocx.sh"] index = "https://index.ocx.sh". A higher tier can restate it with another base URL, andindex = ""is the off-switch that resolves the namespace as a plain OCI registry. indexneeds no<dialect>+URL-scheme prefix, because OCX has one index wire dialect. Omittingindexdoes not clear an inherited value, since tiers merge field-wise. Only an explicitindex = ""does.- A
file://base reads the index straight off disk. See Serving and consuming the tree.
trusted_hosts (array of hostnames or CIDR blocks) is the SSRF escape hatch for the namespace’s physical hosts (reference). Before OCX follows an index root’s oci://<host>/<repository> pointer, it refuses any host that resolves to a private, loopback, link-local or cloud-metadata address. The pointer is remote-controlled data, so the guard is default-on and needs no configuration for public registries. Listing a private registry’s host or network here restores access for exactly this namespace.
[registries."ocx.corp.example"] # the domain in the package nameindex = "https://ocx.corp.example"trusted_hosts = ["gitlab.corp.example"] # where the roots point; no portHostnames match without a port, so "registry.corp:5000" never matches. Write "registry.corp". A refusal names the key: list it (bare host, no port) under [registries."ocx.corp.example"].trusted_hosts. No command-line flag widens the trust set, so a lower tier or a CLI override cannot broaden a system-locked entry’s trusted_hosts. The managed tier can distribute the entry (reference). Behind a configured HTTP proxy, OCX judges the list by name alone, because only the proxy can resolve the physical host.
insecure (boolean, default false) contacts the registry over plain HTTP. It is the per-registry spelling of OCX_INSECURE_REGISTRIES, and the two are a union: a host named in either is plaintext-eligible.
[registries."registry.corp:5000"]insecure = true- Matching is exact on
host[:port]and case-sensitive, so an entry forregistry.corpdoes not coverregistry.corp:5001. Write the resolved registry host.docker.ioresolves toindex.docker.ioand is never served over plain HTTP, so OCX accepts no spelling of it. insecure = falsebelow the system tier restates the default and does not revoke a host the environment granted. A system-scopeinsecure = falselocks the host shut against the environment list too. The managed tier can distribute the entry (reference).- The allowance covers transport and nothing else. OCX still refuses an HTTPS registry that answers with a plaintext token-service realm, and an HTTPS request redirected to
http://. A token service on another host or port needs its own entry.
Plain HTTP sends credentials in the clear. Use the entry for a lab or an isolated network, never for reaching a registry across the public internet.