Holger Secure Update Gateway — operator guide
Maintained companion to
docs/secure-update-gateway-mvp.mdand the threat model. Every command below is exercised by the smoke flow inexamples/secure-update-gateway/e2e-journey.sh.
All keys and certificates shown here are test material generated on the spot. Never commit production private keys.
0. Build the binaries
# Authoring + verifier (network-free)
cargo build -p holger-update-bundle --bins # holger-bundle, holger-verify
# Server + offline ops (import / scrub / backup / audit verify)
cargo build -p holger --bin holger-server
# The protected-node tier (lean, zero-egress)
cargo build -p holger --bin holger-mikro --no-default-features --features mini
1. Key / trust-root placement
Generate an ed25519 signer keypair + self-signed certificate per role (openssl shown; any ed25519 X.509 works). Keep the .key files offline; only the .crt fingerprints go into the trust policy.
for who in security-reviewer system-owner release-signer; do
openssl genpkey -algorithm ed25519 -out "$who.key"
openssl req -x509 -key "$who.key" -subj "/CN=$who" -days 365 -out "$who.crt"
done
# Certificate fingerprint = sha256 of the DER:
fp() { openssl x509 -in "$1" -outform der | sha256sum | cut -d' ' -f1; }
Write the trust policy (trust-policy.ron) listing the fingerprints per role and the trusted release signer. See examples/secure-update-gateway/trust-policy.ron.
2. Prepare a candidate bundle
holger-bundle prepare \
--candidate ./candidate \
--bundle-id acme-drive-2.0.0 --sequence 7 \
--supplier ACME --product ExampleDrive --release-version 2.0.0 \
--model ACS880 --revision R4 \
--verdict pass \
--payload ./firmware.bin \
--sbom ./app.cdx.json \
--receipt ./transfer-receipt.json # from `holger-agent --strict --receipt`
# prints: candidate-digest: <sha256 of core.json>
--verdict is required and has no default. It is recorded in core.json, so every approval and the release signature cover it. Only pass can be finalized, verified and imported; any other verdict (block, warn, …) is refused by name with holger-update-40.
3. Two distinct approvals
holger-bundle approve --candidate ./candidate --role security-reviewer \
--cert security-reviewer.crt --key security-reviewer.key
holger-bundle approve --candidate ./candidate --role system-owner \
--cert system-owner.crt --key system-owner.key
4. Finalize (distinct release signer)
holger-bundle finalize --candidate ./candidate --policy trust-policy.ron \
--release-cert release-signer.crt --release-key release-signer.key \
--output ACME-ExampleDrive-2.0.0.holger.znippy \
--receipt finalization-receipt.json
5. Offline verification (+ target)
holger-verify verify ACME-ExampleDrive-2.0.0.holger.znippy \
--policy trust-policy.ron \
--target-product ExampleDrive --target-model ACS880 --target-revision R4
echo "exit: $?" # non-zero on ANY failure
holger-verify verify ... --json emits a machine-readable report. Tamper one byte and it fails. Carry the single .holger.znippy file across the airgap (USB).
6. Import into the protected node
The protected config declares a raw repository whose ron_data_dir is the served bundle store (see examples/secure-update-gateway/ot-critical-readonly.ron). Stop the server (or ensure no concurrent import), then:
holger-server bundle import \
--config ot-critical-readonly.ron --repo bundles \
--bundle ACME-ExampleDrive-2.0.0.holger.znippy --policy trust-policy.ron \
--target-product ExampleDrive --target-model ACS880 --target-revision R4
Import verifies, checks anti-rollback (a lower/replayed sequence is rejected — there is no --force), writes atomically into the store, records the accepted sequence, emits an import receipt, and appends an audit event. A re-import of the identical bundle is an idempotent no-op.
7. Start the protected (Critical, ReadOnly) node
holger-mikro start --config ot-critical-readonly.ron
Startup refuses any config that cannot enforce the Critical profile (audit off, no auth, read gate off, missing mTLS/client_ca, a failing repo). In ReadOnly mode every network mutation is refused before the body is read.
8. Fetch the approved artifact (maintenance workstation)
Over the raw HTTPS door (mTLS), fetch the signed bundle and re-verify + extract:
curl --cert client.crt --key client.key --cacert ca.crt \
https://ot-node:8443/bundles/ACME/ExampleDrive/2.0.0.holger.znippy \
-o fetched.holger.znippy
holger-verify extract fetched.holger.znippy --policy trust-policy.ron --dest ./unpacked
extract always verifies first (no --skip-verification) and writes into an empty directory atomically. The human then performs the actual (out-of-band) update — the gateway never installs or flashes anything.
9. Scrub, backup, restore
holger-server scrub --config ot-critical-readonly.ron --repo bundles --policy trust-policy.ron
holger-server backup create --config ot-critical-readonly.ron --repo bundles --output store-backup.znippy
holger-server backup verify --input store-backup.znippy
holger-server backup restore --input store-backup.znippy --data-root ./restored-store
holger-server audit verify --config ot-critical-readonly.ron
scrub re-verifies every installed bundle (non-zero on corruption). backup excludes private keys and inline config secrets — restore reports what must be restored separately. restore only writes into an empty target.
Recovery after a failed import
A failed import leaves no partial final file (atomic rename) and no state marker. If the store lock (<store>/.holger-import.lock) is stale after a crash, remove it only once the server is stopped, then re-run the import.
Key rotation limitations
Trust is a fingerprint/subject allowlist. Rotating a signer means updating trust-policy.ron (adding the new fingerprint, removing the old) and re-distributing it. There is no online revocation in the protected zone — a compromised-but-listed fingerprint stays trusted until the policy is updated and re-imported. HSM/PKCS#11-backed keys are the planned migration.
Log locations & exit codes
- Audit: append-only Arrow-IPC segments under
audit.dir(0700 dir / 0600 segments on Unix).holger-server audit verifyreplays them offline. - Import receipts:
<store-root>/holger-import-receipts/<bundle_id>.json. - Anti-rollback state:
<store-root>/holger-update-state/<supplier>/<product>/<NNNN>.accepted. - Exit codes:
holger-verifyand everyholger-serverop return non-zero on any verification / integrity failure;0only on full success.