diff --git a/machines/newton/services.nix b/machines/newton/services.nix index bca5bcd..a1b58ad 100644 --- a/machines/newton/services.nix +++ b/machines/newton/services.nix @@ -136,21 +136,8 @@ in blackbox = { enable = true; }; - # Webserver - nginx = { + webserver = { enable = true; - sso = { - authKeyFile = secrets."sso/auth-key".path; - users = { - felix = { - passwordHashFile = secrets."sso/felix/password-hash".path; - totpSecretFile = secrets."sso/felix/totp-secret".path; - }; - }; - groups = { - root = [ "felix" ]; - }; - }; }; acme = { enable = true; diff --git a/machines/serverle/services.nix b/machines/serverle/services.nix index d20ff2e..5973e11 100644 --- a/machines/serverle/services.nix +++ b/machines/serverle/services.nix @@ -86,21 +86,8 @@ in homer = { enable = true; }; - # Webserver - nginx = { + webserver = { enable = true; - sso = { - authKeyFile = secrets."sso/auth-key".path; - users = { - felix = { - passwordHashFile = secrets."sso/felix/password-hash".path; - totpSecretFile = secrets."sso/felix/totp-secret".path; - }; - }; - groups = { - root = [ "felix" ]; - }; - }; }; acme = { enable = true; diff --git a/modules/services/acme/default.nix b/modules/services/acme/default.nix index 73bb332..13bcfd6 100644 --- a/modules/services/acme/default.nix +++ b/modules/services/acme/default.nix @@ -25,8 +25,8 @@ in acceptTerms = true; # Use DNS wildcard certificate certs = { - "${config.networking.domain}" = { - extraDomainNames = [ "*.${config.networking.domain}" ]; + "${domain}" = { + extraDomainNames = [ "*.${domain}" ]; dnsProvider = "inwx"; inherit (cfg) credentialsFile; }; diff --git a/modules/services/alertmanager/default.nix b/modules/services/alertmanager/default.nix index 8fba576..aa6f70a 100644 --- a/modules/services/alertmanager/default.nix +++ b/modules/services/alertmanager/default.nix @@ -132,7 +132,7 @@ in }; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "alerts"; inherit (cfg) port; diff --git a/modules/services/aria2/default.nix b/modules/services/aria2/default.nix index 3251f11..687c6d5 100644 --- a/modules/services/aria2/default.nix +++ b/modules/services/aria2/default.nix @@ -28,7 +28,7 @@ in inherit (cfg) downloadDir; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "download"; root = "${pkgs.ariang}/share/ariang"; diff --git a/modules/services/bazarr/default.nix b/modules/services/bazarr/default.nix index a45bf0f..90e2f0c 100644 --- a/modules/services/bazarr/default.nix +++ b/modules/services/bazarr/default.nix @@ -43,7 +43,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "subtitles"; inherit port; diff --git a/modules/services/default.nix b/modules/services/default.nix index cc842da..4a0de1a 100644 --- a/modules/services/default.nix +++ b/modules/services/default.nix @@ -27,7 +27,6 @@ ./mumble-server ./navidrome ./nextcloud - ./nginx ./node-exporter ./octoprint ./paperless @@ -43,5 +42,6 @@ ./ssh-server ./tandoor-recipes ./vpn + ./webserver ]; } diff --git a/modules/services/finance/default.nix b/modules/services/finance/default.nix index ef54a55..a6a40f3 100644 --- a/modules/services/finance/default.nix +++ b/modules/services/finance/default.nix @@ -1,5 +1,10 @@ # finance overview -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.my.services.finance; inherit (config.networking) domain; @@ -20,18 +25,26 @@ in services.firefly-iii = { enable = true; virtualHost = "finance"; - enableNginx = true; + user = "caddy"; + group = "caddy"; settings = { APP_KEY_FILE = cfg.appKeyFile; SITE_OWNER = "server@buehler.rocks"; }; }; - services.nginx.virtualHosts."finance" = { - serverName = "finance.${domain}"; - forceSSL = true; - useACMEHost = domain; - }; + my.services.webserver.virtualHosts = [ + { + subdomain = "finance"; + extraConfig = '' + file_server + root * "${config.services.firefly-iii.package}/public" + php_fastcgi unix/${config.services.phpfpm.pools."firefly-iii".socket} { + env modHeadersAvailable true + } + ''; + } + ]; webapps.apps.finance = { dashboard = { diff --git a/modules/services/freshrss/default.nix b/modules/services/freshrss/default.nix index d760bef..0c61ba8 100644 --- a/modules/services/freshrss/default.nix +++ b/modules/services/freshrss/default.nix @@ -45,17 +45,27 @@ in enable = true; baseUrl = "https://news.${domain}"; inherit (cfg) language passwordFile defaultUser; + virtualHost = null; }; - # Set up a Nginx virtual host. - services.nginx = { - virtualHosts."freshrss" = { - serverName = "news.${domain}"; - forceSSL = true; - useACMEHost = domain; - }; + services.phpfpm.pools.freshrss.settings = { + "listen.owner" = lib.mkForce config.services.caddy.user; + "listen.group" = lib.mkForce config.services.caddy.group; }; + my.services.webserver.virtualHosts = [ + { + subdomain = "news"; + extraConfig = '' + root * ${config.services.freshrss.package}/p + php_fastcgi unix/${config.services.phpfpm.pools.freshrss.socket} { + env FRESHRSS_DATA_PATH ${config.services.freshrss.dataDir} + } + file_server + ''; + } + ]; + webapps.apps.freshrss = { dashboard = { name = "News"; diff --git a/modules/services/gitea/default.nix b/modules/services/gitea/default.nix index b1fa29d..5638211 100644 --- a/modules/services/gitea/default.nix +++ b/modules/services/gitea/default.nix @@ -66,7 +66,7 @@ in # Proxy to Gitea my.services = { - nginx.virtualHosts = [ + webserver.virtualHosts = [ { subdomain = "code"; inherit (cfg) port; diff --git a/modules/services/grafana/default.nix b/modules/services/grafana/default.nix index 01ce199..e9473bf 100644 --- a/modules/services/grafana/default.nix +++ b/modules/services/grafana/default.nix @@ -79,7 +79,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "visualization"; inherit (cfg) port; diff --git a/modules/services/hedgedoc/default.nix b/modules/services/hedgedoc/default.nix index 6529ccc..2e47600 100644 --- a/modules/services/hedgedoc/default.nix +++ b/modules/services/hedgedoc/default.nix @@ -76,7 +76,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "notes"; inherit (cfg) port; diff --git a/modules/services/home-automation/default.nix b/modules/services/home-automation/default.nix index be90127..f07df03 100644 --- a/modules/services/home-automation/default.nix +++ b/modules/services/home-automation/default.nix @@ -145,7 +145,7 @@ in }; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "automation"; inherit (cfg) port; diff --git a/modules/services/homepage/default.nix b/modules/services/homepage/default.nix index b028a3f..be075e4 100644 --- a/modules/services/homepage/default.nix +++ b/modules/services/homepage/default.nix @@ -16,7 +16,7 @@ in config = lib.mkIf cfg.enable { - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "blog"; root = inputs.stunkymonkey.packages.${config.nixpkgs.system}.default; diff --git a/modules/services/homer/default.nix b/modules/services/homer/default.nix index 8a613c6..02ef796 100644 --- a/modules/services/homer/default.nix +++ b/modules/services/homer/default.nix @@ -26,24 +26,18 @@ in }; config = lib.mkIf cfg.enable { - services.nginx.virtualHosts = { - # This is not a subdomain, cannot use my nginx wrapper module - ${domain} = { - forceSSL = true; - useACMEHost = domain; - # TODO: 25.05 use stable - root = pkgs.unstable.homer; - locations."=/assets/config.yml" = { - alias = pkgs.writeText "homerConfig.yml" (builtins.toJSON homeConfig); - }; - }; - # redirect any other attempt to the main site - "${domain}-redirect" = { - forceSSL = true; - useACMEHost = domain; - default = true; - globalRedirect = "${domain}"; - }; + # TODO: 25.05 use stable + services.caddy.virtualHosts.${domain} = { + extraConfig = '' + import common + root * ${pkgs.unstable.homer} + file_server + handle_path /assets/config.yml { + root * ${pkgs.writeText "homerConfig.yml" (builtins.toJSON homeConfig)} + file_server + } + ''; + useACMEHost = domain; }; webapps = { diff --git a/modules/services/jellyfin/default.nix b/modules/services/jellyfin/default.nix index 1203a68..601563c 100644 --- a/modules/services/jellyfin/default.nix +++ b/modules/services/jellyfin/default.nix @@ -47,7 +47,7 @@ in }; # sadly the metrics do not contain application specific metrics, only c# -> no dashboard - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "media"; inherit port; diff --git a/modules/services/jellyseerr/default.nix b/modules/services/jellyseerr/default.nix index c3701b0..897f6c2 100644 --- a/modules/services/jellyseerr/default.nix +++ b/modules/services/jellyseerr/default.nix @@ -14,7 +14,7 @@ in enable = true; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "view"; inherit (config.services.jellyseerr) port; diff --git a/modules/services/mumble-server/default.nix b/modules/services/mumble-server/default.nix index e331c78..4721804 100644 --- a/modules/services/mumble-server/default.nix +++ b/modules/services/mumble-server/default.nix @@ -7,11 +7,11 @@ }: let cfg = config.my.services.mumble-server; - domain = "voice.${config.networking.domain}"; + inherit (config.networking) domain; in { options.my.services.mumble-server = { - enable = lib.mkEnableOption "RSS-Bridge service"; + enable = lib.mkEnableOption "mumble server service"; }; config = lib.mkIf cfg.enable { @@ -19,29 +19,29 @@ in enable = true; openFirewall = true; welcometext = "Welcome to the Mumble-Server!"; - sslCert = "/var/lib/acme/${domain}/fullchain.pem"; - sslKey = "/var/lib/acme/${domain}/key.pem"; + sslCert = "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; + sslKey = "${config.security.acme.certs.${domain}.directory}/key.pem"; }; - services.nginx.virtualHosts.${domain}.enableACME = true; - security.acme.certs."${domain}" = { - group = "voice-buehler-rocks"; - postRun = '' - if ${pkgs.systemd}/bin/systemctl is-active murmur.service; then - ${pkgs.systemd}/bin/systemctl kill -s SIGUSR1 murmur.service - fi - ''; + # create a separate certificate for the mumble server + security.acme = { + certs.${domain} = { + reloadServices = [ "murmur" ]; + group = "caddyandmurmur"; + }; }; - - users.groups."voice-buehler-rocks".members = [ + users.groups.caddyandmurmur.members = [ + "caddy" "murmur" - "nginx" ]; - my.services.prometheus.rules = { - mumble_not_running = { - condition = ''systemd_unit_state{name="murmur.service", state!="active"} > 0''; - description = "{{$labels.host}} should have a running {{$labels.name}}"; + my.services = { + acme.enable = true; + prometheus.rules = { + mumble_not_running = { + condition = ''systemd_unit_state{name="murmur.service", state!="active"} > 0''; + description = "{{$labels.host}} should have a running {{$labels.name}}"; + }; }; }; }; diff --git a/modules/services/navidrome/default.nix b/modules/services/navidrome/default.nix index 321be3e..5c341b3 100644 --- a/modules/services/navidrome/default.nix +++ b/modules/services/navidrome/default.nix @@ -89,7 +89,7 @@ in }; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "music"; inherit (cfg) port; @@ -101,7 +101,8 @@ in name = "Music"; category = "media"; icon = "music"; - url = "https://music.${domain}/app/#/login"; + url = "https://music.${domain}"; + method = "get"; }; }; }; diff --git a/modules/services/nextcloud/default.nix b/modules/services/nextcloud/default.nix index 8f77b87..dcc9f4f 100644 --- a/modules/services/nextcloud/default.nix +++ b/modules/services/nextcloud/default.nix @@ -97,17 +97,6 @@ in # ]; #}; - # The service above configures the domain, no need for my wrapper - nginx.virtualHosts."cloud.${domain}" = { - forceSSL = true; - useACMEHost = domain; - - # so homer can get the online status - extraConfig = lib.optionalString config.my.services.homer.enable '' - add_header Access-Control-Allow-Origin https://${domain}; - ''; - }; - prometheus.exporters.nextcloud = { enable = true; url = "https://cloud.${domain}"; @@ -144,6 +133,55 @@ in # requires = [ "postgresql.service" ]; # after = [ "postgresql.service" ]; #}; + services.phpfpm.pools.nextcloud.settings = { + "listen.owner" = config.services.caddy.user; + "listen.group" = config.services.caddy.group; + }; + + users.groups.nextcloud.members = [ + "nextcloud" + config.services.caddy.user + ]; + + my.services.webserver.virtualHosts = [ + { + subdomain = "cloud"; + extraConfig = '' + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 + + @forbidden { + path /.htaccess + path /data/* + path /config/* + path /db_structure + path /.xml + path /README + path /3rdparty/* + path /lib/* + path /templates/* + path /occ + path /console.php + } + respond @forbidden 403 + + header { + X-Frame-Options "sameorigin" + X-Permitted-Cross-Domain-Policies "none" + } + + # TODO: `config.services.nextcloud.package` does not contain additional apps. in nixpkgs there is "nextcloud-with-apps". + # for now we use the path passed to nginx. Can be improved in 25.05 via: https://github.com/NixOS/nixpkgs/pull/376818 + root * ${config.services.nginx.virtualHosts."cloud.${domain}".root} + file_server + php_fastcgi unix/${config.services.phpfpm.pools."nextcloud".socket} { + root ${config.services.nginx.virtualHosts."cloud.${domain}".root} + env front_controller_active true + env modHeadersAvailable true + } + ''; + } + ]; my.services.backup = { exclude = [ diff --git a/modules/services/nginx/default.nix b/modules/services/nginx/default.nix deleted file mode 100644 index 239fa0f..0000000 --- a/modules/services/nginx/default.nix +++ /dev/null @@ -1,452 +0,0 @@ -# A simple abstraction layer for almost all of my services' needs -{ - config, - lib, - pkgs, - ... -}: -let - cfg = config.my.services.nginx; - virtualHostOption = - with lib; - types.submodule { - options = { - subdomain = mkOption { - type = types.str; - example = "dev"; - description = '' - Which subdomain, under config.networking.domain, to use - for this virtual host. - ''; - }; - port = mkOption { - type = with types; nullOr port; - default = null; - example = 8080; - description = '' - Which port to proxy to, through localhost, for this virtual host. - This option is incompatible with `root`. - ''; - }; - root = mkOption { - type = with types; nullOr path; - default = null; - example = "/var/www/blog"; - description = '' - The root folder for this virtual host. This option is incompatible - with `port`. - ''; - }; - sso = { - enable = mkEnableOption "SSO authentication"; - }; - extraConfig = mkOption { - type = types.attrs; # FIXME: forward type of virtualHosts - example = literalExpression '' - { - locations."/socket" = { - proxyPass = "http://localhost:8096/"; - proxyWebsockets = true; - }; - } - ''; - default = { }; - description = '' - Any extra configuration that should be applied to this virtual host. - ''; - }; - }; - }; -in -{ - imports = [ ./sso ]; - options.my.services.nginx = with lib; { - enable = mkEnableOption "Nginx"; - acme = { - credentialsFile = mkOption { - type = types.str; - example = "/var/lib/acme/creds.env"; - description = '' - INWX API key file as an 'EnvironmentFile' (see `systemd.exec(5)`) - ''; - }; - }; - virtualHosts = mkOption { - type = types.listOf virtualHostOption; - default = [ ]; - example = literalExpression '' - [ - { - subdomain = "gitea"; - port = 8080; - } - { - subdomain = "dev"; - root = "/var/www/dev"; - } - { - subdomain = "jellyfin"; - port = 8096; - extraConfig = { - locations."/socket" = { - proxyPass = "http://localhost:8096/"; - proxyWebsockets = true; - }; - }; - } - ] - ''; - description = '' - List of virtual hosts to set-up using default settings. - ''; - }; - sso = { - authKeyFile = mkOption { - type = types.str; - example = "/var/lib/nginx-sso/auth-key.txt"; - description = '' - Path to the auth key. - ''; - }; - subdomain = mkOption { - type = types.str; - default = "login"; - example = "auth"; - description = "Which subdomain, to use for SSO."; - }; - port = mkOption { - type = types.port; - default = 8082; - example = 8080; - description = "Port to use for internal webui."; - }; - users = mkOption { - type = types.attrsOf ( - types.submodule { - options = { - passwordHashFile = mkOption { - type = types.str; - example = "/var/lib/nginx-sso/alice/password-hash.txt"; - description = "Path to file containing the user's password hash."; - }; - totpSecretFile = mkOption { - type = types.str; - example = "/var/lib/nginx-sso/alice/totp-secret.txt"; - description = "Path to file containing the user's TOTP secret."; - }; - }; - } - ); - example = literalExpression '' - { - alice = { - passwordHashFile = "/var/lib/nginx-sso/alice/password-hash.txt"; - totpSecretFile = "/var/lib/nginx-sso/alice/totp-secret.txt"; - }; - } - ''; - description = "Definition of users"; - }; - groups = mkOption { - type = with types; attrsOf (listOf str); - example = literalExpression '' - { - root = [ "alice" ]; - users = [ "alice" "bob" ]; - } - ''; - description = "Groups of users"; - }; - }; - }; - config = lib.mkIf cfg.enable { - assertions = lib.flip builtins.map cfg.virtualHosts ( - { subdomain, ... }@args: - let - conflicts = [ - "port" - "root" - ]; - optionsNotNull = builtins.map (v: args.${v} != null) conflicts; - optionsSet = lib.filter lib.id optionsNotNull; - in - { - assertion = builtins.length optionsSet == 1; - message = '' - Subdomain '${subdomain}' must have exactly one of ${ - lib.concatStringsSep ", " (builtins.map (v: "'${v}'") conflicts) - } configured. - ''; - } - ) - # ++ ( - # let - # ports = lib.my.mapFilter - # (v: v != null) - # ({ port, ... }: port) - # cfg.virtualHosts; - # lib.unique ports; - # lib.compareLists ports - # portCounts = lib.my.countValues ports; - # nonUniquesCounts = lib.filterAttrs (_: v: v != 1) portCounts; - # nonUniques = builtins.attrNames nonUniquesCounts; - # mkAssertion = port: { - # assertion = false; - # message = "Port ${port} cannot appear in multiple virtual hosts."; - # }; - # in - # map mkAssertion nonUniques - # ) ++ ( - # let - # subs = map ({ subdomain, ... }: subdomain) cfg.virtualHosts; - # subsCounts = lib.my.countValues subs; - # nonUniquesCounts = lib.filterAttrs (_: v: v != 1) subsCounts; - # nonUniques = builtins.attrNames nonUniquesCounts; - # mkAssertion = v: { - # assertion = false; - # message = '' - # Subdomain '${v}' cannot appear in multiple virtual hosts. - # ''; - # }; - # in - # map mkAssertion nonUniques - # ) - ; - services = { - nginx = { - enable = true; - statusPage = true; # For monitoring scraping. - - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedTlsSettings = true; - recommendedProxySettings = true; - recommendedBrotliSettings = true; - recommendedZstdSettings = true; - - # Only allow PFS-enabled ciphers with AES256 - sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL"; - - commonHttpConfig = '' - # Add HSTS header with preloading to HTTPS requests. - # Adding this header to HTTP requests is discouraged - map $scheme $hsts_header { - https "max-age=31536000; includeSubdomains; preload"; - } - add_header Strict-Transport-Security $hsts_header; - - # CORS header - # some applications set it to wildcard, therefore this overrides it - proxy_hide_header Access-Control-Allow-Origin; - add_header Access-Control-Allow-Origin https://${config.networking.domain}; - - # Minimize information leaked to other domains - add_header 'Referrer-Policy' 'strict-origin-when-cross-origin'; - - # Disable embedding as a frame - add_header X-Frame-Options DENY; - - # Prevent injection of code in other mime types (XSS Attacks) - add_header X-Content-Type-Options nosniff; - - # Enable XSS protection of the browser. - # May be unnecessary when CSP is configured properly (see above) - add_header X-XSS-Protection "1; mode=block"; - - # This might create errors - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - - # Enable CSP for your services. - #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; - ''; - - virtualHosts = - let - genAttrs' = values: f: lib.listToAttrs (map f values); - inherit (config.networking) domain; - mkVHost = - { subdomain, ... }@args: - lib.nameValuePair "${subdomain}.${domain}" ( - lib.foldl lib.recursiveUpdate { } [ - # Base configuration - { - forceSSL = true; - useACMEHost = domain; - } - # Proxy to port - (lib.optionalAttrs (args.port != null) { - locations."/".proxyPass = "http://localhost:${toString args.port}"; - }) - # Serve filesystem content - (lib.optionalAttrs (args.root != null) { inherit (args) root; }) - # VHost specific configuration - args.extraConfig - # SSO configuration - (lib.optionalAttrs args.sso.enable { - extraConfig = - (args.extraConfig.extraConfig or "") - + '' - error_page 401 = @error401; - ''; - locations = { - "@error401".return = '' - 302 https://${cfg.sso.subdomain}.${config.networking.domain}/login?go=$scheme://$http_host$request_uri - ''; - "/" = { - extraConfig = - (args.extraConfig.locations."/".extraConfig or "") - + '' - # Use SSO - auth_request /sso-auth; - # Set username through header - auth_request_set $username $upstream_http_x_username; - proxy_set_header X-User $username; - # Renew SSO cookie on request - auth_request_set $cookie $upstream_http_set_cookie; - add_header Set-Cookie $cookie; - ''; - }; - "/sso-auth" = { - proxyPass = "http://localhost:${toString cfg.sso.port}/auth"; - extraConfig = '' - # Do not allow requests from outside - internal; - # Do not forward the request body - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - # Set X-Application according to subdomain for matching - proxy_set_header X-Application "${subdomain}"; - # Set origin URI for matching - proxy_set_header X-Origin-URI $request_uri; - ''; - }; - }; - }) - ] - ); - in - genAttrs' cfg.virtualHosts mkVHost; - sso = { - enable = true; - configuration = { - listen = { - addr = "localhost"; - inherit (cfg.sso) port; - }; - audit_log = { - target = [ "fd://stdout" ]; - events = [ - "access_denied" - "login_success" - "login_failure" - "logout" - "validate" - ]; - headers = [ - "x-origin-uri" - "x-application" - ]; - }; - cookie = { - domain = ".${config.networking.domain}"; - secure = true; - authentication_key = { - _secret = cfg.sso.authKeyFile; - }; - }; - login = { - title = "Bühlers's SSO"; - default_method = "simple"; - hide_mfa_field = false; - names = { - simple = "Username / Password"; - }; - }; - providers = { - simple = - let - applyUsers = lib.flip lib.mapAttrs cfg.sso.users; - in - { - users = applyUsers (_: v: { _secret = v.passwordHashFile; }); - mfa = applyUsers ( - _: v: [ - { - provider = "totp"; - attributes = { - secret = { - _secret = v.totpSecretFile; - }; - }; - } - ] - ); - inherit (cfg.sso) groups; - }; - }; - acl = { - rule_sets = [ - { - rules = [ - { - field = "x-application"; - present = true; - } - ]; - allow = [ "@root" ]; - } - ]; - }; - }; - }; - }; - - # services.prometheus = lib.mkIf cfg.monitoring.enable { - prometheus = { - exporters.nginx.enable = true; - scrapeConfigs = [ - { - job_name = "nginx"; - static_configs = [ - { - targets = [ "localhost:${toString config.services.prometheus.exporters.nginx.port}" ]; - labels = { - instance = config.networking.hostName; - }; - } - ]; - } - ]; - }; - grafana.provision = { - dashboards.settings.providers = [ - { - name = "Nginx"; - options.path = pkgs.grafana-dashboards.nginx; - disableDeletion = true; - } - ]; - }; - }; - - my.services.nginx.virtualHosts = [ - { - subdomain = "login"; - inherit (cfg.sso) port; - } - ]; - my.services.backup = { - exclude = [ - # fails often because the file changed - "/var/log/nginx/access.log" - ]; - }; - - networking.firewall.allowedTCPPorts = [ - 80 - 443 - ]; - # Nginx needs to be able to read the certificates - users.users.nginx.extraGroups = [ "acme" ]; - }; -} diff --git a/modules/services/nginx/sso/default.nix b/modules/services/nginx/sso/default.nix deleted file mode 100644 index 01cec09..0000000 --- a/modules/services/nginx/sso/default.nix +++ /dev/null @@ -1,94 +0,0 @@ -# I must override the module to allow having runtime secrets -{ - config, - lib, - pkgs, - utils, - ... -}: -let - cfg = config.services.nginx.sso; - pkg = lib.getBin cfg.package; - confPath = "/var/lib/nginx-sso/config.json"; -in -{ - disabledModules = [ "services/security/nginx-sso.nix" ]; - - options.services.nginx.sso = with lib; { - enable = mkEnableOption "nginx-sso service"; - - package = mkOption { - type = types.package; - default = pkgs.nginx-sso; - defaultText = "pkgs.nginx-sso"; - description = '' - The nginx-sso package that should be used. - ''; - }; - - configuration = mkOption { - type = types.attrsOf types.unspecified; - default = { }; - example = literalExpression '' - { - listen = { addr = "localhost"; port = 8080; }; - - providers.token.tokens = { - myuser = "MyToken"; - }; - - acl = { - rule_sets = [ - { - rules = [ { field = "x-application"; equals = "MyApp"; } ]; - allow = [ "myuser" ]; - } - ]; - }; - } - ''; - description = '' - nginx-sso configuration - (documentation) - as a Nix attribute set. - ''; - }; - }; - - config = lib.mkIf cfg.enable { - systemd.services.nginx-sso = { - description = "Nginx SSO Backend"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - serviceConfig = { - StateDirectory = "nginx-sso"; - WorkingDirectory = "/var/lib/nginx-sso"; - # The files to be merged might not have the correct permissions - ExecStartPre = ''+${pkgs.writeScript "merge-nginx-sso-config" '' - #!${pkgs.bash}/bin/bash - rm -f '${confPath}' - ${utils.genJqSecretsReplacementSnippet cfg.configuration confPath} - - # Fix permissions - chown nginx-sso:nginx-sso ${confPath} - chmod 0600 ${confPath} - ''}''; - ExecStart = lib.mkForce '' - ${pkg}/bin/nginx-sso \ - --config ${confPath} \ - --frontend-dir ${pkg}/share/frontend - ''; - Restart = "always"; - User = "nginx-sso"; - Group = "nginx-sso"; - }; - }; - - users.users.nginx-sso = { - isSystemUser = true; - group = "nginx-sso"; - }; - - users.groups.nginx-sso = { }; - }; -} diff --git a/modules/services/paperless/default.nix b/modules/services/paperless/default.nix index ab304f6..f1ab611 100644 --- a/modules/services/paperless/default.nix +++ b/modules/services/paperless/default.nix @@ -45,7 +45,7 @@ in # monitoring is not really useful, because it only contains the http-worker infos -> skipped for now - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "docs"; inherit (cfg) port; diff --git a/modules/services/passworts/default.nix b/modules/services/passworts/default.nix index 1c2f39c..31bbb60 100644 --- a/modules/services/passworts/default.nix +++ b/modules/services/passworts/default.nix @@ -21,7 +21,7 @@ in inherit (cfg) port; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "passworts"; inherit (cfg) port; diff --git a/modules/services/photos/default.nix b/modules/services/photos/default.nix index 238c4f0..102c296 100644 --- a/modules/services/photos/default.nix +++ b/modules/services/photos/default.nix @@ -85,24 +85,10 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "photos"; inherit (cfg) port; - extraConfig = { - locations."/" = { - proxyWebsockets = true; - extraConfig = '' - # Allow large file uploads - client_max_body_size 1G; - - # Configure timeout - proxy_read_timeout 600s; - proxy_send_timeout 600s; - send_timeout 600s; - ''; - }; - }; } ]; diff --git a/modules/services/prometheus/default.nix b/modules/services/prometheus/default.nix index 1dacb34..1052120 100644 --- a/modules/services/prometheus/default.nix +++ b/modules/services/prometheus/default.nix @@ -188,7 +188,7 @@ in }; }; - nginx.virtualHosts = [ + webserver.virtualHosts = [ { subdomain = "monitor"; inherit (cfg) port; diff --git a/modules/services/promtail/default.nix b/modules/services/promtail/default.nix index ad04bc0..167735d 100644 --- a/modules/services/promtail/default.nix +++ b/modules/services/promtail/default.nix @@ -83,7 +83,7 @@ in }; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "log"; inherit (cfg) port; diff --git a/modules/services/prowlarr/default.nix b/modules/services/prowlarr/default.nix index 4d22aed..8fd7921 100644 --- a/modules/services/prowlarr/default.nix +++ b/modules/services/prowlarr/default.nix @@ -43,7 +43,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "indexer"; inherit port; diff --git a/modules/services/radarr/default.nix b/modules/services/radarr/default.nix index af7b00c..023378f 100644 --- a/modules/services/radarr/default.nix +++ b/modules/services/radarr/default.nix @@ -43,7 +43,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "movies"; inherit port; diff --git a/modules/services/rss-bridge/default.nix b/modules/services/rss-bridge/default.nix index ae777f3..262fa78 100644 --- a/modules/services/rss-bridge/default.nix +++ b/modules/services/rss-bridge/default.nix @@ -1,5 +1,10 @@ # Get RSS feeds from websites that don't natively have one -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.my.services.rss-bridge; domain = "rss-bridge.${config.networking.domain}"; @@ -13,13 +18,23 @@ in services.rss-bridge = { enable = true; config.system.enabled_bridges = [ "*" ]; # Whitelist all - virtualHost = domain; + virtualHost = null; + user = "caddy"; + group = "caddy"; }; - services.nginx.virtualHosts.${domain} = { - forceSSL = true; - enableACME = true; - }; + my.services.webserver.virtualHosts = [ + { + subdomain = "rss-bridge"; + extraConfig = '' + root * ${pkgs.rss-bridge} + php_fastcgi unix/${config.services.phpfpm.pools."rss-bridge".socket} { + env RSSBRIDGE_fileCache_path ${config.services.rss-bridge.dataDir}/cache/ + } + file_server + ''; + } + ]; webapps.apps.rss-bridge = { dashboard = { diff --git a/modules/services/sonarr/default.nix b/modules/services/sonarr/default.nix index a13655e..1a9f0be 100644 --- a/modules/services/sonarr/default.nix +++ b/modules/services/sonarr/default.nix @@ -49,7 +49,7 @@ in ]; }; - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "series"; inherit port; diff --git a/modules/services/tandoor-recipes/default.nix b/modules/services/tandoor-recipes/default.nix index 473d5e0..9cd74d9 100644 --- a/modules/services/tandoor-recipes/default.nix +++ b/modules/services/tandoor-recipes/default.nix @@ -22,7 +22,7 @@ in }; # Proxy to Tandoor-Recipes - my.services.nginx.virtualHosts = [ + my.services.webserver.virtualHosts = [ { subdomain = "recipes"; inherit (cfg) port; diff --git a/modules/services/vpn/default.nix b/modules/services/vpn/default.nix index 981c00e..d92389e 100644 --- a/modules/services/vpn/default.nix +++ b/modules/services/vpn/default.nix @@ -53,17 +53,10 @@ in # Proxy to Headscale my.services = { - nginx.virtualHosts = [ + webserver.virtualHosts = [ { subdomain = "vpn"; inherit (cfg) port; - extraConfig = { - locations = { - "/" = { - proxyWebsockets = true; - }; - }; - }; } ]; diff --git a/modules/services/webserver/default.nix b/modules/services/webserver/default.nix new file mode 100644 index 0000000..367c53c --- /dev/null +++ b/modules/services/webserver/default.nix @@ -0,0 +1,181 @@ +# public webserver with reverseproxy +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.my.services.webserver; + inherit (config.networking) domain; + + virtualHostOption = lib.types.submodule { + options = { + subdomain = lib.mkOption { + type = lib.types.str; + example = "dev"; + description = '' + Which subdomain, under config.networking.domain, to use + for this virtual host. + ''; + }; + port = lib.mkOption { + type = with lib.types; nullOr port; + default = null; + example = 8080; + description = '' + Which port to proxy to, through localhost, for this virtual host. + This option is incompatible with `root`. + ''; + }; + root = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + example = "/var/www/blog"; + description = '' + The root folder for this virtual host. This option is incompatible + with `port`. + ''; + }; + extraConfig = lib.mkOption { + type = with lib.types; nullOr lines; + example = lib.literalExpression '' + { + locations."/socket" = { + proxyPass = "http://localhost:8096/"; + proxyWebsockets = true; + }; + } + ''; + default = null; + description = '' + Any extra configuration that should be applied to this virtual host. + ''; + }; + }; + }; + +in +{ + options.my.services.webserver = { + enable = lib.mkEnableOption "webserver"; + virtualHosts = lib.mkOption { + type = lib.types.listOf virtualHostOption; + default = [ ]; + example = lib.literalExpression '' + [ + { + subdomain = "gitea"; + port = 8080; + } + { + subdomain = "dev"; + root = "/var/www/dev"; + } + { + subdomain = "jellyfin"; + port = 8096; + extraConfig = { + locations."/socket" = { + proxyPass = "http://localhost:8096/"; + proxyWebsockets = true; + }; + }; + } + ] + ''; + description = '' + List of virtual hosts to set-up using default settings. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + services = { + nginx.enable = false; + caddy = { + enable = true; + email = "server@buehler.rocks"; + + globalConfig = '' + servers{ + metrics + } + ''; + extraConfig = '' + (compress) { + encode gzip zstd + } + (headers) { + header { + # enable CORS + Access-Control-Allow-Origin "https://${config.networking.domain}" + # disable FLoC tracking + Permissions-Policy interest-cohort=() + # enable HSTS + Strict-Transport-Security max-age=31536000; + # disable clients from sniffing the media type + X-Content-Type-Options "nosniff" + # clickjacking protection + X-Frame-Options "DENY" + # enable XSS protection + X-XSS-Protection "1; mode=block" + # referrer policy + Referrer-Policy "strict-origin-when-cross-origin" + } + } + (common) { + import headers + import compress + } + ''; + + virtualHosts = + let + mkVHost = + { subdomain, ... }@args: + lib.nameValuePair "${subdomain}.${domain}" ( + lib.foldl lib.recursiveUpdate { } [ + { + useACMEHost = domain; + extraConfig = '' + import common + ${lib.optionalString (args.root != null) '' + root * ${args.root} + file_server + ''} + ${lib.optionalString (args.port != null) '' + reverse_proxy localhost:${toString args.port} { + # remove CORS headers from proxied server, because duplicate headers are not allowed + # remove after new release: https://github.com/navidrome/navidrome/commit/657fe11f5327ff7a3cb6aa9308b0bb7c71eea5c6 + header_down -Access-Control-Allow-Origin + } + ''} + ${lib.optionalString (args.extraConfig != null) args.extraConfig} + ''; + } + ] + ); + in + lib.listToAttrs (map mkVHost cfg.virtualHosts); + }; + + prometheus.scrapeConfigs = [ + { + job_name = "caddy"; + static_configs = [ + { + targets = [ "localhost:2019" ]; + labels.instance = config.networking.hostName; + } + ]; + } + ]; + }; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + }; +}