143 lines
4.7 KiB
Nix
143 lines
4.7 KiB
Nix
# NixOS module for running the gsmol bridge as a service. Curried on `self` so
|
|
# the systemd unit's default package is exactly what `nix build .#default`
|
|
# produces for the target system, with one definition to keep in sync.
|
|
self:
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.gsmol-bridge;
|
|
in
|
|
{
|
|
options.services.gsmol-bridge = {
|
|
enable = lib.mkEnableOption "the gsmol web client and Smol Mail relay bridge";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = self.packages.${pkgs.system}.default;
|
|
defaultText = lib.literalExpression "gsmol.packages.\${system}.default";
|
|
description = "The gsmol-bridge package to run.";
|
|
};
|
|
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = ''
|
|
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.
|
|
'';
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8096;
|
|
description = "Port the bridge listens on.";
|
|
};
|
|
|
|
allowPorts = lib.mkOption {
|
|
type = lib.types.listOf lib.types.port;
|
|
default = [ 1961 ];
|
|
description = ''
|
|
TCP ports the bridge is allowed to relay WebSocket bytes to
|
|
(smolmaild's port on each server your users register with).
|
|
'';
|
|
};
|
|
|
|
allowOrigins = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [ "https://mail.example.org" ];
|
|
description = ''
|
|
Extra browser origins permitted to open a relay, beyond the bridge's
|
|
own. The bridge always refuses a WebSocket upgrade whose `Origin` is
|
|
neither absent nor its own page, so a reverse-proxied public domain
|
|
that differs from what the bridge itself sees needs listing here.
|
|
'';
|
|
};
|
|
|
|
openFirewall = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Open the firewall for `port`. Leave this off when
|
|
`services.gsmol-bridge.nginx.enable` (or another reverse proxy) is
|
|
doing the public exposure instead.
|
|
'';
|
|
};
|
|
|
|
nginx = {
|
|
enable = lib.mkEnableOption "an nginx virtual host in front of the bridge, with TLS via ACME";
|
|
|
|
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 [
|
|
{
|
|
systemd.services.gsmol-bridge = {
|
|
description = "gsmol web client and Smol Mail bridge";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
|
|
serviceConfig = {
|
|
ExecStart = lib.escapeShellArgs ([
|
|
(lib.getExe cfg.package)
|
|
"--host" cfg.host
|
|
"--port" (toString cfg.port)
|
|
]
|
|
++ lib.concatMap (p: [ "--allow-port" (toString p) ]) cfg.allowPorts
|
|
++ lib.concatMap (o: [ "--allow-origin" o ]) cfg.allowOrigins);
|
|
|
|
DynamicUser = true;
|
|
Restart = "on-failure";
|
|
RestartSec = "2s";
|
|
|
|
# No key material, no writes: bridge.py holds no keys and touches
|
|
# no filesystem state beyond serving web/ read-only (README, "why
|
|
# there is a bridge").
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
NoNewPrivileges = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectControlGroups = true;
|
|
ProtectClock = true;
|
|
ProtectHostname = true;
|
|
ProtectProc = "invisible";
|
|
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
LockPersonality = true;
|
|
MemoryDenyWriteExecute = true;
|
|
RemoveIPC = true;
|
|
UMask = "0077";
|
|
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
|
SystemCallArchitectures = "native";
|
|
};
|
|
};
|
|
|
|
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ];
|
|
}
|
|
|
|
(lib.mkIf cfg.nginx.enable {
|
|
services.nginx.enable = true;
|
|
services.nginx.virtualHosts.${cfg.nginx.domain} = {
|
|
forceSSL = true;
|
|
enableACME = true;
|
|
locations."/" = {
|
|
proxyPass = "http://${cfg.host}:${toString cfg.port}";
|
|
proxyWebsockets = true;
|
|
};
|
|
};
|
|
})
|
|
]);
|
|
}
|