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

nordisk/holger

docs/v1/secure-update-gateway-operator-guide.md

Holger Secure Update Gateway — operator guide

Maintained companion to docs/secure-update-gateway-mvp.md and the threat model. Every command below is exercised by the smoke flow in examples/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