Threat model
This page is honest about what The Gateway can do for you, and where it can’t help. Read it before depending on the daemon for anything serious.
What it does protect against
Section titled “What it does protect against”A failing VPN tunnel leaking traffic to the ISP
Section titled “A failing VPN tunnel leaking traffic to the ISP”The kill-switch is reference-counted, lives at prerouting priority -1, and is re-applied by the reconciler on every tick. A stale handshake doesn’t produce a single packet to the default route. This is the strongest guarantee in the system.
Hard-coded DNS resolvers on client devices
Section titled “Hard-coded DNS resolvers on client devices”force_dns DNATs every UDP/53 to the gateway resolver, so a phone that ignores DHCP and hits 8.8.8.8 directly still gets answered by the integrated resolver — with filtering, ECS stripping and per-device policy applied.
DoH bypassing the local resolver
Section titled “DoH bypassing the local resolver”System-wide DoH-block matches by SNI on TCP/443 against a curated list of known providers. Firefox’s “Trusted Recursive Resolver” and similar mechanisms stop working when it’s on. DoT (TCP/853) is dropped when DNS mode is set to paranoid (see Privacy & kill-switch).
Cross-tunnel linkability of simultaneous flows
Section titled “Cross-tunnel linkability of simultaneous flows”The splitting engine round-robins per connection across a tunnel pool. Two simultaneous TLS sessions to two different SaaS providers exit through two different tunnels — an observer at any one exit sees only half of your activity.
Packet-length and timing fingerprinting (at higher levels)
Section titled “Packet-length and timing fingerprinting (at higher levels)”Level 3 adds 512-byte packet padding; levels 4 and 5 raise it to 1400 bytes (near the MSS-clamped frame size). A 40-byte heartbeat and a 1300-byte HTTP request look the same to a passive observer. Levels 3+ also add Poisson-distributed cover traffic so the visible packet rate doesn’t track real activity.
Censored destinations behind SNI fronting
Section titled “Censored destinations behind SNI fronting”The bridge subsystem terminates TLS on a real, innocuous decoy hostname (your decoy site) and tunnels HTTP CONNECT inside. A censor inspecting SNI sees ordinary traffic to a static website.
Drift from sibling processes
Section titled “Drift from sibling processes”The reconciler scans nftables, ip rule and ip route once a minute (configurable). A docker daemon or stray ufw enable that drops a chain wins for at most that interval, then loses.
Cross-tunnel route bleed
Section titled “Cross-tunnel route bleed”Each tunnel runs in its own network namespace with a dedicated veth pair into the root namespace. A misbehaving OpenVPN client or redsocks process can’t accidentally add a route or ip rule that affects another tunnel — the namespaces are kernel-isolated.
Cohort-level fingerprinting via periodic-task timing
Section titled “Cohort-level fingerprinting via periodic-task timing”Every periodic task in the daemon (DNS cache refresh, OCSP refresh, mesh heartbeat, webhook dispatch, reconciler) picks a random phase offset at startup. Two installations configured with the same intervals don’t fire their first tick at the same wall-clock moment, so an upstream observer (DoH provider, CA OCSP responder, mesh control plane) can’t de-anonymise an appliance by its aligned task spikes alone. Phase stays stable across restarts on a single install; it varies across installs.
Harvest-now-decrypt-later TLS interception
Section titled “Harvest-now-decrypt-later TLS interception”The daemon’s outbound TLS (to upstream DoH resolvers, OCSP, webhook receivers, mesh control plane) uses X25519MLKEM768 as the preferred hybrid key-exchange group in Go 1.24+ defaults. Recorded handshakes can’t be decrypted later by a Shor-capable attacker. Operators with peers that have shipped PQ support can switch pq_crypto_mode to required for MLKEM-only strict mode (see Settings).
Insider misuse without a paper trail
Section titled “Insider misuse without a paper trail”Every config-changing API call is logged with operator, source IP, before/after, and timestamp. Audit retention defaults to 90 days; raise it for compliance.
Physical seizure of an unattended box (dead-man’s switch)
Section titled “Physical seizure of an unattended box (dead-man’s switch)”For deployments where the daemon’s host might be physically lost (travel router stolen, on-prem box seized), the dead-man’s switch auto-wipes sensitive tables if no admin has logged in for a configured window:
curl -X PUT https://gateway.lan:8080/api/v1/system/deadman \ -H "Authorization: Bearer $API_KEY" \ -d '{"enabled":true,"hours":72}'A background goroutine polls every 5 minutes. If time.Since(last_admin_login) > hours, the daemon executes a soft wipe automatically:
- Truncates
audit_log,anomalies, and resolvedalerts - Removes manual entries from
dns_blocklist - Clears in-memory ring buffers (live logs, drift, recent events)
The last_admin_login timestamp is bumped by the auth middleware on any authenticated UI or API session — opening the dashboard, or any Authorization: Bearer … API call from an admin token. Long unattended deployments need an explicit ping (a scheduled curl https://gateway.lan:8080/api/v1/status -H "Authorization: Bearer $API_KEY" from a peer is a typical pattern).
Fail-safe direction. A corrupted last_admin_login kv row does not trigger a wipe — that would be the wrong direction for a single bad write. Instead the deadman-broken alert fires so the operator can repair the row before the window matters.
The manual POST /api/v1/system/wipe accepts three levels:
| Level | Effect |
|---|---|
soft |
The dead-man set above. Just deletes sensitive SQLite tables. |
medium |
Soft, then a 3-pass random overwrite of files in /var/lib/gateway (kill-switch state files are preserved so the daemon survives). |
hard |
Soft + medium, then brings down every tunnel and reaps gatewayd-managed OpenVPN processes. |
The dead-man auto-trigger only does soft — the manual wipe is the escalation path for an operator who knows they need it.
Forensic recovery from a powered-off seized box
Section titled “Forensic recovery from a powered-off seized box”The dead-man’s switch handles the running daemon’s state; for the disk itself, the optional LUKS storage container encrypts /var/lib/gateway (the warm tier — SQLite DB, audit log, backups, secrets table) under a passphrase you provide.
# create a 256 MB vault — default size; pass "size_mb" to overridecurl -X POST https://gateway.lan:8080/api/v1/storage/encrypted/setup \ -H "Authorization: Bearer $API_KEY" \ -d '{"passphrase":"REDACTED","size_mb":256}'# daemon must restart; passphrase is required at boot to unlock
# check current statecurl https://gateway.lan:8080/api/v1/storage/encrypted \ -H "Authorization: Bearer $API_KEY"# → {"enabled":true,"file_exists":true,"mapped":true,"mounted":true,"file":"/opt/gateway/vault.img","mount_point":"/var/lib/gateway","size_mb":"256"}Mechanics: file-backed LUKS dm-crypt at /opt/gateway/vault.img, mounted over /var/lib/gateway via the gateway-vault mapper. Only the warm tier is encrypted — the hot tier (tmpfs) is unaffected by design so the daemon’s runtime state is unencrypted in memory regardless of the disk posture.
Default is disabled. The trade-off is honest:
- What you gain. A seized powered-off device with the vault file but no passphrase exposes ciphertext only. Audit log, secrets, federation state, and routing policies aren’t recoverable.
- What you pay. SD/NVMe wear amplifies (every write goes through the LUKS layer’s overwrite-in-place semantics rather than the filesystem’s native journaling). Don’t enable on a deployment with high write churn unless you’ve sized the storage for it.
- What it doesn’t help with. A running daemon has the vault unlocked; live extraction of the running process or a memory image bypasses LUKS entirely. Combine with the dead-man’s switch above for that case.
The teardown endpoint (POST /api/v1/storage/encrypted/teardown) removes the vault file and restores plaintext /var/lib/gateway.
What it does not protect against
Section titled “What it does not protect against”Compromise of the device behind The Gateway
Section titled “Compromise of the device behind The Gateway”If a laptop on your LAN is owned, the attacker has the user’s authenticated sessions, browser state, and any application secrets stored on disk. The Gateway only protects what crosses its boundary. Endpoint security is a separate problem.
Compromise of gatewayd itself
Section titled “Compromise of gatewayd itself”If the daemon binary or /etc/gateway/ are compromised, every guarantee here is void. Protect the daemon with the same care you’d give a CA: read-only root filesystem, sealed boot, separate audit aggregator, restricted SSH.
A malicious VPN provider at the tunnel exit
Section titled “A malicious VPN provider at the tunnel exit”VPN providers see your unencrypted egress (post-tunnel). The Gateway encrypts to them; it doesn’t audit them. Pick exits you trust. Splitting across multiple providers reduces single-provider correlation but doesn’t eliminate it.
Global passive adversaries with traffic-correlation capability
Section titled “Global passive adversaries with traffic-correlation capability”If an adversary can observe both your tunnel ingress AND every exit of the providers you use, they can correlate flow timing and volume. Privacy levels 4–5 raise the cost but don’t make it impossible. This is the same threat model Tor users live with.
Application-layer fingerprinting
Section titled “Application-layer fingerprinting”A browser’s TLS handshake, supported ciphers, JA3/JA4 fingerprints, and HTTP/2 settings are visible to the exit. They’re outside the daemon’s reach. Use Tor / Tor Browser when this matters.
A determined active adversary with kernel-level access
Section titled “A determined active adversary with kernel-level access”If someone roots the daemon’s host, they can edit nftables and the reconciler will fight them — but they win every tick until they’re evicted. The kill-switch is not magical; it’s a regular kernel-mode rule.
Real-time identity correlation by your VPN provider
Section titled “Real-time identity correlation by your VPN provider”If your VPN provider logs source IP + destination per connection, no amount of splitting or cover traffic helps. Choose providers with a verified no-logs policy and / or pay anonymously.
Side-channels on the host itself
Section titled “Side-channels on the host itself”A shared-tenancy VPS, a misconfigured cloud-init script that exposes the daemon to the internet, an unrelated service on the same box that allows arbitrary code execution — these are all outside the daemon’s scope.
Compromise of a federation peer
Section titled “Compromise of a federation peer”Federation syncs config between peers (operator-triggered or via the per-peer prober). If your primary is malicious, your secondaries will obediently apply a hostile policy on the next sync. Treat the primary as a privileged role: separate ops accounts, separate audit, restricted access.
Loss of your backup passphrase
Section titled “Loss of your backup passphrase”Backups are encrypted with AES-256-GCM, key derived from a passphrase via PBKDF2-HMAC-SHA256 (100 000 iterations, per-export salt). If you lose the passphrase, the backup is unrecoverable. The daemon never logs and never escrows it — that’s deliberate.
Network-layer guarantees, end to end
Section titled “Network-layer guarantees, end to end”The Gateway makes these specific promises:
- Outbound IP never leaks to the default route for traffic matched by a
fail_policy: fail-closedpolicy whose tunnel is down. - Outbound DNS for any device with
force_dns: trueand / or system-wideforce_dnsis answered by the integrated resolver, with filtering applied, even if the device hard-codes a public resolver. - DoH bypass by browsers is blocked when
block_doh: trueis on system-wide, for every known DoH provider in the bundled list. - WebRTC IP exposure via STUN is blocked for devices in privacy classes with
block_webrtc: true. - IPv6 leakage is blocked for devices in privacy classes with
ipv6_policy: block. - Per-policy state isolation: traffic for policy A cannot bleed into policy B’s routing table or kill-switch state. Even if one policy’s reconciliation fails, others continue to be enforced.
- Crash safety: a daemon crash mid-write does not leak — SQLite is WAL-mode, and the daemon does not
SIGKILLon shutdown. - Per-tunnel namespace isolation: each tunnel runs in its own
netnsconnected by avethpair. A per-tunnel sidecar (redsocks, openvpn client) can’t touch another tunnel’s interface, routes, or rules — the kernel boundary is hard.
Outside of these, you are operating without a guarantee.
Recommended posture for an operator
Section titled “Recommended posture for an operator”- Run the daemon on a dedicated box. Don’t multi-tenant with Docker,
ufw,firewalld, or anything else that mutates nftables — the reconciler will fight whatever else touches the kernel. - Restrict the API to
auth.local_net(loopback / management VLAN) or put a reverse proxy in front that requires mutual TLS. Never expose the API directly to the public internet. - Pick at least two unrelated VPN providers for your pool. Provider diversity is part of the threat model.
- Bind sensitive policies (work email, banking, source-of-truth federation primary) to
fail_policy: fail-closed. Accept the brief outage trade-off. - Keep the daemon updated. The reconciler protects against drift, not against bugs in the daemon’s own logic.
- Take regular encrypted backups. Verify the restore once a quarter on a spare box.
Related
Section titled “Related”- Privacy & kill-switch — the machinery behind the guarantees above.
- Traffic splitting — what each privacy level actually defends against.
- FAQ — common privacy questions.