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):
provenance: require(identity: (issuer, subject), slsa_min: 2) | prefer | offsbom: require | offmalware: on | offlicense: (deny: ["AGPL-3.0-only", …])cve: (max: info | low | medium | high | critical)secrets: block | warn | offblock_formats: [ … ]
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):
incoming,quarantine,approved,production: the four repositories (those names are the defaults);scan: [...]: the scanner kinds that must report for every artifact (the floor adds its own);approvals: distinct approvers APPROVED needs, never the importer. Default 2.approvals: 1is a choice the file writes down;transitions,inspect,advisory_db,allow_licenses,unknown_license(Holdby default,BlockorPass),trust_policy(the bundle trust policy of the import doors),npm,oci_push,secrets: (rules: "<file>"),sandbox,gates.
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):
Autoresolves at the start:Offlinewhen the config names nothing that dials,Onlinewhen it names an upstream (a pull-through source with a url, a mirror, a twin's primary, a sync upstream, the intel feeds) or the build is an estate box. The start says which, and why.Online: upstreams, mirrors and twins may dial out.Offline: no outbound call at all. The start refuses a config that needs the network, and every dial is refused before a socket is opened.
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
boundary: OneWay | Fetch: omitted, a downstream build isOneWay;Fetchneeds thefetchbuild (chapter 8).domain: "<label>": printed bystartand the CLI verbs, sent asX-Holger-Domainon every HTTP and gRPC answer, and shown in the console's top bar (holger-boundary-3if it is not a label).trust: (first_import: Admin): the first import needs an admin's grant.export: Some((push_to: Some("<dir>"), every: "5m", vendor: "holger", product: "packages", signer_dir: None, expires_days: 30)): push signed bundles into a drop folder (the defaults shown; refused on a one-way downstream,holger-export-1).
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
- The source kinds, gates, steps and the default floor of v1/README-v1.md, checked against
sources.rs.