Serving a Deb / RPM repo from holger — guide
Class:
guide· Function:serve. Architecture:deb-rpm-native-metadata.md. Dead-simple version:deb-rpm-native-metadata-for-idiots. This is the how-to: stand up an APT or dnf repo backed by a.znippyarchive and see the real control/header fields that now land in the index.
What holger does for you
Point holger at an archive that contains bare .deb / .rpm files and it synthesizes the native index the client expects, straight from the packages present — no upstream index required:
- Deb:
dists/{dist}/Release+dists/{dist}/{component}/binary-{arch}/Packages, each stanza carrying realPackage/Version/Architectureplus the authoritativeDepends/Maintainer/Descriptionparsed out of the package's owncontrol.tar.*, with a real SHA256 over the served bytes soapt [trusted=yes]verifies. - RPM:
repodata/repomd.xml+repodata/primary.xml, each<package>carrying the filename NEVRA enriched with the realepochand the<format>block —<summary>,<url>,<rpm:license>,<rpm:provides>,<rpm:requires>— parsed out of the binary RPM header, again with a real SHA256.
If the archive already carries a real index (a byte-exact upstream mirror), holger serves that verbatim instead — passthrough always wins over synthesis.
1 · Put the packages in an archive
Use the agent to pack a directory of packages into a .znippy (see agent-guide). A plain mirror is fine — you do not need the znippy deb_native / rpm_native plugin; the native parse works on the raw package bytes. (If you do build with the plugin, its typed sub-index is used in preference and you get identical fields.)
2 · Wire a repository in holger.ron
Add a repository backed by the archive and expose it, exactly like any other native backend (serve-guide shows the endpoint/storage wiring). The Deb backend registers as repository-znippy-deb, the RPM backend as repository-znippy-rpm.
3 · Fetch the index and confirm the real fields
Debian / APT — the client fetches Release, then the per-arch Packages:
# the dist Release referencing each Packages by SHA256+size
curl -s http://localhost:8081/debrepo/dists/stable/Release
# the synthesized Packages — note the parsed control fields
curl -s http://localhost:8081/debrepo/dists/stable/main/binary-amd64/Packages
A stanza now looks like this (the Depends / Maintainer / Description came from the package's control, not the filename):
Package: hello
Version: 2.10-2
Architecture: amd64
Maintainer: Santiago <santiago@debian.org>
Depends: libc6 (>= 2.14)
Description: friendly GNU greeter
Filename: pool/main/h/hello/hello_2.10-2_amd64.deb
Size: 1234
SHA256: <64-hex>
Point apt at it. A chain view (an archive-less deb repository over the intake chain), and an archive-backed repository (ron_archive_path, lane HOL-SIGN-2), on a box with a repository key serve InRelease and Release.gpg signed with it (lane HOL-SIGNED-APT-DNF): the operator makes the key once on the box with holger repo-key create --config <ron>, and the client pins its public half from /-/repo-key.asc:
curl -fsSL http://holger.example:8081/-/repo-key.asc | sudo tee /etc/apt/keyrings/holger.asc
deb [signed-by=/etc/apt/keyrings/holger.asc] http://holger.example:8081/deb stable main
A box without a key serves an unsigned Release and needs [trusted=yes]:
deb [trusted=yes] http://holger.example:8081/debrepo stable main
then apt update && apt install hello.
RHEL / Fedora / dnf — the client fetches repomd.xml, then primary.xml:
curl -s http://localhost:8081/rpmrepo/repodata/repomd.xml
curl -s http://localhost:8081/rpmrepo/repodata/primary.xml
A <package> now carries the real epoch and <format>:
<package type="rpm">
<name>zlib</name>
<arch>x86_64</arch>
<version epoch="7" ver="1.3.1" rel="2.fc40"/>
<checksum type="sha256" pkgid="YES">…</checksum>
<summary>zlib compression library</summary>
<url>https://zlib.net</url>
<size package="1234"/>
<location href="Packages/zlib-1.3.1-2.fc40.x86_64.rpm"/>
<format>
<rpm:license>Zlib</rpm:license>
<rpm:provides><rpm:entry name="libz.so.1"/></rpm:provides>
<rpm:requires><rpm:entry name="libc.so.6"/></rpm:requires>
</format>
</package>
Add a .repo file and dnf install zlib resolves against it. A chain view or an archive-backed repository on a box with a repository key also serves repodata/repomd.xml.asc, so dnf checks the metadata (repo_gpgcheck=1, gpgkey=http://holger.example:8081/-/repo-key.asc) and the packages against their vendor's key (gpgcheck=1, the key rpm --imported). A box without a key needs repo_gpgcheck=0.
What the repository key signs (lane HOL-SIGN-2)
- Only the index of the repository's own type: deb
dists/<suite>/InRelease(theReleaseclear-signed) anddists/<suite>/Release.gpg, rpmrepodata/repomd.xml.asc, each over the exact bytes the repository serves at that moment. The signature is kept while the document stays the same and is made again when it changes. - Only at a plain Debian suite word (
[A-Za-z0-9._+~-], at most 64 bytes, not.or..). The suite is part of the URL and is written intoSuite:, so any other suite gets no signature. - A chain view leaves its index tree (
dists/,repodata/) out of its set. ARelease,InReleaseorrepomd.xmlthat came through the chain is never served or signed as the view's index; the view always serves the index it synthesized over the AVAILABLE packages. - An archive-backed repository signs the index the archive serves. If the operator packed a mirrored
Releaseinto the archive, the box signs that one: the archive is the operator's statement.
The security review of the signer is in the lane handback hol-sign-review.md (plan repo).
A downstream's key and the chain of trust
Every Holger signs with its own key. A downstream (holger-downstream) is a box like any other: its operator makes its key on it (holger-downstream repo-key create --config <ron>), and its views sign with that key. A downstream never holds its upstream's secret key, and it never serves its upstream's signatures. Its packages reach its views through its own chain (bundle import lands AVAILABLE through intake), and its own synthesized indexes are what it signs.
The client trusts the box it talks to. The console's client setup (Account, MyRepositories, account::clients_for) names this box's key by fingerprint:
# The key to trust: this Holger's repository key, fingerprint 4F1C…9A20 (Holger repository signing key).
# Check that the file holds that key before apt trusts it:
gpg --show-keys /etc/apt/keyrings/apt.asc
On a downstream that is the downstream's fingerprint. A box with no key says so and names holger repo-key create.
How an operator sees the chain of trust. The packages are signed by their vendors; the upstream verified them and its views sign under key U; the downstream verified the upstream's signed bundles and its views sign under key D. Its clients pin D. To see U and D side by side today:
- On each box,
holger repo-key show --config <ron>prints the fingerprint on stderr and the public key on stdout.GET /-/repo-key.ascserves the same public key without a credential. - The console's apt and dnf setup on each box names that box's fingerprint.
How a downstream shows its upstream's key in its sync report. The sync report (holger.grow.v1.DownstreamReport) carries the downstream's own RON summary (DownstreamSummary, signed by summary_proof over summary_text). The two fingerprints belong there, beside the boundary and the repositories:
| Field | Value |
repo_key |
the downstream's own repository key fingerprint (D), or empty when it holds none |
upstream_repo_key |
the fingerprint of the key its upstream served at /-/repo-key.asc the last time it fetched from it (U); empty for a one-way downstream, which never dials |
The rules for these fields:
- Each field adds one
summary \t <field> \t <hex>line tosummary_text, and only when it is not empty. A report from an older downstream then has the same text as before, and its proof still opens (HOL-SYNC-SKEW). - A newer downstream that reports to an older upstream sends fields the upstream does not know. The upstream drops them and its recomputed text no longer matches, so it refuses the summary (never the report or its sync point) until it is upgraded. The upstream is upgraded first, as for every summary field.
- The upstream's Sync board shows, for each downstream: "signs with D; trusts upstream key U". U is checked against the upstream's own key. A mismatch (the downstream pinned an old or a foreign key) is a board alarm with both fingerprints, so the operator sees the break in the chain where it is.
These two fields are not built yet (Holger 3.1.1 ships the per-box key, the console's fingerprint and this procedure). They are the next row on the plan (HOL-SIGN-CHAIN).
Which fields appear now (vs. before)
| Client field | Deb source | RPM source | Before this change |
| name / version / arch | control stanza → filename | header/filename NEVRA | filename only |
| Depends / Requires | control.tar.* Depends |
header REQUIRENAME |
absent |
| Maintainer / Vendor | control Maintainer |
header VENDOR |
absent |
| Description / Summary | control Description |
header SUMMARY |
absent |
| epoch | n/a | header EPOCH |
0 |
| license / url / provides / sourcerpm | n/a | header tags | absent |
| Filename / Size / SHA256 | holger, over served bytes | location / size / checksum |
already real |
Supported codecs & limits
- Deb
control.tarmembers: uncompressed,.gz,.xz,.zst. An unknown codec (e.g..lz4) simply falls back to filename-derived fields — the index still serves. - The control tarball is capped at 16 MiB decompressed and the RPM header at 64 MiB / 65 536 tags; a decompression bomb or a lying header degrades to the filename fallback rather than exhausting memory. Hostile bytes never crash the server — worst case you get the filename-only stanza you had before.