{ config, lib, ... }: let cfg = config.modules.reverse-proxy; routeType = lib.types.submodule ( { name, ... }: { options = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Whether this reverse-proxy route is active."; }; host = lib.mkOption { type = lib.types.str; default = name; defaultText = lib.literalMD "the route attribute name"; description = "Primary hostname served by this route."; }; aliases = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ ]; description = "Additional hostnames served by the primary route."; }; path = lib.mkOption { type = lib.types.str; default = "/"; description = "URL path prefix proxied by this route."; }; backend = lib.mkOption { type = lib.types.str; description = "Full HTTP backend URL that receives proxied traffic."; }; tls = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to serve this route with ACME-backed TLS."; }; websockets = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to enable websocket proxy headers for this route."; }; }; } ); enabledRoutes = lib.filterAttrs (_: route: route.enable) cfg.routes; routesByHost = lib.groupBy (route: route.host) (lib.attrValues enabledRoutes); rootRoutesByHost = lib.mapAttrs ( host: routes: let roots = builtins.filter (route: route.path == "/") routes; in if roots == [ ] then null else builtins.head roots ) routesByHost; hasDuplicatePaths = routes: let paths = map (route: route.path) routes; in lib.length paths != lib.length (lib.unique paths); mkLocation = route: { proxyPass = route.backend; proxyWebsockets = route.websockets; }; mkVirtualHost = host: routes: let rootRoute = rootRoutesByHost.${host}; in lib.mkIf (rootRoute != null) { forceSSL = rootRoute.tls; enableACME = rootRoute.tls; serverAliases = rootRoute.aliases; locations = lib.listToAttrs ( map (route: lib.nameValuePair route.path (mkLocation route)) routes ); }; in { options.modules.reverse-proxy = { enable = lib.mkEnableOption "the Nginx reverse proxy route realizer"; routes = lib.mkOption { type = lib.types.attrsOf routeType; default = { }; description = "Reverse-proxy route declarations keyed by stable route name."; }; }; config = lib.mkIf cfg.enable { assertions = lib.mapAttrsToList (host: rootRoute: { assertion = rootRoute != null; message = "modules.reverse-proxy.routes for ${host} must include a primary / route."; }) rootRoutesByHost ++ lib.mapAttrsToList (host: routes: { assertion = !hasDuplicatePaths routes; message = "modules.reverse-proxy.routes for ${host} must not declare the same path more than once."; }) routesByHost; networking.firewall.allowedTCPPorts = [ 80 443 ]; security.acme = { acceptTerms = true; defaults.email = "contact@alexion.dev"; }; services.nginx = { enable = true; recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; virtualHosts = lib.mapAttrs mkVirtualHost routesByHost; }; }; }