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-threat-model.md

Holger Secure Update Gateway — threat model (MVP)

Status: maintained design document (not generated). Pairs with docs/secure-update-gateway-mvp.md. Each mitigation names the code that enforces it.

1. Assets and adversaries

Assets: the protected OT/IT environment; the integrity + authenticity of every byte that crosses the airgap; the approval record; the audit trail.

Adversaries considered:

id adversary capability
A1 Malicious/compromised upstream source serves tampered or malicious artifacts, wrong bytes, wrong size, redirects
A2 Man-in-the-middle on the intake network rewrites downloads, swaps evidence
A3 Malicious insider on the intake side tries to push an unreviewed/unsigned artifact, or forge one approval
A4 Tampering-in-transit across the airgap flips bytes, truncates, replays an old bundle, swaps evidence
A5 A single rogue approver tries to satisfy the two-person rule alone
A6 Attacker with a hostile bundle at the protected import boundary zip-slip, symlink escape, decompression bomb, rollback, oversize
A7 Local user on the protected node tries to read identities/secrets from audit or config, or write via the network

Out of scope (documented): physical destruction of the OT node; a compromised release-signer private key held by a trusted signer (key custody is the operator's responsibility — mitigated only by the two-person rule + distinct release signer); supply chain compromise of the Rust toolchain / crates the gateway is built from.

2. Per-stage attacks and mitigations

2.1 Strict intake (A1, A2)

2.2 Candidate preparation and evidence (A2, A3)

2.3 Approvals and the two-person rule (A3, A5)

2.4 Finalization and release signature (A3, A4)

2.5 Airgap transport tampering + replay (A4)

2.6 Hostile bundle at the import boundary (A6)

Every bundle is treated as hostile input. The verifier rejects, before writing any payload:

No unwrap/expect/unchecked indexing/panic on untrusted bundle data — parsing returns typed errors with safe, path-free client messages. extract always verifies first; there is no --skip-verification production path. Extraction stages into an empty directory and atomically renames into place (verify-before-extract, no TOCTOU window on the final directory).

2.7 Import atomicity (A6)

2.8 Read-only protected serving (A7)

2.9 Local disclosure on the protected node (A7)

3. Critical-profile fail-closed matrix

In security_profile = Critical, startup refuses any configuration that cannot enforce the profile (clear validation error, never a silent rewrite):

condition result
audit.enabled = false startup fails
audit sink init fails / dir unwritable startup fails
no authentication configured startup fails
require_auth_for_reads = false startup fails
no compiled-in auth provider for the tier startup fails
an exposed endpoint without mTLS (cert+key+client_ca) startup fails
static bearer as the only auth mechanism startup fails
inline secret where an env/file reference is supported rejected
any exposed repository fails to initialize startup fails (not just that repo disabled)
/metrics open and not loopback-bound rejected (must be authed/disabled/loopback)

/healthz and /readyz may stay open but expose only minimal status.

4. Residual risk