{ lib, config, ... }: { options.services.reverseProxy = { enable = lib.mkEnableOption "turn on the reverse proxy"; openFirewall = lib.mkEnableOption "open the firewall"; ports = { http = lib.mkOption { type = lib.types.port; description = "HTTP port for the reverse proxy"; default = 80; }; https = lib.mkOption { type = lib.types.port; description = "HTTPS port for the reverse proxy"; default = 443; }; }; acme = { enable = lib.mkOption { type = lib.types.bool; description = "enable ACME certificate management"; default = true; }; email = lib.mkOption { type = lib.types.str; description = "email address for ACME certificate registration"; }; }; services = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule ({name, ...}: { options = { target = lib.mkOption { type = lib.types.str; description = "what url will all traffic to this application be forwarded to"; }; domain = lib.mkOption { type = lib.types.str; description = "what is the default subdomain to be used for this application to be used for"; default = name; }; extraDomains = lib.mkOption { type = lib.types.listOf lib.types.str; description = "extra domains that should be configured for this domain"; default = []; }; settings = { certificateRenewal.enable = lib.mkOption { type = lib.types.bool; description = "auto renew certificates"; default = true; }; forceSSL.enable = lib.mkOption { type = lib.types.bool; description = "auto renew certificates"; default = true; }; proxyHeaders = { enable = lib.mkEnableOption "should we proxy headers"; timeout = lib.mkOption { type = lib.types.int; default = 60; }; }; proxyWebsockets.enable = lib.mkEnableOption "should the default config proxy websockets"; forwardHeaders.enable = lib.mkEnableOption "should the default config contain forward headers"; noSniff.enable = lib.mkEnableOption "should the no sniff flags be set"; proxyBuffering.enable = lib.mkOption { type = lib.types.bool; description = "should proxy buffering be enabled"; default = true; }; maxBodySize = lib.mkOption { type = lib.types.nullOr lib.types.int; description = ""; default = null; }; }; }; })); }; }; config = let httpPort = config.services.reverseProxy.ports.http; httpsPort = config.services.reverseProxy.ports.https; in lib.mkIf config.services.reverseProxy.enable { security.acme = lib.mkIf config.services.reverseProxy.acme.enable { acceptTerms = true; defaults.email = config.services.reverseProxy.acme.email; }; services.nginx = { enable = true; virtualHosts = lib.mkMerge ( lib.lists.flatten ( lib.attrsets.mapAttrsToList ( name: service: let hostConfig = { forceSSL = service.settings.forceSSL.enable; enableACME = service.settings.certificateRenewal.enable; locations = { "/" = { proxyPass = service.target; proxyWebsockets = service.settings.proxyWebsockets.enable; recommendedProxySettings = service.settings.forwardHeaders.enable; extraConfig = let # Client upload size configuration maxBodySizeConfig = lib.optionalString (service.settings.maxBodySize != null) "client_max_body_size ${toString service.settings.maxBodySize}M;"; # Security header configuration noSniffConfig = lib.optionalString service.settings.noSniff.enable "add_header X-Content-Type-Options nosniff;"; # Proxy buffering configuration proxyBufferingConfig = lib.optionalString (!service.settings.proxyBuffering.enable) "proxy_buffering off;"; # Proxy timeout configuration proxyTimeoutConfig = lib.optionalString service.settings.proxyHeaders.enable '' proxy_read_timeout ${toString service.settings.proxyHeaders.timeout}s; proxy_connect_timeout ${toString service.settings.proxyHeaders.timeout}s; proxy_send_timeout ${toString service.settings.proxyHeaders.timeout}s; ''; in maxBodySizeConfig + noSniffConfig + proxyBufferingConfig + proxyTimeoutConfig; }; }; }; in ( [ { ${service.domain} = hostConfig; } ] ++ builtins.map (domain: {${domain} = hostConfig;}) service.extraDomains ) ) config.services.reverseProxy.services ) ); }; networking.firewall.allowedTCPPorts = lib.mkIf config.services.reverseProxy.openFirewall [ httpPort httpsPort ]; }; }