local-registry 0.14.0
Authentication review and fixes. Credential JSON and Authorization headers are parsed without ambiguous alternatives. Token rotation is serialized across processes sharing the same auth file; losing an observed file fails closed. Public OCI HEAD remains available when credentials are damaged. Access logs use the actual socket peer and the verified identity. See iteration 0.14.0 and the current verification report.
Gateway administrator login. The server consumes Remote-Email and the
comma-separated Remote-Groups from explicitly trusted gateway peers.
Members of registry_admin enter the console automatically. Present headers
have priority; existing Basic/Bearer client authentication applies only when
both identity headers are absent. Configure the direct peer with -auth-proxy IP.
See gateway setup and the exact contract,
iteration 0.13.0 and the
0.13.0 verification report.
Manifest-optional publishing CLI. Feed a finished binary or installation
tree to package, or use publish -input to package, sign and publish in one
call. Set metadata with flags or use the explicit interactive mode; complete
manifests remain supported. Both inputs use the same native packaging and
snapshot publication contracts. See the publishing guide,
iteration notes and
0.12.0 verification report.
./local-registry publish -interactive -input ./rootfs -channel testing
Run this on the trusted publishing host with local registry storage and your
operator-provisioned signing key. The build stays separate; the HTTP server
needs no changes. For packaging alone, use package instead of publish.
Recent image tags and one-call multiarch packages. The Images screen shows the three most recently published tag bindings per repository, with durable publication times that also work when retagging older images. A schema 2 package manifest maps architectures to binaries or installation trees; one command builds all DEB/RPM targets into an atomic bundle. See tag publication semantics, the packaging example and the 0.11.0 test report.
Complete multiarch image imports. Published pull jobs copy the entire OCI index or Docker manifest list, every platform manifest, config and layer directly into the registry. The original digests are preserved, shared blobs are reused, and the destination tag changes only after the complete graph is verified and durable. Imports work without a container engine or backing database. See operation and upgrade notes and the 0.10.0 test report.
Upstream freshness through REST. Your existing scheduler can check a source, import its exact digest and follow the durable run ID. Checks and synchronization have separate timestamps. Older single-platform receipts now request a complete import. See the REST contract and examples.
Podman by default, Docker as fallback. Automatic engine selection prefers Podman. If it is unavailable, Docker is selected and a persistent notice appears on every authenticated console page. Explicit engine settings remain binding; selecting Docker yourself does not show a fallback warning. Helpers follow the same preference and report fallback in their output. See engine configuration and the 0.8.2 test report.
SQLite snapshots from the source host. The new sqlite-backup.py helper
creates and checks consistent online copies, including committed WAL data.
The registry deduplicates 64 KiB blocks before compression and retains complete,
independent snapshots. Recovery verifies the recorded digest and SQLite
integrity before publishing a new file. The registry server needs only its
binary and data directory for this feature; Python/SQLite run on the source
and recovery hosts. See setup and recovery,
measurements and
the 0.8.0 test report.
Native MySQL and MariaDB page backups. Full, differential and incremental
backups now share the database console and durable job system. Dedicated
repositories retain complete chains, verify archive checksums, and prepare
isolated recovery copies through local-registry database-restore. MariaDB
uses a checked checkpoint procedure to reduce repeated page copies after bulk
loads. See configuration and recovery,
measurements and
the 0.7.0 test report.
Native PostgreSQL block backups. The Backups → Databases view runs pgBackRest full, differential and incremental backups, checks WAL archiving, verifies stored backups, and applies dependency-aware retention. Jobs have durable intent, bounded logs and explicit interruption/error states. The acceptance suite restores real PostgreSQL data, tablespaces and a point in time before an intentional deletion. See setup and recovery, database measurements and the 0.6.0 test report.
Private deduplicated backups, locally validated on 2026-09-30. New backups share content-defined chunks and store a complete manifest for each snapshot. Downloads reproduce the exact uploaded bytes; old raw/gzip backups remain readable. The console separates restore size from stored size and can verify an entire backup. See backup operation and format, the measured comparison and the 0.5.0 test report.
Matt's Go registry now accepts built programs, produces DEB/RPM packages and publishes signed APT/DNF repositories. The existing OCI image, managed pull/build and backup paths remain part of the same executable. Go code uses the standard library plus one pinned, vendored BSD-licensed Rabin chunker from restic. Native packaging and signing use the distribution tools. No runtime dependency downloads are required.
The larger Patronage Systems logo remains anchored to the center of the whole page, accounting for the sidebar. It overlaps the header separator with a clear cutout and small corner accents. Stable scrollbar space prevents page-height changes from shifting the layout. The logo is embedded in the HTML; displaying it needs no external requests.
Version 0.4.0 hardened concurrent uploads, OCI publication, cleanup, backups, managed jobs and credential replacement against interrupted operations and storage errors. The package-first console shows interrupted jobs and uncertain credential commits explicitly. See transaction and recovery guarantees and the console guide. Local browser automation now covers desktop and mobile layouts, navigation, credentials, downloads and errors; the full manual accessibility checklist remains separate.
Current verification: 621 Go race-test results passed, no failures;
three GPG integration tests were skipped because this environment denies Unix
sockets. Go vet, the static Linux amd64 build, UI logic and 43 checks through
a real HTTP test gateway passed. Production idgate and a real browser were not
exercised. Historical packaging/database/browser reports remain in docs/ and
are not presented as fresh validation. See the iteration record.
Build and run
Linux, Go 1.24 or newer (the vendored dependency permits offline builds):
CGO_ENABLED=0 go build -trimpath -o local-registry .
./local-registry -listen 127.0.0.1:5000 -root ./registry-data
# For builds and local-only pulls, select the engine installed on this host:
./local-registry -engine podman -root ./registry-data
# Or: REGISTRY_ENGINE=docker ./local-registry -root ./registry-data
./run.sh builds from source and forwards its arguments to the executable.
The included local-registry executable is Linux amd64; rebuild on other Linux
architectures. On Windows use WSL2 with a Linux distribution and keep the data
root in its Linux filesystem. Native Windows builds are not supported because
the process locks use Linux/Unix flock.
Only one HTTP server may own a data root; startup refuses a second writer.
Stop the previous server and back up the complete root before upgrading.
The included Linux binary is statically linked. Registry metadata and backup
manifests are files; the registry requires no backing database. SQLite backup
production runs on a separate source host using the included helper.
The server itself needs no package tools. Optional native database profiles additionally require
pgBackRest for PostgreSQL, or Python 3.10+ and the matching native backup/client
tools for MySQL/MariaDB. Scheduled API jobs can use the included Python helper.
Build/publish hosts need:
- DEB:
dpkg-deb; OpenSnitch source builds also usedpkg-shlibdeps. - RPM:
rpmbuild,rpm,rpmsign,rpmkeys,createrepo_c. - Publishing:
gpg, an explicitly chosen signing key and its GPG home.
For example, on Ubuntu/Debian the tool packages are dpkg-dev rpm createrepo-c gnupg. On Fedora/Rocky they are rpm-build rpm-sign createrepo_c gnupg2;
install Debian packaging tools separately if that host must also build DEBs.
Target-native builders are appropriate for programs with shared-library needs.
Remote deployments use HTTPS at a reverse proxy. For gateway identity, configure
its exact direct peer with -auth-proxy IP and have it overwrite Remote-Email
and Remote-Groups from the authenticated session. registry_admin grants
administrator access; absent headers select the existing client token. See
the gateway contract for trust, forwarding and session details.
For header-free client access, put {"user":"admin","token":"<secret>"} in
<root>/auth.json with mode 0600; -auth-file can select another file. Without a
gateway or token, administration is restricted to a direct loopback peer, a
loopback Host and same-origin browser requests. A configured gateway disables
tokenless bootstrap. X-Forwarded-For never grants access. Unsafe browser requests
must be same-origin; preserve Host, Origin and Sec-Fetch-Site at the proxy.
Header-free CLI clients retain their token behavior. OCI image pulls remain public.
Credential files require the exact token key; user is optional. Duplicate,
unknown, differently cased or null fields are rejected. Usernames are at most
254 UTF-8 bytes, printable, without a colon or surrounding whitespace. Tokens
are at most 4096 visible ASCII bytes without whitespace. Bearer credentials
cannot contain commas; such custom tokens require Basic authentication.
Generate a random token with POST /v2/_auth and {"generate":true} or supply
{"token":"..."}; supplying both is rejected. A missing file permits initial
local bootstrap only until that process has observed an auth file. To clear a
token intentionally, use authenticated direct-loopback DELETE /v2/_auth, which
writes an explicit empty token. Forwarding headers disable bootstrap and token
clearing. Restore an accidentally removed file atomically; after a process
restart, a missing file is indistinguishable from a fresh installation. Configure
-auth-proxy before proxying an installation without a token.
Rotations use a persistent <auth-file>.lock file. Instances sharing credentials
must use the same file path on a filesystem supporting flock; do not delete the
lock while any instance runs. External editors should replace the credential
file atomically and coordinate on that same lock when rotating concurrently.
Container clients negotiate a Bearer token at /v2/, which returns HTTP 401
with a challenge when administrator credentials are configured. The local
/v2/_token exchange issues tokens valid for five minutes: anonymous clients
receive pull access; valid administrator credentials can request repository-
scoped push/delete access. OCI tokens cannot administer the server or mint
broader tokens. Administrator credential rotation immediately invalidates them.
Direct administrator Basic/Bearer API requests remain supported. A health check
requiring HTTP 200 from /v2/ must supply credentials when authentication is on.
An HTTPS-terminating proxy must preserve Host and set X-Forwarded-Proto to https
so the challenge advertises the correct token endpoint. See
OCI authentication for client examples and deployment details.
The embedded console at / puts package repositories first, with separate
views for container images, managed image jobs, backups, enterprise access and
settings. Gateway administrators are recognized automatically. Header-free
users can sign in with the administrator button; their token stays in page
memory and is forgotten on reload. No CDN, font service or JavaScript build
step is required. See the console guide. Managed Docker/Podman pushes receive credentials through
an isolated temporary config removed at completion; remote source credentials
are not changed. The publisher is a local operator command, not an HTTP
endpoint that accepts arbitrary filesystem paths or runs uploaded programs.
1. Describe the program
A manifest is optional. The publishing CLI accepts direct
flags and offers an opt-in interactive mode. For a complete reusable manifest,
copy examples/program.json for one architecture, or examples/program-multiarch.json
for several, and replace the metadata and target declarations. A raw executable is
installed as /usr/bin/<name>; a directory is an installation root containing,
for example, usr/bin/program, usr/share/... and etc/program.conf.
{
"schema_version": 1,
"name": "example-program",
"version": "1.0.0",
"release": "1",
"architecture": "amd64",
"maintainer": "Your Organization <packages@example.invalid>",
"description": "Program description",
"license": "MIT",
"source_date_epoch": 1750000000,
"targets": [
{"format":"deb","distribution":"ubuntu","suite":"noble","dependencies":[]},
{"format":"rpm","distribution":"fedora","suite":"44","dependencies":[]}
]
}
This dual-target example fits a binary whose compatibility with both targets has been established, such as a suitably tested static program. Target labels do not prove ABI compatibility. Dynamic programs require their target libraries, correct dependencies and target-specific execution checks.
| Manifest architecture | DEB | RPM |
|---|---|---|
amd64 |
amd64 |
x86_64 |
arm64 |
arm64 |
aarch64 |
riscv64 |
riscv64 |
riscv64 |
386 |
i386 |
i686 |
ELF architecture is checked against the declaration. This command does not
cross-compile. source_date_epoch must be positive and not in the future;
it normalizes payload timestamps. With fixed inputs and native tool versions,
the DEB and RPM build tests check byte-identical repeatability.
In a supplied manifest, dependencies are required explicitly as an array,
including an empty array when appropriate. Without a manifest, -depends sets
them; omitted declarations produce an empty array and are shown in the CLI plan. RPM also derives ELF library requirements. Regular files
under /etc become Debian conffiles or RPM %config(noreplace) entries.
Special files, setuid/setgid payloads, unsafe names and escaping symlinks are
rejected. Unsupported inputs fail; there is no unsigned publication mode.
Optional source_archive is a clean path relative to the manifest. The archive
is copied into the bundle and bound by its full SHA-256. Optional scripts
contains pre_install, post_install, pre_remove, post_remove, each a
relative local file beginning with #!/bin/sh. These are trusted package
lifecycle scripts that run with the package manager's privileges. Their bodies
are hashed with the input; they are not executed while packaging. Supply
format-appropriate scripts: Debian and RPM lifecycle arguments differ.
2. Build the packages
For an interactive start without a manifest:
./local-registry package -interactive -input ./rootfs
For scripted calls, metadata flags replace the manual JSON step; see flags, defaults and examples. These existing manifest commands also remain supported:
./local-registry package -manifest program.json -input ./program -output ./bundle
# Or use an installation directory:
./local-registry package -manifest program.json -input ./rootfs -output ./bundle
For several architectures, use schema 2 with inputs instead of architecture:
{
"schema_version": 2,
"name": "example-program",
"version": "1.0.0",
"release": "1",
"inputs": {
"amd64": "dist/amd64/program",
"arm64": "dist/arm64/rootfs",
"riscv64": "dist/riscv64/program",
"386": "dist/386/program"
},
"maintainer": "Your Organization <packages@example.invalid>",
"description": "Program description",
"license": "MIT",
"source_date_epoch": 1750000000,
"targets": [
{"format":"deb","distribution":"ubuntu","suite":"noble","dependencies":[]},
{"format":"rpm","distribution":"fedora","suite":"44","dependencies":[]}
]
}
./local-registry package -manifest program-multiarch.json -output ./bundle
This builds eight native packages with one call. Include only the architectures
you need (one to four); all declared targets apply to every input. Each input is
either an already-built executable or an installation directory. Paths are clean,
relative to the manifest directory, and must stay within it even through parent
symlinks. Input roots themselves must be regular files or directories. The current
working directory does not change their resolution. Schema 2 rejects -input and
the single architecture field; schema 1 continues to require both. Dependencies,
metadata, lifecycle scripts and the optional source archive are shared. The
packager does not compile or run foreign binaries and needs no emulator.
RPM architecture is selected with rpmbuild --target; the spec does not impose
a host-compatible BuildArch constraint. See the
RPM build-target documentation.
The installed native packaging tools must support the requested target architecture.
The output directory must be new and outside every input tree. All targets must
succeed before it appears. It contains native packages, package-manifest.json,
package-receipt.json and the optional source archive. The receipt binds the
exact staged file paths/modes/bytes and scripts, the manifest, and every output
package. Schema 2 adds input_sha256, one content hash per architecture; its
source_sha256 binds those sorted architecture/hash pairs using the framed
multi-architecture-inputs-v2 domain. Publication verifies the complete
architecture/target matrix and rejects missing, duplicate or unexpected entries.
One publish call publishes all architectures in the same signed snapshot.
Tool versions and target execution evidence should accompany a release.
A signed ZIP is a later authenticated transport for this bundle. ZIP ingestion and handoff-signature verification are not implemented in this iteration; a ZIP is not silently treated as a program or a trusted directory. Payload packing and repository signing already have separate, explicit contracts.
3. Publish atomically
Provision a signing key through your key-management process. The publisher requires the complete primary fingerprint and a dedicated GPG home. Its agent must be able to sign non-interactively (for example, an unlocked key on the isolated publishing host). Private keys and passphrases never enter the bundle, repository or subscriber configuration.
./local-registry publish -root ./registry-data -bundle ./bundle \
-gnupghome /secure/repository-keys -key FULL_PRIMARY_FINGERPRINT \
-channel testing
Repeat the same command with community or enterprise to promote the same
artifacts. Publication checks native metadata against the receipt, hashes,
source provenance and the selected key. A version/release/architecture/target
identity cannot change bytes in another channel. Promotions reuse signed RPM
bytes. To change package content, increment the version or release.
Publication holds an interprocess writer lock, prepares a complete snapshot,
signs and verifies native metadata, syncs files and atomically switches
packages/current. Failed preparation leaves the active snapshot unchanged.
DNF fetches metadata and its detached signature separately; a publication
between those requests can require a client refresh. Atomicity applies to
the filesystem commit, not an entire multi-request client transaction.
Package files, APT by-hash indexes and DNF metadata from earlier publications
remain available to clients already downloading. Do not edit snapshots in place.
| URL | Content |
|---|---|
/apt/<channel>/<distribution>/dists/<suite>/... |
APT Release/InRelease and indexes |
/apt/<channel>/<distribution>/pool/... |
DEB payloads |
/dnf/<channel>/<distribution>/<suite>/<rpm-arch>/... |
Signed RPMs and repodata |
/keys/repository.asc |
Repository public key |
/sources/index.json and .asc |
Signed binary-to-source mapping |
/sources/<archive> and .asc |
Corresponding source and signature |
APT uses SHA-256 indexes, Acquire-By-Hash, signed InRelease/Release.gpg and
a 14-day Valid-Until. Re-publish an existing bundle at least weekly to refresh
its metadata if no new version is released. Scheduling belongs to the operator's
existing CI/scheduler. DNF uses signed RPMs and signed repomd.xml; its ordinary
metadata refresh setting is not an equivalent cryptographic freshness deadline.
The signing key is pinned on first publication. Key rollover and snapshot retention/garbage collection are not implemented. Disk use grows with retained releases; old files must not be removed while clients may still reference them. The existing OCI GC does not touch package snapshots. Back up the whole storage root and signing-key material separately.
Old manually populated <root>/apt and <root>/dnf mirrors are no longer served.
They are not deleted or silently imported: generate and publish bundles through
the new contract. Existing OCI image paths and legacy backup objects remain
readable. New backup data uses the separate private chunk layout described in
BACKUPS.md; copy both manifests and chunks when backing up the registry.
4. Configure clients once
The examples below use an operator-owned HTTPS endpoint repo.example.org.
Download /keys/repository.asc, check its full fingerprint through an
independently trusted channel, and install it as
/etc/apt/keyrings/registry.asc or /etc/pki/rpm-gpg/REGISTRY.asc (mode 0644).
A key fetched from the same repository without independent verification does
not establish initial trust.
Ubuntu 24.04 example, /etc/apt/sources.list.d/registry.sources:
Types: deb
URIs: https://repo.example.org/apt/community/ubuntu
Suites: noble
Components: main
Signed-By: /etc/apt/keyrings/registry.asc
sudo apt-get update
sudo apt-get install example-program
sudo apt-get upgrade
Fedora 44 example, /etc/yum.repos.d/registry.repo:
[registry]
name=Registry community
baseurl=https://repo.example.org/dnf/community/fedora/44/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/REGISTRY.asc
metadata_expire=1h
sudo dnf install example-program
sudo dnf update
Keep the target distribution's normal repositories enabled to resolve library and daemon dependencies. The registry does not mirror the base operating system.
Subscriptions and commercial model
testing and community are public. enterprise requires an unexpired,
per-customer Basic credential on every GET and HEAD, including localhost.
Credentials grant downloads only. Administrator tokens cannot substitute for
subscriber credentials. Source archives and their index remain public after
expiry; installed programs do not perform license checks.
./local-registry subscriber -root ./registry-data -user customer-001 \
-expires 2027-09-28T00:00:00Z
# Rotate: repeat the command. Revoke:
./local-registry subscriber -root ./registry-data -user customer-001 -revoke
Issuance prints the new random token once. Only its SHA-256 and expiry are stored
in packages/subscriptions.json (0600); HTTP reads use a username index and a
cached configuration. Atomic replacement applies rotations without restart.
A COMMIT_UNCERTAIN failure can mean a new token is already active; the issuing
response still provides that token once. Preserve it and inspect current state
before retrying. The CLI prints that response and exits nonzero. This is local
provisioning, not billing, host enrollment or a payment check.
For APT enterprise access, change the URI channel and create a root-readable
/etc/apt/auth.conf.d/registry.conf (0600):
machine https://repo.example.org/apt/enterprise
login customer-001
password <issued-token>
For DNF, change community to enterprise and add username=customer-001 and
password=<issued-token> to the repo file; make that file 0600. Use HTTPS;
credentials never belong in repository URLs or CI logs.
COMMERCIAL.md explains Proxmox's separation of open-source licenses, repository channels and paid maintenance/support, and proposes the corresponding model here. The supplied registry did not declare a software license; this iteration does not invent one for Matt's code.
OpenSnitch and checks
examples/opensnitch/README.md contains the exact
CLI source, original Fedora CLI RPM, build/repack procedures, licenses and
provenance. deliverables/ contains the generated unpublished, unsigned
package bundles for Ubuntu 24.04 amd64 and Fedora 44 x86_64, ready for publish.
Fedora execution, daemon interaction and other architecture runtime behavior
remain unverified; see the evidence before declaring a supported target.
go test -race -v ./...
go vet ./...
python3 test/crash_recovery.py --binary ./local-registry
python3 test/backup_chunks.py --binary ./local-registry --output /tmp/registry-backup-checks
python3 test/backup_chunks.py --binary ./local-registry --output /tmp/registry-backup-benchmark --benchmark
node test/ui_logic.mjs
python3 test/publishing_cli.py --binary ./local-registry --results /tmp/registry-cli-checks
bash test/e2e_pulls.sh
python3 test/oci_clients.py --binary ./local-registry --engine podman
python3 test/oci_clients.py --binary ./local-registry --engine docker
# Strict native gate on a disposable Linux host with APT, DNF and signing tools:
sudo test/packages.sh /tmp/registry-test-results
The native gate builds a second unrelated program at two versions, signs and publishes both, installs/upgrades it with real APT/DNF, and checks tamper rejection. A missing prerequisite or failed step is a nonzero result. Some optional native Go integration tests skip when a tool is absent or GPG cannot run; their skip status must not be counted as passing the strict release gate. New tests clean up their keys, processes and temporary roots. Legacy shell tests are retained for the original OCI scenarios; focused OCI regression coverage is also in Go. The local 0.5.0 run completed the full native gate and all Go tests with zero skips. The previous 0.4.3 validation remains in its historical evidence directory. Docker requires a running daemon for its real-client test; the test builds a scratch image without downloading a base.