From 00e28dbbc9cd4168e47226043928bbdeb83d2a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sun, 5 Jul 2026 12:19:57 -0400 Subject: [PATCH 1/2] Document X-Real-IP trust + warn on insecure OPRF ws (security) --- nixos/frost-gate.nix | 7 ++++++- nixos/ingress.nix | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/nixos/frost-gate.nix b/nixos/frost-gate.nix index f0e9e3b..af246a4 100644 --- a/nixos/frost-gate.nix +++ b/nixos/frost-gate.nix @@ -742,7 +742,12 @@ in "/dev/disk/by-partuuid/" ]) ) - "keepNode.frostGate.volumeDevice (${cfg.volumeDevice}) is not a stable /dev/disk/by-id or /dev/disk/by-uuid path. On real hardware a kernel-enumeration name (or topology-based /dev/disk/by-path) can re-point at a different disk, and first-boot provisioning could format the wrong device. See the volumeDevice option docs."; + "keepNode.frostGate.volumeDevice (${cfg.volumeDevice}) is not a stable /dev/disk/by-id or /dev/disk/by-uuid path. On real hardware a kernel-enumeration name (or topology-based /dev/disk/by-path) can re-point at a different disk, and first-boot provisioning could format the wrong device. See the volumeDevice option docs." + # Production tripwire: allowInsecureWs disables the ws->wss upgrade guard on the boot OPRF + # exchange, so the unlock share/response travels in plaintext and is MITM-able by a hostile relay + # or anyone on the network path. It is test-only; surface it loudly so it can never ship enabled + # by accident. + ++ lib.optional cfg.allowInsecureWs "keepNode.frostGate.allowInsecureWs is ENABLED: the boot threshold-OPRF unlock exchange runs over plaintext ws:// (KEEP_ALLOW_WS), so a hostile relay or on-path attacker can observe or tamper with the unlock. This is TEST-ONLY and must never be set on a real deployment."; assertions = [ { diff --git a/nixos/ingress.nix b/nixos/ingress.nix index df4ab7d..0f41a22 100644 --- a/nixos/ingress.nix +++ b/nixos/ingress.nix @@ -106,6 +106,14 @@ in # Vaultwarden must know its public URL (links, WebAuthn, etc.) and must trust the proxy's # forwarded client IP, so its log (and thus fail2ban) bans the real attacker, not 127.0.0.1. + # + # SECURITY ASSUMPTION: IP_HEADER trusts X-Real-IP UNCONDITIONALLY. That is safe here ONLY because + # nginx is the SOLE reacher of Vaultwarden's loopback port (8222): Vaultwarden binds 127.0.0.1 + # (vaultwarden.nix), nginx's recommendedProxySettings overwrites X-Real-IP with the real connection + # address on every proxied request, and no other loopback path terminates there. If a future change + # lets anything else reach 127.0.0.1:8222 (e.g. a mesh->localhost termination, or a local process), + # it could forge X-Real-IP to make fail2ban ban an attacker-chosen victim IP or dodge its own ban. + # Keep Vaultwarden's loopback port reachable by nginx only. services.vaultwarden.config = { DOMAIN = "https://${cfg.hostName}"; IP_HEADER = "X-Real-IP"; From 61e52c3afdba4eb7b8289b12ccf0943f907d7589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kyle=20=F0=9F=90=86?= Date: Sun, 5 Jul 2026 12:30:57 -0400 Subject: [PATCH 2/2] Reframe Vaultwarden X-Real-IP trust as operational assumption (security) --- nixos/ingress.nix | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nixos/ingress.nix b/nixos/ingress.nix index 0f41a22..49d613e 100644 --- a/nixos/ingress.nix +++ b/nixos/ingress.nix @@ -107,13 +107,16 @@ in # Vaultwarden must know its public URL (links, WebAuthn, etc.) and must trust the proxy's # forwarded client IP, so its log (and thus fail2ban) bans the real attacker, not 127.0.0.1. # - # SECURITY ASSUMPTION: IP_HEADER trusts X-Real-IP UNCONDITIONALLY. That is safe here ONLY because - # nginx is the SOLE reacher of Vaultwarden's loopback port (8222): Vaultwarden binds 127.0.0.1 - # (vaultwarden.nix), nginx's recommendedProxySettings overwrites X-Real-IP with the real connection - # address on every proxied request, and no other loopback path terminates there. If a future change - # lets anything else reach 127.0.0.1:8222 (e.g. a mesh->localhost termination, or a local process), - # it could forge X-Real-IP to make fail2ban ban an attacker-chosen victim IP or dodge its own ban. - # Keep Vaultwarden's loopback port reachable by nginx only. + # SECURITY ASSUMPTION: IP_HEADER trusts X-Real-IP UNCONDITIONALLY. nginx's recommendedProxySettings + # overwrites X-Real-IP with the real connection address on every proxied request, so no *remote* + # client can forge it. That reduces the trust boundary to Vaultwarden's loopback port: Vaultwarden + # binds 127.0.0.1 (vaultwarden.nix), which keeps the port off the network but does NOT structurally + # restrict it to nginx. This config therefore rests on an OPERATIONAL ASSUMPTION, not an enforced + # control: that no other local process (and no future mesh->localhost or loopback termination) can + # reach Vaultwarden's loopback port. Any local process that can connect there could forge X-Real-IP + # to make fail2ban ban an attacker-chosen victim IP or dodge its own ban. If that assumption ever + # weakens, make it structural (e.g. a unix-domain socket or dedicated netns for the nginx<->Vaultwarden + # hop) rather than relying on loopback binding alone. services.vaultwarden.config = { DOMAIN = "https://${cfg.hostName}"; IP_HEADER = "X-Real-IP";