Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

.nornir/deb-rpm-native-metadata-guide.md

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 .znippy archive 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:

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)

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:

  1. On each box, holger repo-key show --config <ron> prints the fingerprint on stderr and the public key on stdout. GET /-/repo-key.asc serves the same public key without a credential.
  2. 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:

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