Holger — Idiot Guide, version 2
Class: idiot. No jargon, nothing assumed, every step spelled out. Version 1 (docs/idiot_guide.pdf, 2026-07) explained what Holger is and how to lock its front door. This version shows the setup Holger is built for, in its two modes, and the application watcher, both of them click by click. If you know the RON, read the manual instead.
1. The one thing to understand first
Holger is a warehouse for software packages, for places that cannot or must not fetch them from the internet. Programmers' tools (cargo, pip, npm, mvn, apt, podman) are pointed at Holger and think they talk to the public registry. Holger only hands out what has been checked and approved.
The setup you want has two Holgers:
- The internet Holger. It stands where the internet is. It pulls packages from the public registries, checks every one of them (known vulnerabilities, malware, look-alike names, secrets, licences, signatures), lets two people approve them, and keeps the list of known vulnerabilities fresh. It also watches your applications (section 5).
- The on-prem Holger. It stands inside your network. It takes the approved packages from the internet Holger and serves them to your build systems. It never talks to a public registry.
How the approved packages travel from the first to the second is the mode. There are two, and you pick one:
| mode 1: streaming | mode 2: CDS (airgap) | |
| in one sentence | the on-prem Holger fetches live from the internet Holger over an encrypted tunnel | nothing is connected; signed bundles are carried one way, by a diode or a person |
| when | your network may have one outbound tunnel to one machine | your network may have no connection at all |
| what crosses | signed bundles and the sync manifest, over WireGuard | the same signed bundles, as files |
| what the on-prem Holger dials | its one upstream, nothing else | nothing, ever |
Both modes rest on the same three things on the internet Holger:
- The intake chain. Every package is named exactly (
pkg:cargo/smallvec@1.6.0), walked by the scanners in a sandbox, and approved by two people who are not the one who brought it in. A scanner that cannot judge says "unknown", and "unknown" means the package waits; it is never let through because nobody could check it. - The one gate. Every way of writing a package into Holger, whatever door it comes through, passes one function. A package cannot land in a served repository without an intake record, a scan and an approval, unless you wrote in the config that it may.
- The CVE data. The list of known vulnerabilities. The internet Holger fetches it daily. An airgapped Holger gets it as a signed bundle, shipped on its install medium and carried in later.
2. Mode 1: streaming
2.1 The internet Holger
What you type (one machine with internet, Rust installed):
cargo install holger --features online,intel-fetch # the server: pulls from the internet, fetches the CVE data
cargo install holger-ui # the web console
cargo install holger-cli # the command-line tool
holger start # writes ~/.holger/holger.ron and starts
The first start writes a config file, holger.ron, and a first sign-in link for the console. Open the console in a browser, enrol your passkey (your fingerprint or security key): you are the admin. Then the config, in a text editor:
- Say where the packages come from. The
pull_through:block names the public registries, one line each:
pull_through: Some((
sources: [
(name: "pypi", kind: python, url: "https://pypi.org/simple/"),
(name: "npmjs", kind: npm, url: "https://registry.npmjs.org/"),
(name: "crates-io", kind: crates, url: "sparse+https://index.crates.io/"),
],
)),
-
Say who approves. In the
auth:block, give two people the dutySecurityApproverand the person who imports the dutyImporter. Two approvers is the default; the importer can never approve what they imported. -
Say where the CVE data comes from.
intel: Some((db_dir: "/var/lib/holger/intel", offline: false)). Holger then fetches OSV, NVD and KEV at boot and every day, and the popular-names lists it needs to catch look-alike names. The console's Overview shows one line, "CVE data: refreshed … 0 days old", green. Yellow means older than a week. Red means none: then every package waits. -
Restart Holger (
holger start --config holger.ron).
What you click: in the console, Security shows the queues. A package somebody asked for appears under Incoming (being checked), then under Awaiting approval. Open it, read what the scanners found, press Approve. When the second approver has pressed too, the package moves to Available and can be served. A package the scanners refused sits under Blocked with the reason; a package nobody could judge yet (no CVE data for its ecosystem, for example) stays under Incoming marked unknown, and is checked again.
2.2 The on-prem Holger
What you type (a machine inside your network):
cargo install holger-downstream --features fetch
What you type, on the internet Holger, as a Repository Admin (there is no console button or CLI verb for this yet; it is one HTTP door):
curl -fsS -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://holger.example.com/-/sync/downstreams/invite?name=onprem" > onprem.invite.json
Holger answers a signed invite (it lives 7 days and carries the tunnel's pre-shared key). Carry onprem.invite.json to the on-prem machine, and name it in that machine's holger.ron: invite: Some("/etc/holger/onprem.invite.json") in the sync: block's upstream entry, with boundary: Fetch. Start it. From then on it is paired: it fetches every new signed bundle over WireGuard (udp/51830 with the pre-shared key) or mutual TLS, from its one upstream, and verifies each one (the signatures of the two approvers and the release signer, every file's hash, the signed evidence, and that it is newer than the last). It dials nothing else. The details of pairing are in chapter 6.
2.3 Point the build tools at it
On every developer machine and CI runner, point the tool at the on-prem Holger. One line each; chapter 3 has them all.
pip config set global.index-url https://holger.example.internal/production/pypi/simple/
npm config set registry https://holger.example.internal/production/npmjs/
A package that is not approved yet is answered "not in Holger yet: … waits for approval". Run the tool again after the approvals.
2.4 When a vulnerability is found later
The internet Holger refreshes its CVE data every day. When a new advisory hits a package that was already approved, the internet Holger invalidates it, the next sync manifest carries the invalidation, and the on-prem Holger holds that package at once and sends an ack. Admin → Sync shows the board: every downstream, what it has applied, what it has acked.
3. Mode 2: CDS (cross-domain, airgap)
The internet Holger is set up exactly as in 2.1. The difference is how the bundles cross.
3.1 On the internet side: push into a drop folder
In the internet Holger's holger.ron:
export: Some((push_to: Some("/srv/holger-out"), every: "5m")),
Every five minutes Holger writes each new signed bundle into /srv/holger-out as holger-packages-<sequence>.holger.znippy. Each bundle is signed by the two approvers and by a release signer who is neither of them, and every file in it carries two hashes. The drop folder is what your diode reads, or what a person copies onto a stick.
Once, on the internet side, make the trust policy the secure side will verify with: holger export-push --config holger.ron --trust-policy-out trust.ron. Carry trust.ron across one time.
3.2 On the secure side: the inbox
What you type (a machine with no network, or the one-way appliance from the Build Thing):
cargo install holger-downstream # one-way is the default: it has no code that dials
In its holger.ron: boundary: OneWay, network: Offline, and the trust policy you carried. The box refuses to start if the config names anything that would reach out.
Bundles arrive in <data>/bundle.inbox: the diode writes there, or you copy the file from the stick (the appliance takes a USB stick labelled HOLGERINBOX by itself). Holger sweeps the inbox, verifies each bundle inside a sandboxed job (the release signature first, then every file, then the evidence), and makes the packages available. A bundle that fails is moved to refused/ with a .why.txt beside it.
The first bundle needs your say. A box that has never imported has no "newest sequence" to compare against, so a rollback attack could slip in. An admin grants the first import once:
holger bundle allow-first-import --config holger.ron --from-bundle holger-packages-1.holger.znippy --policy trust.ron
or, in the console, Admin → Sync → First import. After that, an older bundle is refused by itself.
3.3 The CVE data on the secure side
The on-prem downstream does not scan, so it needs no CVE data. But if the secure side runs a full holger (because it must check packages of its own, or watch applications, section 5), it needs the data, and it cannot fetch it:
- The install medium of an estate box ships a signed CVE bundle; the box imports it at its first boot, before anyone signs in. The Overview line says "CVE data: bundled (signed by Holger release)".
- Later bundles are carried like any other file: into
<data>/intel.inbox, or with Overview → Import newer in the console, orholger intel import <file> --config holger.ron. A bundle older than the one installed is refused.
The line turns yellow after seven days and red after thirty, and with red every package waits.
3.4 Point the build tools at it
As in 2.3, at the secure-side Holger.
4. What is the same in both modes
- The internet Holger does all the checking. The on-prem side verifies signatures and hashes; it does not scan again, and it does not fetch trust from anywhere.
- Two people approve; the importer is never one of them.
- Everything is written to a hash-chained audit log before it takes effect, on both sides.
- A package that could not be checked waits. It is never served because the check failed.
5. The application watcher
So far Holger watched the warehouse. Since 3.2 it also watches your application: the program you build from those packages. You tell Holger what the application is, your CI sends Holger the application's graph (which packages it uses, and which functions call which), and Holger keeps a lamp on it:
| lamp | means |
| red | a package the application ships has a vulnerability whose vulnerable function the application actually calls, or is malware, or has an advisory Holger cannot narrow down |
| yellow | the vulnerable package is there, but the application never calls the vulnerable function (Holger writes that down as a VEX statement, not_affected); or a waiver on it ends within two weeks |
| none | clean |
| unknown | Holger could not judge: no graph yet, or no CVE data |
A red lamp tells the application's owners. The lamp is judged again every minute against the CVE data Holger has, so a vulnerability published today lights the lamp today, without a new build.
5.1 Online: the internet Holger watches
What you click: Security → Watched applications → Declare an application. Fill in:
- the name (
shop), - the repository it is built from,
- the products: the workspace members you ship (leave empty for all of them),
- the owners: who is told when the lamp turns red,
- the graph key: the public key your CI signs the graph with (Ed25519, hex).
What your CI types, on every commit (it needs the dwarves tool and the pinned nightly):
cargo lens graph-package # writes target/dwarves/graph-package.znippy
# sign it: a DSSE envelope over an in-toto statement about the file's sha256,
# predicate type https://holger.rs/watched-app-graph/v1, with the app name and the commit
curl -X PUT "https://holger.example.com/-/apps/shop/graph?commit=$GIT_COMMIT" \
-H "Authorization: Bearer $HOLGER_TOKEN" \
-H "X-Holger-Graph-Envelope: $(base64 -w0 envelope.json)" \
--data-binary @target/dwarves/graph-package.znippy
$HOLGER_TOKEN is a sign-in token (a static token or an OIDC token of the auth: block), not a repository token: repository tokens open the package doors only. What Holger believes is the signature on the envelope; it must be one of the application's graph keys.
What you see: the lamp on the Overview and Flow pictures (a band "WATCHED APPLICATIONS" on top, one box per application, a dot per product). Click it, and /apps/shop shows the reason: the advisory, the package, the call path from your code to the vulnerable function, and who was told. Download OpenVEX gives the VEX document your auditors ask for. From the command line, holger-cli app shop.
5.2 Airgapped: the same, with no network
The watcher needs the scanners and the CVE data, so on the secure side it runs on a full holger (the one of section 3.3), not on the plain downstream. Everything else is the same:
- Declare the application in that Holger's console (or in its
holger.ron, anapps:block). - Your on-prem CI builds the graph from your on-prem git and PUTs it to that Holger over the LAN, signed the same way.
- The CVE data is the signed bundle: shipped on the medium, carried in later (3.3).
- The lamp lights offline, from the data the box has. With no data it says unknown, never clean. The owners are told by the alarm line in the log and the audit row; a mail needs a relay the box was fed, which an airgapped box usually has not.
6. The rule to remember
Pick the mode your network allows. Streaming if one tunnel to one machine is allowed; CDS if nothing may be connected. In both, the internet Holger checks, two people approve, the on-prem side verifies and serves, and nothing that could not be checked is ever served. Declare your applications, and let the lamp tell you before your users do.
Everything here is written for the code on holger main bb80377b and holger-ui main e6b8623 (2026-11-04). The manual chapters name the files and the tests: 1, 5, 6, 8, 10, 12.