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

nordisk/holger

docs/v2/idiot-guide.md

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:

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:

  1. 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.
  2. 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.
  3. 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:

  1. 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/"),
    ],
)),
  1. Say who approves. In the auth: block, give two people the duty SecurityApprover and the person who imports the duty Importer. Two approvers is the default; the importer can never approve what they imported.

  2. 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.

  3. 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 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

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:

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:

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.