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

nordisk/holger

server/cli/Cargo.toml

[package]
# `cargo install holger` is the server (one of Holger's four products: `holger`,
# `holger-downstream`, `holger-ui`, `holger-cli`). The program is this crate's library
# (`src/lib.rs`, `holger::run`); `holger-downstream` is a thin crate over it.
name = "holger"
version = "3.1.1"
edition = "2021"
authors = ["Ignalina ApS", "Vetra Automation AB"]
description = "Holger, the artifact security gateway: `cargo install holger` gives the `holger` server. Reads holger's RON config graph, wires repositories to storage and exposed endpoints, and serves gRPC plus the HTTP doors of twelve package formats, with the intake chain (scanners, approvals, audit) and the offline bundle verbs."
license = "AGPL-3.0-only"
repository = "https://git.gunnar.rs/nordisk/holger"
homepage = "https://www.holger.rs"
readme = "../../README.md"
keywords = ["artifact", "registry", "repository", "airgap", "supply-chain"]
categories = ["command-line-utilities", "development-tools"]
# Development notes, agent instructions and machine-local tooling stay in the
# repository; they are not part of the published crate.
exclude = [
    ".nornir/",
]

# `holger` — the product: the only [[bin]] without required-features, so a plain
# `cargo build --release` at the root links `target/release/holger` and nothing else.
[[bin]]
name = "holger"
path = "src/main.rs"

# `holger-server` — the old name of the same build, for scripts and units that call it.
[[bin]]
name = "holger-server"
path = "src/main.rs"
required-features = ["holger-server-alias"]

# The estate's builds of the same program (Build Thing rows `mvp-server-musl*`): the
# static appliance (`holger-mvp`) and the metered, sold one (`holger-mvp-metered`).
[[bin]]
name = "holger-mvp"
path = "src/main.rs"
required-features = ["mvp-server"]

[[bin]]
name = "holger-mvp-metered"
path = "src/main.rs"
required-features = ["mvp-server", "metered"]

[dependencies]
# The console key (A7 row 14, `paired`): the box derives the console's login from the front's
# offered x25519 key and its own (boringtun's x25519-dalek re-export, already linked by
# wg-appliance-core), through blake3 `derive_key`.
boringtun = { version = "0.7.1", default-features = false, optional = true }
blake3 = { version = "1", optional = true }
# `default-features = false`: every feature below picks server-lib's closure itself, so
# a build that does not name `online` links no outbound HTTP client.
server-lib = { package = "holger-server-lib", version = "3.1.0", path = "../lib", default-features = false }

clap = { version = "4.6.7", features = ["derive"] }
# `fleet-enrol` (metered): the estate owner's OpenSSH key signs the attach enrolment.
ssh-key = { version = "0.7.0-rc.11", default-features = false, features = ["ed25519", "std", "alloc"], optional = true }  # the owner key signs an EnrolRequest of plugin-door-wire 0.1.1 (ssh-key 0.7)
anyhow = "1.0.104"
# The offline bundle verbs (`secure_update`, `bundle-verbs`); network-free.
update-bundle = { package = "holger-update-bundle", version = "3.0.0", path = "../../update-bundle", optional = true }
# Offline vulnerability intelligence (`intel build|import|status|scan`), riding
# `secure_update` like the bundle verbs it reuses. Network-free; `online` names
# its `fetch` below, so only an online binary can fetch feeds.
holger-intel = { version = "3.1.1", path = "../../intel", optional = true }
serde_json = "1"
chrono = { version = "0.4", features = ["clock"] }
# Skíðblaðnir's systemd lifecycle (lane B4): the lifecycle crate alone (unit
# render, scope, user and dirs, the guarded purge, start/stop/status over D-Bus),
# not the whole skidbladnir crate. Behind `systemd`: the exe flavour keeps it, a container or the
# appliance builds without it.
skidbladnir-systemd = { version = "0.1.0", optional = true }
env_logger = "0.11.11"
# `termination`: SIGTERM stops Holger too (the kubelet, and PID 1 in a FROM
# scratch container ignores a signal it has no handler for).
ctrlc = { version = "3.5.2", features = ["termination"] }
tokio = { version = "1.54.1", features = ["full"] }
# `testmatrix` only (in no default and no row): each verb's functional-status row for
# `nornir test`. A normal (not dev) dependency because `functional_status` is bin code.
nornir-testmatrix = { version = "0.2.13", optional = true }
# The appliance identity ceremony + userspace WireGuard data plane (edda's ONE
# implementation — the same crate gunnar consumes). Optional: only the
# `appliance` feature links it, so every existing tier binary is byte-for-byte
# unchanged. Zero-egress by construction: boringtun + base64ct + rand_core +
# libc, no HTTP client (asserted by build-space/tests/auth_ship_small.rs).
wg-appliance-core = { version = "0.1.5", optional = true }
# `paired` only: the installer's wire and the shared box side (Skidbladnir), with
# the WireGuard link the installer reaches a box over.
# 0.5.5: the box-side pairing loop after the feed (`boxloop`), the wg0 of a box on
# the front's relay and the boot-log tap.
nordisk-pairing = { version = "0.5.26", features = ["wireguard"], optional = true }
# The trial confirm's jera jobs (`holger-live/jobs`) are NAMED by `paired` below, not by this
# edge: a feature an edge turns on is a rider no build row declares (holger-recipe RULE 1).
holger-live = { path = "../../estate/holger-live", version = "3.0.0", optional = true }
# The scan sandbox laid from the medium at boot (lane HOLGER-MEDIA, `sandbox_boot`): the
# running slot's archive is read the way korp-start reads it (`live`, `datahome`), the
# entries are laid through server-lib's `sandbox::media`, clamd is started with crun.
korp-installer = { version = "0.2.11", default-features = false, optional = true }
holger-sandbox = { version = "4.0.1", path = "../sandbox", optional = true }
libc = { version = "0.2", optional = true }
# The STAGE lines and DEV-NARRATE's start (`paired`; A28 L12: a crate, shared with
# holger-estate-front, no longer a file compiled in by `#[path]`). Its sender, the box's boot
# log sealed to the installer's key over udp/51822 (Skidbladnir's shared `boxside`, the one
# gunnar-server starts), exists only under `dev-narrate`: absent from every release row, the
# installer option `dev-narrate` (default off) forbids `nordisk_narrate`.
holger-dev-narrate = { version = "3.0.0", path = "../dev-narrate", optional = true }
# Not optional (lane B3): Start says the declaration it read through `log` on every build.
log = { version = "0.4" }
# `paired` only: when holger IS PID 1 on a box, the network it owes itself (netlink +
# DHCP on eth0 and the utility eth1, the lease kept alive), SIGTERM and the reaper —
# edda appliance-pid1, the code gunnar-server's PID 1 was proven with.
appliance-pid1 = { version = "0.1.8", features = ["dhcp", "runtime"], optional = true }
# appliance-pid1 speaks `tracing`; with no subscriber, `log` carries its lines into
# holger's env_logger (and so into the boot log the installer collects).
tracing = { version = "0.1", features = ["log"], optional = true }

[features]
# ── `cargo install holger` ──
# The server, HTTPS (rustls; `ron_tls` on an endpoint), the static bearer token and mTLS
# (`downstream-core`), the twelve MVP formats, the intake chain with its scanners and the
# offline bundle and intel verbs (`secure_update`), the signed evidence an airgap export
# needs (`sign`), the sync protocol that enrols and feeds its downstreams (`sync`), and the
# systemd verbs. It links no outbound client and no monetize crate: `online` and the estate
# are opt-in. (`sign` and `sync` joined with the by-hand guide, HOL-PRODUCTS: without them
# the plain server could neither export a bundle nor enrol a downstream.)
default = ["downstream-core", "fmt-raw", "fmt-rust", "fmt-npm", "fmt-maven", "fmt-python", "fmt-docker", "fmt-go", "fmt-helm", "fmt-deb", "fmt-rpm", "fmt-znippy", "fmt-build-thing", "secure_update", "sign", "sync", "fetch", "export-push", "systemd"]

# ── The named extras: the same four on holger, holger-downstream and holger-cli ──
# `all-formats`: the twelve plus NuGet, gems, conda and Composer.
all-formats = ["server-lib/fmt-all"]
# `online`: the outbound HTTP client (pull-through from crates.io, PyPI, npm, Maven, OCI
# registries; OIDC `/userinfo` and JWKS fetches; the advisory feed fetch), with `jobs`: the
# pull-through is lowered and run on the job queue, so `online` alone pulled nothing (field
# test F2, lane hol-312fix).
online = ["server-lib/online", "holger-intel?/fetch", "jobs"]
# `federation`: Artifactory, Nexus, AWS and Azure upstreams through Skidbladnir, and the
# migration from Nexus and Artifactory.
federation = ["online", "migration", "server-lib/federation"]
# `full`: all three, with the rest of the open server (every login method, the mirror,
# admission, Maven Central's PGP check).
full = ["all-formats", "online", "federation", "maven-pgp", "server-lib/full", "secure_update", "fetch", "export-push"]
# The old name of `all-formats`.
fmt-all = ["all-formats"]

# ── The formats (server-lib passthroughs) ──
fmt-raw = ["server-lib/fmt-raw"]
fmt-rust = ["server-lib/fmt-rust"]
fmt-npm = ["server-lib/fmt-npm"]
fmt-maven = ["server-lib/fmt-maven"]
fmt-python = ["server-lib/fmt-python"]
fmt-docker = ["server-lib/fmt-docker"]
fmt-go = ["server-lib/fmt-go"]
fmt-helm = ["server-lib/fmt-helm"]
fmt-deb = ["server-lib/fmt-deb"]
fmt-rpm = ["server-lib/fmt-rpm"]
fmt-znippy = ["server-lib/fmt-znippy"]
fmt-build-thing = ["server-lib/fmt-build-thing"]

# ── The pieces ──
# The small core of every server: server-lib's `downstream` (the static bearer token and
# the mTLS doors).
downstream-core = ["server-lib/downstream"]
# `holger install …` and `holger service …` (systemd, lane B4). A container and the
# appliance (Holger is PID 1) leave it off.
systemd = ["dep:skidbladnir-systemd"]
# Builds the `holger-server` bin too: the same program under its old name.
holger-server-alias = []
# The CMS provenance verify path and the `znippy-common/sign` seal reader.
sign = ["server-lib/sign"]
# The durable job queue (jera) and its `JobService`, with the APK pull-through.
jobs = ["server-lib/jobs", "server-lib/pull-apk"]
# The intake chain without its scanners (states, approvals, quarantine, audit, the sealed
# production store) and the `intake` verb.
intake = ["server-lib/intake"]
# The intake chain with its scanners.
supply-chain = ["intake", "server-lib/supply-chain"]
# The sync protocol (`holger sync …`, the sync doors, the manifest an export carries).
sync = ["intake", "server-lib/sync"]
# A downstream whose upstream is reached over WireGuard from a signed invite (lane
# HOL-WGDS): the tunnel to the upstream's front, and `sync wg-key`.
downstream-wg = ["sync", "fetch", "server-lib/downstream-wg"]
# The offline bundle verbs (`bundle import`, `scrub`, `backup`).
bundle-verbs = ["intake", "dep:update-bundle", "server-lib/bundle-import"]
# The way out of a downstream (lane HOL-CDS): the mirror timer and connector, the gRPC
# client of another Holger, the sync pull and its signed ack. Every server build names it;
# the one-way downstream (`downstream`) does not, `cargo install holger-downstream
# --features fetch` adds it.
fetch = ["server-lib/fetch"]
# **`sandbox-off`**: a TEST build of the binary (lane HOL-SANDBOX-TRAIT): it lets
# `promotion.intake.rules: (sandbox: off)` run every job (a bundle's verification, the scans)
# in this process, for the tests that drive the real `holger-downstream` where no container
# engine is (the toolchain image). No shipped build names it
# (`tests/release_has_no_test_runner.rs`); without it `off` refuses the boot (`holger-sandbox-6`).
sandbox-off = ["server-lib/sandbox-off"]
# **`heimdall`** (lane HOL-HEIMDALL-02): `promotion.intake.sandbox: (engine: heimdall, …)`, the
# scans and bundle verifies under Heimdall's protected runner (youki-plain, then crun, where
# it cannot start). Off by default: the runner needs its shim and a kernel with Landlock.
heimdall = ["intake", "server-lib/heimdall"]
# The upstream's `export: (push_to, every)` push into a diode's folder, and `export-push`.
export-push = ["secure_update", "server-lib/export-push"]
mirror = ["server-lib/mirror", "jobs"]
admission = ["server-lib/admission"]
migration = ["server-lib/migration", "mirror", "online"]
# holger.rs's public web page (`/`, `/-/front`, `/-/releases`) in this binary.
front = ["server-lib/front"]
# The OpenPGP verifier for Maven Central's `.asc` (rpgp, about 96 crates). Without it a
# pgp pin is refused at load by name (`[provenance-pgp-not-built]`).
maven-pgp = ["server-lib/maven-pgp"]
# Login methods. Passkeys are holger's own `/-/auth/passkey/*` door (no IdP needed).
auth-passkey = ["server-lib/auth-passkey"]
auth-oidc = ["server-lib/auth-oidc"]
auth-oauth2 = ["server-lib/auth-oauth2"]
# The Secure Update Gateway: the intake chain with its scanners, the offline bundle and
# intel verbs, the import door, and the estate's signed time (`clock-follow`, no client).
secure_update = ["bundle-verbs", "dep:holger-intel", "server-lib/clock-follow", "server-lib/import-door", "supply-chain"]
# The box fetches its own advisories (the metered rows only).
intel-fetch = ["secure_update", "server-lib/intel-refresh", "holger-intel/fetch"]
# The paid state (the signed-entitlement door). In no default: the open build does not
# contain the vocabulary.
metered = ["estate", "server-lib/metered", "dep:ssh-key"]
# The paid download admission (every metered row).
admit-share = ["server-lib/admit-share"]
# The wasm plugin host and the meter guests (the metered rows); `allow-meter` is the open
# rows' built-in Allow meter, with no wasm runtime.
wasm-plugins = ["server-lib/wasm-plugins"]
meter-guests = ["server-lib/meter-guests"]
allow-meter = ["server-lib/allow-meter"]

# ── The downstream (`cargo install holger-downstream` turns this on) ──
# The on-prem, airgapped gateway: it takes packages from ONE upstream Holger only (a signed
# bundle carried in, or a pull or sync from that upstream), verifies them (release
# signature, approvals, every payload hash, the upstream's signed scan evidence), runs no
# scanner of its own and serves the twelve formats from the sealed production store. No
# outbound connection except to that upstream (`downstream_shape` guards it).
downstream = [
    "downstream-core",
    "server-lib/gateway",
    "intake",
    "bundle-verbs",
    "server-lib/jobs",
    "server-lib/clock-follow",
    "sync",
    "sign",
    "fmt-raw", "fmt-rust", "fmt-npm", "fmt-maven", "fmt-python", "fmt-docker",
    "fmt-go", "fmt-helm", "fmt-deb", "fmt-rpm", "fmt-znippy", "fmt-build-thing",
]

# ── The estate's builds ──
# The appliance and metered server's marker (the `holger-mvp*` bins).
mvp-server = ["downstream-core", "fetch", "export-push", "secure_update"]
# The WireGuard identity ceremony (edda `wg-appliance-core`), gated at run time by
# `HOLGER_APPLIANCE=1`, and the runtime grow engine.
appliance = ["dep:wg-appliance-core", "grow-runtime"]
grow-runtime = ["estate", "server-lib/grow-runtime"]
# The twin over mutual TLS, the recharge lane, the grow plane, the admin view's disks.
estate = ["fetch", "server-lib/estate"]
# Holger as a box of an installer-built estate (front, main, twin): PID 1's network, the
# shared pairing loop, the live slot, the scan sandbox laid from the medium.
# **`pid1`** (lane HOL-CDS-5, owner 2026-10-30): the ONE-WAY appliance's init without
# pairing — the reaper, the medium's network (static or DHCP on the declared NIC, no resolver
# probe), a HOLGERINBOX USB stick's signed bundles into `bundle.inbox`, the panic fault and
# the clean stop (`src/pid1.rs`, edda appliance-pid1). Pairing dials the estate's front, which
# a one-way box refuses; this is what a box needs to be its own init and dial nothing. The
# row `downstream-musl` is holger-downstream + `pid1`. With `paired` the paired duties win.
pid1 = ["dep:appliance-pid1", "dep:libc", "dep:korp-installer", "dep:holger-sandbox", "server-lib/sandbox-media"]
paired = ["appliance", "dep:boringtun", "dep:blake3", "dep:nordisk-pairing", "dep:appliance-pid1", "dep:tracing", "server-lib/clock-sync", "dep:holger-live", "holger-live/jobs", "dep:korp-installer", "dep:holger-sandbox", "server-lib/sandbox-media", "dep:libc", "dep:holger-dev-narrate"]
# The DEV media's boot log, sealed to the installer's key (the `…-narrate` rows only).
dev-narrate = ["paired", "holger-dev-narrate/dev-narrate"]
# Each verb's functional-status row for `nornir test` (in no default and no row).
testmatrix = ["dep:nornir-testmatrix", "nornir-testmatrix/testmatrix"]

# The appliance ceremony robot (tests/appliance_gate.rs). `required-features`
# rather than a file-level `#![cfg]`: a build without the feature must not
# compile an EMPTY test binary that prints `running 0 tests … ok` (the
# dark-test-file disease measured in korp) — it must not build the target at
# all.
[[test]]
name = "appliance_gate"
required-features = ["appliance"]

# The REAL boot of an estate box (tests/real_boot_estate_box.rs, lane D7): the built
# binary, the shipped declaration, a staged WireGuard seed and the kept feed.
[[test]]
name = "real_boot_estate_box"
required-features = ["paired", "metered"]

[dev-dependencies]
# tests/the_default_serves_the_mvp_formats.rs (lane HOL-MVP-FORMATS) builds a small znippy
# archive per archive-backed MVP format to boot.
znippy-compress = { version = "0.10.0" }
# tests/the_four_products.rs reads the product manifests (lane HOL-PRODUCTS).
toml = "1"
# airgap_export_import's provenance test builds a DSSE envelope (G3a).
base64 = "0.23"
traits = { package = "holger-traits", path = "../../traits", features = ["test-support"] }
# Test builds may scan in-process (`promotion.intake.rules: (sandbox: off)`); a
# release build refuses it (`holger-sandbox-6`). Only the test targets carry it.
server-lib = { package = "holger-server-lib", version = "3.1.0", path = "../lib", default-features = false, features = ["sandbox-off"] }
# src/paired.rs `d4_become_twin` (lane D4): an estate twin boots the shipped declaration
# (holger-recipe `estate_ron`) and the front's certify steps (nordisk-twin) run against it.
holger-recipe = { path = "../../holger-recipe", default-features = false }
nordisk-twin = { version = "0.1.2" }
tonic = "0.14"
nordisk-pairing = { version = "0.5.26" }
# The box's live door over a bench ESP (src/paired.rs tests, feature `paired`), and the trial's
# jera jobs (`jobs`, L21): this edge is what compiles holger-live's the_trial_job on a plain
# `cargo test` (the silenced-tests guard).
holger-live = { path = "../../estate/holger-live", features = ["bench", "jobs"] }
# The benchmark scenarios. DEV ONLY, by owner ruling 2026-09-20 — a bench crate's
# manifest must never be able to unify a feature into a shipped graph. `server/cli`
# uses it in tests only; the `holger-ds` driver is `bench-scenarios/src/bin/`.
# `secure-update`: `finalized_bundle_expiring_at`, the signed-bundle fixture of
# `tests/bundle_time.rs` (a bundle fresh by one clock and expired by another).
bench-scenarios = { package = "holger-bench-scenarios", path = "../../bench-scenarios", features = ["served", "secure-update"] }
# `tests/bundle_time.rs` serves the house's ONE signed-time format (the peer half
# of edda estate-clock, what holger's front and `estate-clock run` answer with)
# to the `bundle import` it runs. `peer` only: no HTTP client, even in tests.
estate-clock = { version = "0.1.1", features = ["peer"] }
# The time key's type is estate-clock's own (`estate_clock::peer::ssh_key`,
# =0.7.0-rc.11 since edda estate-clock 0.1.1, lane G3b), so no ssh-key here.
# Non-UI **tool robot** (tests/tools_robot.rs): reuse nornir's test-matrix row
# vocabulary so each holger tool verb's safe-path verdict becomes a real
# `functional` TestResultRow — the data twin of holger-server-ui's egui robot. Pure row
# builder, no `testmatrix` feature needed. Sibling path under git/ (same style as
# holger-server-ui's nornir-robotui dev-dep).
nornir-testmatrix = { version = "0.2.13" }
# The ceremony robot's euid probe (the TUN red control must SKIP loudly as
# root, where TUNSETIFF would succeed and the precondition does not hold).
libc = "0.2"
# The `uninstall --purge` law test builds a FAKE holger service home (ssh deploy
# key + token + data dirs) in a temp tree and drives the real purge path over it,
# so the "nuke the data, keep the keys" guarantee is proven without touching a
# real `$HOME` or `/home/holger`.
tempfile = "3"
# The `promote` CLI robot (tests/promote_cli.rs) recomputes the blake3 content
# address INDEPENDENTLY of the engine, so the test proves the seal landed at the
# right address instead of trusting the number the binary printed back.
blake3 = "1"
# Test-only PKI for the intel verbs' tests: in-process ed25519 certs.
rcgen = "0.14"
# A real wheel (zip) fixture for the intel scan test.
zip_entry = { package = "holger-zip-entry", version = "3.1.0", path = "../repository/zip-entry", features = ["fixtures"] }
# The downstream test's `.crate`, image layer and module tarballs.
tar = "0.4"
flate2 = "1"

# Feature-gated test targets: cargo skips them unless the features are on,
# instead of linking an empty binary. The gate's arms (.nornir/testmatrix-arms.json)
# turn them on; the silenced-tests guard reads `required-features`.
# Holger v2.0 H3: the upstream's signed export, imported and pulled by the real
# `holger-downstream` binary and served to every build system (the real clients ignored).
[[test]]
name = "downstream_serves_the_build_systems"
required-features = ["supply-chain", "secure_update", "sign", "jobs", "sync"]

# Lane HOL-CDS: the upstream pushes signed bundles into the diode's folder, the ONE-WAY
# `holger-downstream` (no fetch code) imports them through its inbox after the admin's
# first-import authorization and serves them; a way out refuses its start by line.
# Lane HOL-CDS-5: the one-way appliance's init (`pid1`): the medium's network, the USB stick's
# inbox (signed bundles only), held over the pure parts.
[[test]]
name = "one_way_appliance_init"
required-features = ["pid1"]

[[test]]
name = "one_way_downstream"
required-features = ["supply-chain", "secure_update", "sign", "jobs", "sync"]

# Holger v2.0 H6: the end-to-end gate. A fake internet, the upstream pulling from it
# (`online`, `jobs`), two-person approval, the signed bundle on a USB directory, the real
# `holger-downstream`, real pip, uv, cargo, mvn, skopeo and go, and an invalidation back.
# It runs in the holger-e2e-clients image (`cargo xtask e2e`); elsewhere it skips by name.
[[test]]
name = "e2e_internet_to_build_systems"
required-features = ["supply-chain", "secure_update", "sign", "jobs", "sync", "online", "fmt-all"]

[[test]]
name = "airgap_export_import"
# `sign`: the online side signs its scan evidence (DSSE); a build without it refuses
# the attestation signer at boot, so the test cannot pass without it (G1, measured).
required-features = ["supply-chain", "secure_update", "sign"]

[[test]]
name = "promote_cli"
required-features = ["supply-chain"]

# The Secure Update Gateway through the real binary. `secure_update` left `default` on
# 2026-10-22 and came back on 2026-10-23 (Holger G0: tier 1's steps are in the plain
# build); `.nornir/testmatrix-arms.json` names each one.
[[test]]
name = "bundle_time"
required-features = ["secure_update"]

[[test]]
name = "intel_offline"
required-features = ["secure_update"]

[[test]]
name = "bundle_import_audit"
required-features = ["secure_update"]

[[test]]
name = "scanner_sigs_bundle"
required-features = ["secure_update"]