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

nordisk/holger

docs/v2/04-the-ron.md

4. The RON

Holger is configured by one RON file, holger.ron. The first holger start writes one to ~/.holger/holger.ron (or $HOLGER_DATA/holger.ron); holger start --config <file> runs another.

An unknown key stops the start. Every block refuses a key it does not know, by name. A key Holger does not understand is a key it does not honour, and the worst case is a security control that reads as on in the file and is off in the process (server/lib/src/lib.rs, the root struct Holger; read_ron_config).

The blocks

block what it says default when absent
exposed_endpoints: where Holger listens (gRPC ron_url, HTTP ron_http_url, the admin plane ron_admin_url, TLS ron_tls) none: nothing listens
storage_endpoints: where the bytes live (ron_storage_type: "znippy", ron_path) none
repositories: one entry per repository: name, type, upstreams, storage and endpoint none
sources: where packages come from (below) one implicit upload source
steps: what a package passes on the way in (below) the intake chain's defaults
rules: the floor that holds for every source (below) the security gateway's floor
pull_through: the internet sources pulled through the steps (below) none
intel: the local advisory database (below) none: the vulnerability scan has no data and holds every package
auth: the sign-in methods, the role policy and the duties none: open access, which the safe defaults refuse off loopback
access: (reads: Gated | Open, front: Open | Gated) the build decides: an estate build gates reads, the default and downstream builds read openly (server/lib/src/posture.rs)
audit: (dir, enabled, mode, checkpoint_every) on, the hash-chained log; mode: Off refuses every start (holger-audit-13)
security_profile: Standard or Critical (fail closed: the start refuses what the profile cannot enforce) Standard
operation_mode: ReadWrite or ReadOnly (every network write refused before its body is read) ReadWrite
network: Auto, Online or Offline (below) Auto
boundary: OneWay or Fetch: whether this Holger may reach its upstream a downstream build is OneWay
domain: which side this Holger is on: a label of 1 to 64 printable ASCII none
trust: (first_import: Admin): the first import needs an admin's grant Admin, the only value
export: push signed bundles into a drop folder none
sync: the sync protocol a downstream enrols with ((name: "…")) none
attestation: the evidence signer: trust_roots, signer_key, signer_cert, signer_alg none: no signed evidence
promotion: the old spelling of steps:; it still loads, without a floor none

Naming steps: and promotion: together is refused.

sources

sources: is a list. Each entry has a name, a kind and the fields its kind takes (server/lib/src/sources.rs, Source and SourceKind).

kind what it is build
upload packages pushed into incoming (HTTP PUT, twine, npm publish, a Maven deploy). importers: [...] limits who may upload as it. With several upload sources, the first path segment names the source: PUT /incoming/<source>/<file> default
holger an upstream Holger, mirrored on a timer: url, repositories, credential, every_secs. Its bytes skip the steps, so the floor must say allow_unchecked_sources: true fetch (in the server's default)
crates with serves crates.io served as fetched for the repositories in serves. Its bytes skip the steps (as above) online
crates, python, npm, maven, oci with url an internet index pulled THROUGH the steps: each package is ingested as pull-through:<name> and walks the chain under its source's policy online (with jobs)
apk Wolfi, Chainguard or Alpine packages. With url they are pulled through the steps, each held against the upstream's signed APKINDEX.tar.gz; without url they are uploaded, and the signed index is read from <data_root>/holger-provenance/apk/<name>/ online for url

The fields of a pull-through source: url, credential: (username, password_env | password_file), select, max_files, platforms (OCI), max_bytes, refresh_secs, wait_secs (how long a request waits for its pull), gates and auto_approve.

gates

gates: on a source, and steps.gates (the defaults every source starts from). Every field is optional (sources.rs, Gates):

A gate 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 and shown by GetArtifactState, /-/intake/state and the audit trail.

rules: the floor

rules: is the global floor. Every field's default is the security gateway's default, so writing one rule never loosens another (sources.rs, Rules and its Default):

rule default meaning
require_signed on when the build checks signatures nothing unsigned comes in
require_provenance as require_signed every artifact carries verified provenance
slsa_min 0 the least SLSA build level (0 = none asked)
identities, builders, keys, trust_root empty the signers, SLSA builders, keys and the signed Sigstore trust bundle trusted at all
require_sbom false every artifact needs an SBOM
malware on file inspection and every configured malware engine must pass
cve_max high the highest advisory severity allowed: a critical advisory is refused
secrets block a high or critical secret finding is refused
deny_licenses empty SPDX ids refused everywhere
block_formats ["pickle"] the pickle family (.pkl, .pickle, .pt, .pth, .joblib) is refused at ingest
allow_auto_approve false no source may approve by itself
allow_unchecked_sources false a source whose bytes skip the steps is refused at load
sandbox required every scan runs in the scan sandbox; off only in a test build (holger-sandbox-6)

steps

steps: is the intake chain (sources.rs, Steps):

pull_through

The internet sources pulled through the steps, with their own floor (server/lib/src/pull_intake/mod.rs, PullThroughSettings):

pull_through: Some((
    sources: [
        (name: "pypi",  kind: python, url: "https://pypi.org/simple/"),
        (name: "npmjs", kind: npm,    url: "https://registry.npmjs.org/"),
        (name: "central", kind: maven, url: "https://repo1.maven.org/maven2/"),
        (name: "crates-io", kind: crates, url: "sparse+https://index.crates.io/"),
        (name: "goproxy", kind: go,   url: "https://proxy.golang.org/"),
        (name: "cgr",   kind: oci,    url: "https://cgr.dev", platforms: ["linux/amd64"]),
    ],
    rules: (cve_max: high),
    retry_after_secs: Some(2),
)),

The kinds are python, oci, npm, maven, crates, apk and go. retry_after_secs is the Retry-After a client gets while a pull is in flight (default 2 s). The pull-through needs the online build. The field test of 2026-11-01 ran these sources against the real registries with real clients. Clients reach a source at /production/<name>/ (chapter 3).

intel

The local advisory database the vulnerability scan reads (server/lib/src/intel.rs, IntelConfig). A scan never dials; it reads only this database.

key default meaning
db_dir (required) the database directory: the live set, the anti-rollback floor, the kept bundles
offline true intel comes in only as a signed bundle through the import doors. false: the intel.refresh job fetches OSV, NVD and KEV at boot and daily (needs online; network: Offline refuses it)
warn_after_days 7 a scan against an older database warns
block_after_days 30 a scan against an older database holds the package (HOLGER-INTEL-EXPIRED, chapter 5)
trust_policy none the trust policy an intel bundle is verified against
ecosystems the default set the OSV ecosystems an online refresh fetches
feed_mirror none a base URL to fetch the feeds from instead of the public sources (https, or http on loopback)

network

network: is Auto when absent (server/lib/src/egress.rs, NetworkMode):

In a downstream build, network: Offline that also names an upstream is refused at the start (holger-offline-2): an offline downstream makes no network call at all (chapter 10).

The one-way and export keys

Endpoints

Off loopback an endpoint needs an authentication method (else holger-listen-1) and ron_tls, or insecure: true where something else encrypts the wire (else holger-listen-2). ron_admin_url, ron_admin_http_url and ron_admin_allow put the admin plane on its own listener; ron_tls.crl checks client certificates against a CRL (chapter 10).

An example

(
    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: "/var/lib/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("/var/lib/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("/var/lib/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("/var/lib/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("/var/lib/holger/production")),
    ],
    sources: [
        (name: "team", kind: upload, gates: (license: (deny: ["AGPL-3.0-only"]), cve: (max: medium))),
    ],
    steps: (approvals: 2),
    rules: (require_signed: true, require_provenance: true, slsa_min: 2),
)

The README's guide "Three products by hand" has a complete server RON with tokens, duties, the evidence signer and intel (chapter 3).

The flow picture

Holger draws the artifact flow from the running RON: the sources, the steps, the repositories, the gates, the estate (console, front, main, twin, every downstream by name) and a counter on every box. The fetches, bytes sent, pullers and promotion counters are the audit index's aggregates; a server without a readable audit log shows "not recorded". It has three zoom levels of one model: with no open it draws the machines and the upstream sources, ?open=main,lab-1 unfolds those machines with their part of the RON, and ?open=all is the full RON. It is served as GET /-/flow.svg (the model as JSON at GET /-/flow.json, schema holger.flow/v2), over gRPC as AdminService.FlowSvg (FlowSvgRequest.open), and by holger-cli flow (server/lib/src/flow.rs). The console's Flow page shows it (chapter 11).

Moved from v1