From a244dab94ee1b6285d036fdec6a13bc2d475b70d Mon Sep 17 00:00:00 2001 From: randogoth Date: Sat, 26 Sep 2026 20:11:58 +0300 Subject: [PATCH] feat: add a Caddy reverse-proxy option to the NixOS module --- README.md | 2 +- nix/module.nix | 48 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 304f9d3..7333a5b 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ For a NixOS host, `nixosModules.default` provides `services.gsmol-bridge`: } ``` -The service runs under `DynamicUser`, restarts on failure, and — like the standalone script — listens on `127.0.0.1` by default. `nginx.enable` puts a TLS-terminating reverse proxy in front rather than exposing the bridge directly: it has no TLS of its own, and a browser refuses a `ws://` connection from an `https://` page, so a real deployment needs the proxy regardless of `--allow-origin`. `openFirewall = true` is for exposing the bridge's port directly instead, which is not the recommended path (see "Notes and limits"). +The service runs under `DynamicUser`, restarts on failure, and — like the standalone script — listens on `127.0.0.1` by default. `nginx.enable` (or `caddy.enable`, wiring into `services.caddy.virtualHosts` — automatic HTTPS, no `enableACME`/`forceSSL` equivalent to set) puts a TLS-terminating reverse proxy in front rather than exposing the bridge directly: it has no TLS of its own, and a browser refuses a `ws://` connection from an `https://` page, so a real deployment needs one of the two regardless of `--allow-origin`. Enable only one; a deployment that replaces `services.caddy.configFile` with a hand-written Caddyfile instead of using `virtualHosts` needs the domain added there directly, proxying to `host`:`port`. `openFirewall = true` is for exposing the bridge's port directly instead of either, which is not the recommended path (see "Notes and limits"). `services.gsmol-bridge.presetServer = { host, publicKey }` bakes a server's key into the pages this deployment serves, so a first-time visitor is pre-pinned instead of asked to copy a base32 key into settings by hand (§4). The value reaches the browser over this deployment's own TLS, so it only stands in for "from the operator, through a trusted channel" when the deployment operator and the smolmaild operator are the same trusted party — set it only for a server you operate or otherwise vouch for. It seeds the first run only: the browser stores it as an ordinary pin from then on, replaceable or removable in settings like any other. diff --git a/nix/module.nix b/nix/module.nix index 3f02147..f8d0f74 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -72,8 +72,9 @@ in Address the bridge listens on. The bridge speaks plain HTTP/WS with no TLS of its own, and browsers refuse a `ws://` connection from an `https://` page — so leave this at loopback and put a reverse proxy - (see `services.gsmol-bridge.nginx`) in front for TLS and public - exposure, rather than binding this directly to a public interface. + (`services.gsmol-bridge.nginx` or `.caddy`) in front for TLS and + public exposure, rather than binding this directly to a public + interface. ''; }; @@ -123,10 +124,42 @@ in description = "Public hostname to serve gsmol from."; }; }; + + caddy = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Put a Caddy virtual host in front of the bridge, with automatic + HTTPS — no `enableACME`/`forceSSL` to set, Caddy does this itself + for any site address that isn't `http://`-prefixed. + + This wires into the standard `services.caddy.virtualHosts` option + and has no effect if your own configuration replaces + `services.caddy.configFile` outright with a hand-written Caddyfile + — that style of deployment needs the domain added to that file + instead (e.g. an imported drop-in), proxying to + `services.gsmol-bridge.host`:`.port`. + ''; + }; + + domain = lib.mkOption { + type = lib.types.str; + example = "mail.example.org"; + description = "Public hostname to serve gsmol from."; + }; + }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { + assertions = [ + { + assertion = !(cfg.nginx.enable && cfg.caddy.enable); + message = "services.gsmol-bridge: enable only one of .nginx or .caddy"; + } + ]; + systemd.services.gsmol-bridge = { description = "gsmol web client and Smol Mail bridge"; wantedBy = [ "multi-user.target" ]; @@ -186,5 +219,16 @@ in }; }; }) + + (lib.mkIf cfg.caddy.enable { + services.caddy.enable = true; + # No proxyWebsockets-style flag needed: Caddy's reverse_proxy upgrades a + # WebSocket connection transparently, and it self-provisions TLS for a + # non-http:// site address, so neither forceSSL nor enableACME has a + # caddy-side equivalent to set here. + services.caddy.virtualHosts.${cfg.caddy.domain}.extraConfig = '' + reverse_proxy ${cfg.host}:${toString cfg.port} + ''; + }) ]); }