Holger guards your artifacts at rest
Immutable artifact repository in pure Rust.
⚡ GATLING — one all-core engine · zero rayon · zero-copy · ~11/12 cores
- Airgap: transfer from internet, Nexus, Artifactory → immutable znippy archive
- Versioned: atomic transactions, content-addressed (Blake3), O(1) file random access
- Extreme performance: Apache Arrow zero-copy, multi-core parallel I/O
- Pure Rust: no OS packages, no shell, no Python (even decompressors rewritten from C)
- Embeddable: runs on bare-metal/OS-free devices, 2 MB RSS
Build and install
Anyone can clone the repository read-only, with no registered SSH key:
git clone ssh://anonymous@git.gunnar.rs:2222/nordisk/holger.git. The commands below use
the git@ address, which needs a registered key; the anonymous address works the same way.
git clone ssh://git@git.gunnar.rs:2222/nordisk/holger.git && cd holger && cargo build --release
target/release/holger
A plain cargo build --release at the root builds the server package only
(default-members) and links one binary, target/release/holger: the small
server-local build with the intake chain (the steps and their scanners), and no
bench, no wasm plugins, no online and no metered. No Build Thing and no xtask are needed, and nothing has to sit beside the
clone: the root workspace is the open core, so a lone clone resolves every crate from
crates.io, and cargo install --path server/cli installs the same server
(cargo xtask lone-clone proves both end to end with --locked, /healthz included; until
the open sibling crates it lists are on crates.io, --stage packages them from the siblings).
The estate, appliance and build-description crates (holger-fleet, holger-live, the build
declaration, xtask, the monetize guest and the estate tests) are members of the separate
estate/ workspace (cargo test --manifest-path estate/Cargo.toml --workspace), which
builds only in an estate checkout.
In an estate checkout, with ../edda, ../Skidbladnir, ../monetize, ../tunnr,
../draupnir, ../modgunn and ../facett beside it, run cargo xtask siblings
once after the clone (every other cargo xtask verb refreshes it too): it copies
.cargo/estate-siblings.toml to the uncommitted .cargo/siblings.toml, which
.cargo/config.toml includes when it is there, and switches the sibling patches on.
cargo xtask starts in any checkout: its launcher (xtask/boot) is a detached
workspace with no dependency. The tests against the real ONE box are
estate-tests/, an estate/ member (cargo test --manifest-path estate-tests/Cargo.toml),
which needs those siblings.
Install it instead:
cargo install --path server/cli
cargo install --git ssh://git@git.gunnar.rs:2222/nordisk/holger.git holger
cargo install holger
holger
The last cargo install line installs the release from crates.io (Holger 2.0.0 and later).
The on-prem downstream gateway (Holger v2.0 H3) is the same package with its own feature
set and binary, exactly the Build Thing's downstream row:
cargo install --locked --path server/cli --no-default-features --features downstream --bin holger-downstream. It takes packages only from its one upstream Holger (a signed
bundle on a stick, or a pull), verifies them, serves Maven, cargo, Python (PEP 503/691),
OCI and Go, blocks what its upstream invalidates, and dials nothing else.
The first holger creates ~/.holger/holger.ron (or $HOLGER_DATA/holger.ron) and
starts the server on loopback:
- health is
http://127.0.0.1:18444/healthz, - cargo reads
sparse+http://127.0.0.1:18444/cache/index/and publishes to/sparring(with--features online,/cacheis a caching mirror of crates.io), http://127.0.0.1:18444/is the front page with--features front.
To run it as a service: sudo ~/.cargo/bin/holger install add (systemd), or the Helm
chart with its scan-worker sidecar (Kubernetes). The exact commands for each flavour are
in Holger v2.0 — Run it: three flavours.
The default build is small on purpose: the server, HTTPS (rustls; set ron_tls on
an endpoint), the static bearer token and mTLS, the raw and rust (cargo)
repositories, and the intake chain (secure_update, which carries supply-chain). online, auth-oidc, auth-oauth2, auth-passkey, front, sign
and the other formats (fmt-all, fmt-npm, …) are opt-in features.
The next start keeps your edited holger.ron. holger start --config <file> runs any
other config; holger --help lists every verb. --features holger-server-alias also
builds the same program under its old name, holger-server. The default build links no
monetize crate (a test runs cargo tree -p holger -e normal to prove it).
The server variants
Every server variant row (holger-recipe/src/variants.rs) is a cargo feature set
on the same holger package. Metered and shop variants are opt-in only.
| variant row | install line |
|---|---|
server-local (the default) | cargo install holger |
upstream (the OPEN delivery's default) | cargo install holger |
downstream (the on-prem gateway alone) | cargo install holger --no-default-features --features downstream --bin holger-downstream |
embedded | cargo install holger --no-default-features --features embedded,sign,fmt-all,jobs,supply-chain,admission,mirror --bin holger-embedded |
mini | cargo install holger --no-default-features --features mini,appliance,sign,fmt-all,jobs,supply-chain,admission,mirror --bin holger-mikro |
full | cargo install holger --no-default-features --features full,front,sign,fmt-all,jobs --bin holger-full |
mvp-server | cargo install holger --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,allow-meter,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,mirror --bin holger-mvp |
mvp-server-musl | cargo install holger --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,allow-meter,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,mirror --bin holger-mvp --target x86_64-unknown-linux-musl |
mvp-server-metered (opt-in, metered) | cargo install holger --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,metered,admit-share,front,wasm-plugins,meter-guests,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,intel-fetch,mirror --bin holger-mvp-metered |
mvp-server-musl-metered (opt-in, metered) | cargo install holger --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,metered,admit-share,front,wasm-plugins,meter-guests,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,intel-fetch,mirror --bin holger-mvp-metered --target x86_64-unknown-linux-musl |
mvp-server-musl-metered-narrate (opt-in, metered, DEV boot log) | cargo install holger --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,metered,admit-share,front,wasm-plugins,meter-guests,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,intel-fetch,mirror,dev-narrate --bin holger-mvp-metered --target x86_64-unknown-linux-musl |
downstream-musl (a downstream's medium, OX-WGDS3) | cargo install holger --no-default-features --features downstream,downstream-wg,paired --bin holger-downstream --target x86_64-unknown-linux-musl |
The client side is not in this package: holger-agent-cli and holger-update-bundle.
The open rows: the Build Thing builds the cargo install line (Holger v2.0 H5)
The Build Thing rows of the open products build exactly what one plain cargo install
line builds: the same package, bin, features, default switch, target and release profile
(holger-recipe/src/twins.rs, held by holger-recipe/tests/rows_equal_cargo_install.rs).
cargo xtask twin builds each row and its line in the pinned toolchain image and compares
the two binaries: byte-identical for all three. holger-ui's ui row is
cargo install --locked --path crates/holger-ui-server in holger-ui.
| product | Build Thing row | cargo install line | crates | binary | adds over the default |
|---|---|---|---|---|---|
| upstream | upstream | cargo install --locked --path server/cli | 332 | 50.4 MB | nothing: it is the default |
| downstream | downstream | cargo install --locked --path server/cli --no-default-features --features downstream --bin holger-downstream | 329 | 50.1 MB | the gateway rules, sync and the Maven, Python, OCI and Go formats; it drops the scanners and the export |
| suite-mvp | mvp-server | cargo install --locked --path server/cli --no-default-features --features mvp-server,appliance,paired,auth-passkey,sign,allow-meter,fmt-raw,fmt-rust,fmt-npm,jobs,supply-chain,admission,mirror --bin holger-mvp | 408 | 61.2 MB | the estate: pairing, the appliance ceremony, passkeys, the built-in allow-meter, npm, the job queue, admission and the mirror |
| cli | cli | cargo install --locked --path holger-cli | 84 | 9.8 MB | the thin gRPC client: every console action as a command; no server, it dials only --at |
Biggest gap —
p50_ms: holger leads naive by 785.3× (0.15 vs 118.82).
v0.1.0 · oden · 32 cores · 2026-07-23
| workload | ops_per_sec | plans_per_sec | records_per_sec | p50_ms | p99_ms | p999_ms | iters | metadata_mbs | served_mbs | mean_ms |
|---|---|---|---|---|---|---|---|---|---|---|
| holger | 181863 | 761.95 | 3.8097M | 0.15 | 0.46 | 1.10 | 2000 | 348.79 | 108.43 | 0.17 |
| artifactory | 20578 | 1.10 | 6.11 | 37.63 | 12.24 | 1.53 | ||||
| dryrun | 100 | |||||||||
| naive | 8.38 | 0.0419M | 118.82 | 131.88 | 24 | 3.84 | 119.35 | |||
| nexus | 15832 | 1.40 | 7.96 | 62.64 | 10.60 | 2.00 |
holger vs Nexus vs Artifactory, served over HTTP on all sides, measured
identically. Fair hardware — no throttled rival: each JVM competitor gets every
core on the host and ~75 % of its RAM as a fair JVM heap; holger runs in-process on
the same cores. The host and core count these numbers were taken on are stamped in the
table header above and in the provenance block below — never assume a number from one
host is comparable with one from another. Full method + every metric:
README-full.
Which rival versions these are, and what they can serve. The numbers above were taken against Sonatype Nexus 3.72.0 (released 2024-09-04) and JFrog Artifactory OSS 6.23.13 (released 2021-02-21). Two limits follow from that and are stated here rather than left for the reader to discover:
- Artifactory OSS serves Maven/Gradle/Ivy/SBT and generic repositories only — no Cargo, no PyPI, no npm, in any free JFrog edition. No version fixes this. Every Artifactory row is a maven/generic data point.
- Nexus 3.72.0 predates Cargo support, which arrived in 3.73.0 (2024-10-10). A 3.72.0 server cannot serve Rust crates at all, so no row above supports a claim about holger versus Nexus on Rust crates.
The bench suite now also runs both rivals at their current tags
(Nexus 3.96.2, Artifactory OSS 7.161.26) as separate arms, so the modern
comparison exists beside the historical series rather than replacing it. Method
and every version fact: 10-benchmark-baseline.
Where these numbers come from
v0.1.0 · oden · 32 cores · 2026-07-23
| host | oden · 32 cores |
| version | 0.1.0 |
| measured | 2026-07-23 |
| arms | 16 |
| tests | 16/16 passed |
| publishable | yes |
The mashup and bench_status tables above are a static snapshot of the v0.1.0
run (the installed nornir predates those two docs render sections). The rest of
this page is still rendered by nornir docs render holger; refresh these two by
hand at release time, or restore their nornir:gen blocks once a newer nornir is
on PATH.
Release provenance (SLSA Build L3)
Releases are built by dwarves, the build server, in its isolated release-build mode and carry signed SLSA v1 provenance:
one in-toto statement per release, every artifact by its sha256, signed on the owner's
YubiKey. The bundles are in attestations/, the release key in
slsa/; how to verify (cosign, dwarves or Holger itself) and what still keeps us below
L3: docs/SLSA.md.
Tier 1 in five minutes
Tier 1 is the server from a plain cargo build plus one RON file that says where
packages come from (sources:), what they pass on the way in (steps:) and the
rules that hold for every source (rules:). There is no UI and no SVG in tier 1.
1. Clone and build. The plain build carries the intake chain (the steps, the
scanners and the offline advisory import). It does not carry online or monetize.
git clone ssh://git@git.gunnar.rs:2222/nordisk/holger.git && cd holger
cargo build --release
Every scan runs in a container, never in the server (rules: (sandbox: required), the
default): mint the scan worker image once. It needs rootless podman; without it Holger
refuses every scan by name (holger-sandbox-1).
cargo xtask scan-worker image # localhost/holger-scan-worker:0.1.0, FROM scratch
2. Write holger.ron. Four raw repositories hold the steps, one upload source
brings packages in, and four tokens hold the duties (an Importer, an approver, a
repository admin and an auditor).
(
exposed_endpoints: [ (ron_name: "door", ron_url: "127.0.0.1:50051", ron_http_url: Some("127.0.0.1:8080")) ],
storage_endpoints: [ (ron_name: "disk", ron_storage_type: "znippy", ron_path: "/tmp/holger/store") ],
repositories: [
(ron_name: "incoming", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: Some((ron_storage_endpoint: "disk", ron_exposed_endpoint: "door")), ron_data_dir: Some("/tmp/holger/incoming")),
(ron_name: "quarantine", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: Some((ron_storage_endpoint: "disk", ron_exposed_endpoint: "door")), ron_data_dir: Some("/tmp/holger/quarantine")),
(ron_name: "approved", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: Some((ron_storage_endpoint: "disk", ron_exposed_endpoint: "door")), ron_data_dir: Some("/tmp/holger/approved")),
(ron_name: "production", ron_repo_type: "raw", ron_upstreams: [], ron_in: None, ron_out: Some((ron_storage_endpoint: "disk", ron_exposed_endpoint: "door")), ron_data_dir: Some("/tmp/holger/production")),
],
audit: (dir: "/tmp/holger/audit", enabled: true),
auth: Some((
methods: [
StaticToken(subject: "ida", token: Some("ida-token-of-sufficient-length")),
StaticToken(subject: "bob", token: Some("bob-token-of-sufficient-length")),
StaticToken(subject: "adm", token: Some("adm-token-of-sufficient-length")),
StaticToken(subject: "aud", token: Some("aud-token-of-sufficient-length")),
],
duties: { "ida": [Importer], "bob": [SecurityApprover], "adm": [RepositoryAdmin], "aud": [Auditor] },
)),
sources: [
(name: "team", kind: upload, gates: (license: (deny: ["AGPL-3.0-only"]), cve: (max: medium))),
],
steps: (approvals: 1, unknown_license: Pass),
rules: (require_signed: false, require_provenance: false, deny_licenses: ["GPL-3.0-only"]),
)
Two lines loosen the gateway's defaults for this demo, and both say so in the file:
require_signed: false, require_provenance: false lets an unsigned file in (the default
floor refuses anything without a verified signature), and unknown_license: Pass lets
a file that declares no licence through (the default holds it for review).
3. Start it.
target/release/holger start --config holger.ron
4. Upload a package as the Importer.
echo hello > demo-1.0.txt
curl -X PUT -H "Authorization: Bearer ida-token-of-sufficient-length" \
--data-binary @demo-1.0.txt http://127.0.0.1:8080/incoming/demo-1.0.txt # 201 INGESTED
5. See its state, with its source and its effective policy, over HTTP and with the CLI:
curl -H "Authorization: Bearer aud-token-of-sufficient-length" \
"http://127.0.0.1:8080/-/intake/state?name=demo-1.0.txt"
HOLGER_TOKEN=aud-token-of-sufficient-length \
target/release/holger intake state --server 127.0.0.1:50051 --name demo-1.0.txt
Walk it through the steps: POST /-/intake/advance?name=demo-1.0.txt&verb=advance
as bob to SCANNED, POLICY_CHECKED and APPROVED, then as adm to SEALED, as bob
to PROMOTED and as adm to AVAILABLE. Production then serves it at
/production/demo-1.0.txt. A refusal names the rule and who set it, for example
rule `deny_licenses` (source 'team') for source 'team': licence AGPL-3.0-only is denied.
The RON shape
-
sources:is a list. Each entry has aname, akindand the fields its kind takes:kind what it is build notes uploadpackages pushed into the incomingrepository (PUT, twine, npm publish)default importers: [...]limits who may upload as it. With several upload sources, the first path segment names the source:PUT /incoming/<source>/<file>. No upload source declared = one implicit source namedupload.holgeran upstream Holger, mirrored on a timer default url,repositories: [(ron_from, ron_to)],credential,every_secs. Its bytes skip the steps, so the floor must sayallow_unchecked_sources: true.crateswithservescrates.io as a pull-through for the repositories in serves--features onlineThe default build refuses it by name. Its bytes skip the steps (as above). python,ocia PEP 503/691 index (PyPI, Chainguard Libraries) or an OCI registry ( cgr.dev), pulled THROUGH the steps--features online,jobsurl,credential: (username, password_env),select,max_files(python),platforms(oci),max_bytes,refresh_secs,auto_approve: off | when(provenance: require, cve_max: medium). A fetched package is INGESTED aspull-through:<name>and walks the steps under its source's policy.npm,maven,crateswithurlan npm registry (registry.npmjs.org, Chainguard Libraries for JavaScript at libraries.cgr.dev/javascript/), a Maven 2 repository (Maven Central, Chainguard Libraries for Java atlibraries.cgr.dev/java/) or a cargo sparse index (sparse+https://index.crates.io/), pulled THROUGH the steps--features online,jobsAs python, pluswait_secs(a request waits that long for its pull; default 30 s formaven, whose client does not retry a 503). npm: the tarball againstdist.integrity(sha512);dist.signaturesand thedist.attestations(SLSA provenance, publish attestation) go to the provenance verifier, the registry's keys inrules.keys. Maven: the POM and the JAR together, each against the.sha512/.sha256/.sha1beside it; Chainguard's.slsa-attestation.jsonbeside the JAR goes to the verifier; PGP.ascis not checked. crates: the.crateagainst its indexcksum; crates.io publishes no signature, so a floor withrequire_signed: truerefuses every crate. Clients:npm --registry <holger>/production/<name>/, a Maven<repository>at<holger>/production/<name>/, cargoregistry = "sparse+<holger>/production/<name>/". The packument,maven-metadata.xmland the index list only AVAILABLE versions. -
gates:on an upload source:provenance: require(identity: (issuer, subject), slsa_min: 2) | prefer | off,sbom: require | off,malware: on | off,license: (deny: [...]),cve: (max: info|low|medium|high|critical),block_formats: [...],secrets: block | warn | off. Every field is optional. -
steps:is the new spelling ofpromotion:with itsintake:block:incoming,quarantine,approvedandproduction(those are the defaults),scan: [...],approvals,transitions,inspect,advisory_db,allow_licenses,unknown_license,trust_policy,npm,secrets: (rules: "/path.ron")(the secrets scanner's rule file; absent, the built-in set), andgates: (...), the defaults every source starts from. -
rules:is the global floor:require_signed,require_provenance,slsa_min,identities,builders("id"or(id: "…", level: N)),keys,trust_root,require_sbom,malware,cve_max,secrets,deny_licenses,block_formats,allow_auto_approveandallow_unchecked_sources. The provenance fields are the verifier's own (holger-provenance), with one parser.
A source's gates (and steps.gates) may only tighten the floor. A gate that would
loosen it, such as provenance: off under require_signed: true or cve: (max: critical)
under cve_max: high, stops the load by name, with the rule it breaks. An artifact's
effective policy is the floor plus its source's gates. It is fixed at ingest, kept on
every line of its intake record, and shown by GetArtifactState, /-/intake/state and
the audit trail. Every block refuses an unknown key by name, and so does the auth block.
The old shape (promotion: with intake:, mirror:, rust-remote repositories) still
loads unchanged; it has no floor. Naming steps: and promotion: together is refused.
The default floor (rules: absent, or a rule left out) is a security gateway's
default: it fails closed on what this build can check.
malware: on: file inspection (archive bombs, path traversal, nesting, executables) and any configured ClamAV or YARA engine must pass.cve_max: high: an advisory rated critical is refused.secrets: block: the secrets scanner (in the scan sandbox) walks every package and image layer for private keys (PEM, OpenSSH, PGP, PuTTY), cloud credentials (AWS, GCP, Azure), tokens (GitHub, GitLab, Slack, Stripe, npm, PyPI, crates.io),.envfiles, kubeconfigs with credentials and high-entropy strings next to a secret-like name. A high or critical finding is refused by name; medium findings warn. A source may writegates: (secrets: block)over awarnfloor, never the other way. The evidence names the rule (secret:<id>), the path and the line, never the secret. The rules are data: aholger.secret-rules/v1RON file (steps.secrets.rules), carried over the airgap like the YARA rules; the built-in set isserver/lib/src/scanner/secrets-rules.ron.block_formats: ["pickle"]: the pickle family (.pkl,.pickle,.pt,.pth,.joblib) is refused at ingest.allow_unchecked_sources: false: aholgerorcratessource is refused at load.require_signed: trueandrequire_provenance: true: the provenance verifier (holger-provenance: DSSE, in-toto, SLSA, Sigstore, PEP 740, npm, cosign) is in the build, so nothing without a verified signature and provenance comes in. The material waits beside the artifact by its sha256 (<data_root>/holger-provenance/<sha256>/).trust_rootnames the signed Sigstore trust bundle for keyless signatures. APK packages (Wolfi, Chainguard, Alpine) come from akind: apksource: their provenance is the signedAPKINDEX.tar.gzin<data_root>/holger-provenance/apk/<source>/, checked against the apk keys the same trust bundle pins (apk-keys/<name>); a gate may narrow them withprovenance: require(apk_keys: ["wolfi-signing.rsa.pub"]).allow_auto_approve: false: nothing is approved automatically. A source'sauto_approve: when(...)works only when the floor allows it, decides only the approval vote (auto-approve:<source>, never the importer), and counts as at most one ofapprovals: 2.
Read the real docs
- 🧭 Holger v2.0 — the plan: internet → upstream → USB → on-prem downstream — the products, what each does, the phases H1–H6, and what is built through the Build Thing.
- 🗒️ Features —
docs/FEATURES.md— what is new in Holger, with status, date, source path and commit. - 📖 The complete manual —
docs/manual.pdf— every subsystem chapter (server, agent, storage, auth, release & history). - 📄 Full overview —
.nornir/README-full.md— the detailed pitch, quickstart, design, the full competitive matrix, and all measured benchmarks.
Development setup
The generated docs in this repo are kept in sync by git hooks in .githooks/.
Point git at them once per clone:
git config core.hooksPath .githooks
On every commit that stages Rust source, a Cargo.toml, or
.nornir/ai-context.toml, the pre-commit hook regenerates the AI-context packs
(docs/ai-context.md + docs/ai-context-full.md) with
kvasir and stages them, so they never drift;
it also re-renders README/CHANGELOG via nornir. A missing tool warns and skips; a
kvasir validation error aborts the commit. kvasir is taken from PATH (install it
with cargo install --path ../kvasir) or a sibling ../kvasir release build.
Contributing, security and changes
- CONTRIBUTING.md: how to get the code, report a bug and propose a change.
- SECURITY.md: how to report a vulnerability privately (holger@holger.rs).
- CHANGELOG.md: what changed in each release, and the vulnerabilities each one fixed.
License
AGPL-3.0-only, © 2026 Vetra Automation AB and Ignalina ApS (Rickard Ernst Björn Lundin) (jointly owned in equal
shares, 50/50). Commercial terms are available from both co-owners jointly; write to
gunnar@gunnar.rs. Versions published to crates.io on or before 2026-10-02 keep the
license they were published with (mostly MIT). See LICENSE, NOTICE,
COMMERCIAL-LICENSING.md, TRADEMARKS.md and THIRD_PARTY_LICENSES.md.