From 2058665de0407368d8856c4679753a9a928e631f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= Date: Thu, 13 Nov 2025 05:20:09 +0800 Subject: [PATCH 1/2] test(nginx): Add simple tests to assert nginx features work --- checks/default.nix | 2 + configuration/default.nix | 2 +- configuration/nginx/default.nix | 18 +++++---- configuration/nginx/logging.nix | 58 +++++++++++++++++++++++++++- configuration/nginx/ssl.nix | 68 +++++++++++++++++++++++++++++++++ modules/default.nix | 7 +++- modules/serviceTests/mocks.nix | 27 +++++++++++++ modules/serviceTests/stub.nix | 20 ++++++++++ 8 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 modules/serviceTests/mocks.nix create mode 100644 modules/serviceTests/stub.nix diff --git a/checks/default.nix b/checks/default.nix index 674b19d..737313d 100644 --- a/checks/default.nix +++ b/checks/default.nix @@ -6,6 +6,8 @@ let in { x86_64-linux = lib.mergeAttrsList [ + flake-inputs.self.nixosConfigurations.hetzner-1.config.serviceTests + { nix = checkLib.mkLint { name = "nix-lints"; diff --git a/configuration/default.nix b/configuration/default.nix index 9881db2..6f73f54 100644 --- a/configuration/default.nix +++ b/configuration/default.nix @@ -11,7 +11,7 @@ flake-inputs.tlaternet-webserver.nixosModules.default "${modulesPath}/profiles/minimal.nix" - (import ../modules) + ../modules ./services/backups.nix ./services/battery-manager.nix diff --git a/configuration/nginx/default.nix b/configuration/nginx/default.nix index dab0259..92ea53c 100644 --- a/configuration/nginx/default.nix +++ b/configuration/nginx/default.nix @@ -10,13 +10,15 @@ description = "The base domain name to append to virtual domain names"; }; - config.services.nginx = { - enable = true; - recommendedTlsSettings = true; - recommendedOptimisation = true; - recommendedGzipSettings = true; - recommendedProxySettings = true; - clientMaxBodySize = "10G"; - statusPage = true; # For metrics, should be accessible only from localhost + config = { + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + clientMaxBodySize = "10G"; + statusPage = true; # For metrics, should be accessible only from localhost + }; }; } diff --git a/configuration/nginx/logging.nix b/configuration/nginx/logging.nix index 0c6a955..e41bfae 100644 --- a/configuration/nginx/logging.nix +++ b/configuration/nginx/logging.nix @@ -1,4 +1,10 @@ -{ config, lib, ... }: +{ + flake-inputs, + pkgs, + config, + lib, + ... +}: let hostNames = lib.attrNames config.services.nginx.virtualHosts; logPath = name: "/var/log/nginx/${name}/access.log"; @@ -80,5 +86,55 @@ in }; }; }; + + serviceTests = + let + testHostConfig = + { config, ... }: + { + imports = [ + ./. + ../../modules/serviceTests/mocks.nix + ]; + + networking.firewall.allowedTCPPorts = [ 80 ]; + + services.nginx = { + domain = "testHost"; + virtualHosts."${config.services.nginx.domain}".locations."/".return = "200 ok"; + }; + }; + in + { + nginxMetricsWork = pkgs.testers.runNixOSTest { + name = "nginx-metrics-work"; + node.specialArgs = { inherit flake-inputs; }; + + nodes = { + testHost = testHostConfig; + + client = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + }; + }; + + testScript = '' + import time + + start_all() + + testHost.wait_for_unit("nginx.service") + client.succeed("curl --max-time 10 http://testHost") + + # Wait a bit for the prometheus exporter to scrape our logs + time.sleep(5) + + res = testHost.succeed("curl localhost:${builtins.toString config.services.prometheus.exporters.nginxlog.port}/metrics") + assert 'nginxlog_http_response_count_total{method="GET",status="200",vhost="testHost"} 1' in res, res + ''; + }; + }; }; } diff --git a/configuration/nginx/ssl.nix b/configuration/nginx/ssl.nix index 7abc38e..dc2b999 100644 --- a/configuration/nginx/ssl.nix +++ b/configuration/nginx/ssl.nix @@ -1,4 +1,5 @@ { + flake-inputs, pkgs, config, lib, @@ -69,5 +70,72 @@ "porkbun/api-key".owner = "acme"; "porkbun/secret-api-key".owner = "acme"; }; + + serviceTests = + let + testHostConfig = + { config, ... }: + { + imports = [ + ./. + ../../modules/serviceTests/mocks.nix + ]; + + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + + security.acme.certs."tlater.net".extraDomainNames = [ config.services.nginx.domain ]; + + services.nginx = { + domain = "testHost"; + + virtualHosts."${config.services.nginx.domain}" = { + useACMEHost = "tlater.net"; + forceSSL = true; + enableHSTS = true; + locations."/".return = "200 ok"; + }; + }; + }; + in + { + hstsIsSet = pkgs.testers.runNixOSTest { + name = "hsts-is-set"; + + node.specialArgs = { inherit flake-inputs; }; + nodes = { + testHost = testHostConfig; + + client = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + }; + }; + + testScript = '' + start_all() + + testHost.wait_for_unit("nginx.service") + testHost.copy_from_vm("/var/lib/acme/tlater.net/", "certs") + client.copy_from_host(f"{testHost.out_dir}/certs", "/certs") + + client.succeed("curl --max-time 10 http://testHost") + res = client.succeed(" ".join([ + "curl", + "--show-error", + "--silent", + "--dump-header -", + "--cacert /certs/tlater.net/fullchain.pem", + "https://testHost", + "-o /dev/null" + ])) + + assert "strict-transport-security: max-age=15552000; includeSubDomains" in res + ''; + }; + }; }; } diff --git a/modules/default.nix b/modules/default.nix index 9483c66..1bf5314 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1 +1,6 @@ -{ imports = [ ./crowdsec ]; } +{ + imports = [ + ./crowdsec + ./serviceTests/stub.nix + ]; +} diff --git a/modules/serviceTests/mocks.nix b/modules/serviceTests/mocks.nix new file mode 100644 index 0000000..f864253 --- /dev/null +++ b/modules/serviceTests/mocks.nix @@ -0,0 +1,27 @@ +/** + Module containing mock definitions for service test runners. +*/ +{ flake-inputs, lib, ... }: +{ + imports = [ + flake-inputs.sops-nix.nixosModules.sops + ../. + ../../configuration/services/backups.nix + ]; + + sops.defaultSopsFile = ../../keys/staging.yaml; + environment.etc."staging.key" = { + mode = "0400"; + source = ../../keys/hosts/staging.key; + }; + services.openssh = { + enable = true; + hostKeys = lib.mkForce [ + { + type = "rsa"; + bits = 4096; + path = "/etc/staging.key"; + } + ]; + }; +} diff --git a/modules/serviceTests/stub.nix b/modules/serviceTests/stub.nix new file mode 100644 index 0000000..d4a4b78 --- /dev/null +++ b/modules/serviceTests/stub.nix @@ -0,0 +1,20 @@ +/** + Module to make writing service-specific tests easy. +*/ +{ lib, ... }: +let + inherit (lib) mkOption types; +in +{ + options = { + serviceTests = mkOption { + type = types.attrsOf types.package; + + description = '' + NixOS tests to run. + ''; + + default = { }; + }; + }; +} From 156551b217d86ba8900fe96bdba2400be73fc796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Dani=C3=ABl=20Maat?= Date: Thu, 20 Nov 2025 00:17:43 +0800 Subject: [PATCH 2/2] refactor(firewall): Make services responsible for opening ports --- configuration/default.nix | 33 ---------------------- configuration/services/conduit/default.nix | 30 ++++++++++++++++++++ configuration/services/foundryvtt.nix | 5 ++++ configuration/services/gitea.nix | 5 ++++ configuration/services/immich.nix | 5 ++++ configuration/services/metrics/grafana.nix | 5 ++++ configuration/services/nextcloud.nix | 5 ++++ configuration/services/starbound.nix | 2 ++ configuration/services/webserver.nix | 5 ++++ 9 files changed, 62 insertions(+), 33 deletions(-) diff --git a/configuration/default.nix b/configuration/default.nix index 6f73f54..2b8a16f 100644 --- a/configuration/default.nix +++ b/configuration/default.nix @@ -49,39 +49,6 @@ networking = { usePredictableInterfaceNames = false; useDHCP = false; - - firewall = { - allowedTCPPorts = [ - # http - 80 - 443 - # ssh - 2222 - # matrix - 8448 - # starbound - 21025 - - config.services.coturn.listening-port - config.services.coturn.tls-listening-port - config.services.coturn.alt-listening-port - config.services.coturn.alt-tls-listening-port - ]; - - allowedUDPPorts = [ - config.services.coturn.listening-port - config.services.coturn.tls-listening-port - config.services.coturn.alt-listening-port - config.services.coturn.alt-tls-listening-port - ]; - - allowedUDPPortRanges = [ - { - from = config.services.coturn.min-port; - to = config.services.coturn.max-port; - } - ]; - }; }; systemd.network.enable = true; diff --git a/configuration/services/conduit/default.nix b/configuration/services/conduit/default.nix index 6e01e81..b6f8f27 100644 --- a/configuration/services/conduit/default.nix +++ b/configuration/services/conduit/default.nix @@ -17,6 +17,36 @@ in ./matrix-hookshot.nix ]; + networking.firewall = { + allowedTCPPorts = [ + # These are for "normal" clients + 80 + 443 + + # Federation happens on 8448 + 8448 + + config.services.coturn.listening-port + config.services.coturn.tls-listening-port + config.services.coturn.alt-listening-port + config.services.coturn.alt-tls-listening-port + ]; + + allowedUDPPorts = [ + config.services.coturn.listening-port + config.services.coturn.tls-listening-port + config.services.coturn.alt-listening-port + config.services.coturn.alt-tls-listening-port + ]; + + allowedUDPPortRanges = [ + { + from = config.services.coturn.min-port; + to = config.services.coturn.max-port; + } + ]; + }; + services = { matrix-conduit = { enable = true; diff --git a/configuration/services/foundryvtt.nix b/configuration/services/foundryvtt.nix index 6c475a3..5c8a21f 100644 --- a/configuration/services/foundryvtt.nix +++ b/configuration/services/foundryvtt.nix @@ -11,6 +11,11 @@ in { imports = [ flake-inputs.foundryvtt.nixosModules.foundryvtt ]; + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services = { foundryvtt = { enable = true; diff --git a/configuration/services/gitea.nix b/configuration/services/gitea.nix index 613d30c..b4dd719 100644 --- a/configuration/services/gitea.nix +++ b/configuration/services/gitea.nix @@ -8,6 +8,11 @@ let domain = "gitea.${config.services.nginx.domain}"; in { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services = { forgejo = { enable = true; diff --git a/configuration/services/immich.nix b/configuration/services/immich.nix index 516ea3e..39673d0 100644 --- a/configuration/services/immich.nix +++ b/configuration/services/immich.nix @@ -8,6 +8,11 @@ let hostName = "immich.${config.services.nginx.domain}"; in { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services = { immich = { enable = true; diff --git a/configuration/services/metrics/grafana.nix b/configuration/services/metrics/grafana.nix index f4b6956..765a364 100644 --- a/configuration/services/metrics/grafana.nix +++ b/configuration/services/metrics/grafana.nix @@ -3,6 +3,11 @@ let domain = "metrics.${config.services.nginx.domain}"; in { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services.grafana = { enable = true; settings = { diff --git a/configuration/services/nextcloud.nix b/configuration/services/nextcloud.nix index ef2a6ac..77cfa4c 100644 --- a/configuration/services/nextcloud.nix +++ b/configuration/services/nextcloud.nix @@ -9,6 +9,11 @@ let hostName = "nextcloud.${config.services.nginx.domain}"; in { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services = { nextcloud = { inherit hostName; diff --git a/configuration/services/starbound.nix b/configuration/services/starbound.nix index a667b57..6b97471 100644 --- a/configuration/services/starbound.nix +++ b/configuration/services/starbound.nix @@ -8,6 +8,8 @@ let inherit (lib) concatStringsSep; in { + networking.firewall.allowedTCPPorts = [ 21025 ]; + # Sadly, steam-run requires some X libs environment.noXlibs = false; diff --git a/configuration/services/webserver.nix b/configuration/services/webserver.nix index 864f6c0..8f08e4f 100644 --- a/configuration/services/webserver.nix +++ b/configuration/services/webserver.nix @@ -3,6 +3,11 @@ let inherit (config.services.nginx) domain; in { + networking.firewall.allowedTCPPorts = [ + 80 + 443 + ]; + services.tlaternet-webserver = { enable = true; listen = {