diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-12 16:28:46 +0200 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-04-23 12:10:33 +0200 |
commit | 2452418a7a3aea1ecfd54f135f4814e0c51b946c (patch) | |
tree | 997a1107995930d3f036544f7690d4d46d5296dd /tests | |
parent | modules/stalwart-mail: Don't restart the systemd service (diff) | |
download | nixos-server-2452418a7a3aea1ecfd54f135f4814e0c51b946c.zip |
tests/email-http: Test the http self-service availability
Diffstat (limited to '')
-rw-r--r-- | tests/by-name/em/email-http/nodes/mail_server.nix | 57 | ||||
-rw-r--r-- | tests/by-name/em/email-http/nodes/name_server.nix | 210 | ||||
-rw-r--r-- | tests/by-name/em/email-http/nodes/user.nix | 26 | ||||
-rw-r--r-- | tests/by-name/em/email-http/test.nix | 110 |
4 files changed, 403 insertions, 0 deletions
diff --git a/tests/by-name/em/email-http/nodes/mail_server.nix b/tests/by-name/em/email-http/nodes/mail_server.nix new file mode 100644 index 0000000..e94c4e9 --- /dev/null +++ b/tests/by-name/em/email-http/nodes/mail_server.nix @@ -0,0 +1,57 @@ +{ + extraModules, + pkgs, + vhackPackages, +}: { + mkMailServer = serverName: principal: { + config, + lib, + nodes, + ... + }: { + imports = + extraModules + ++ [ + ../../../../../modules + ../../../../common/acme/client.nix + ]; + + environment.systemPackages = [ + pkgs.bind + pkgs.openssl + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + age.identityPaths = ["${../../../../common/email/hostKey}"]; + + vhack = { + stalwart-mail = { + enable = true; + fqdn = "${serverName}.server.com"; + admin = "admin@${serverName}.server.com"; + security = { + dkimKeys = let + loadKey = name: { + dkimPublicKey = builtins.readFile (../../../../common/email/dkim + "/${name}/public"); + dkimPrivateKeyPath = ../../../../common/email/dkim + "/${name}/private.age"; + keyAlgorithm = "ed25519-sha256"; + }; + in { + "mail.server.com" = loadKey "mail1.server.com"; + "bob.com" = loadKey "bob.com"; + }; + verificationMode = "strict"; + }; + openFirewall = true; + principals = + if principal == null + then null + else [principal]; + }; + }; + }; +} diff --git a/tests/by-name/em/email-http/nodes/name_server.nix b/tests/by-name/em/email-http/nodes/name_server.nix new file mode 100644 index 0000000..a7e3ce9 --- /dev/null +++ b/tests/by-name/em/email-http/nodes/name_server.nix @@ -0,0 +1,210 @@ +{extraModules}: { + config, + lib, + nodes, + pkgs, + ... +}: let + keyAlgoToKeyType = keyAlgo: + if keyAlgo == "ed25519-sha256" + then "ed25519" + else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1" + then "rsa" + else builtins.throw "Impossible"; + + mkZone = user: nodes: lib: cfg: { + SOA = { + nameServer = "ns.server.com"; + adminEmail = "${user}@${user}.com"; + serial = 2024012301; + }; + + MX = [ + { + preference = 10; + exchange = "${cfg.fqdn}."; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.1 + # Also see the policy in the hmtl part. + MTA-STS = [ + { + id = "20250228Z"; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc7208.html + # https://en.wikipedia.org/wiki/Sender_Policy_Framework + TXT = [ + (builtins.concatStringsSep " " + [ + "v=spf1" # The version. + "+mx" # Allow mail from this domain MX record. + "-all" # Reject all other emails if the previous mechanism did not match. + ]) + ]; + + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1 + # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5 + DKIM = [ + { + selector = "mail"; + k = keyAlgoToKeyType cfg.security.dkimKeys."${user}.com".keyAlgorithm; + p = cfg.security.dkimKeys."${user}.com".dkimPublicKey; + s = ["email"]; + t = ["s"]; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3 + DMARC = [ + { + adkim = "strict"; + aspf = "strict"; + fo = ["0" "1" "d" "s"]; + p = "quarantine"; + rua = cfg.admin; + ruf = [cfg.admin]; + } + ]; + + A = [ + nodes.${user}.networking.primaryIPAddress + ]; + AAAA = [ + nodes.${user}.networking.primaryIPv6Address + ]; + }; + mkServerZone = serverName: nodes: lib: let + cfg = nodes."${serverName}_server".vhack.stalwart-mail; + in { + SOA = { + nameServer = "ns.server.com"; + adminEmail = "admin@server.com"; + serial = 2024012301; + }; + MX = [ + { + preference = 10; + exchange = "${serverName}.server.com."; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1 + # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5 + DKIM = [ + { + selector = "mail"; + k = keyAlgoToKeyType cfg.security.dkimKeys."${serverName}.server.com".keyAlgorithm; + p = cfg.security.dkimKeys."${serverName}.server.com".dkimPublicKey; + s = ["email"]; + t = ["s"]; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3 + DMARC = [ + { + adkim = "strict"; + aspf = "strict"; + fo = ["0" "1" "d" "s"]; + p = "reject"; + rua = cfg.admin; + ruf = [cfg.admin]; + } + ]; + + # https://www.rfc-editor.org/rfc/rfc7208.html + # NOTE(@bpeetz): This server might not be directly sending mail, but it is still required for + # the SMTP EHLO check. <2025-02-25> + TXT = [ + (builtins.concatStringsSep " " + [ + "v=spf1" # The version. + "+mx" # Allow mail from this domain MX record. + "-all" # Reject all other emails if the previous mechanism did not match. + ]) + ]; + + A = [ + nodes."${serverName}_server".networking.primaryIPAddress + ]; + AAAA = [ + nodes."${serverName}_server".networking.primaryIPv6Address + ]; + }; +in { + imports = + extraModules + ++ [ + ../../../../../modules + ../../../../common/acme/client.nix + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + vhack = { + nginx = { + enable = true; + }; + dns = { + enable = true; + openFirewall = true; + interfaces = [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + zones = { + "bob.com" = mkZone "bob" nodes lib nodes.mail_server.vhack.stalwart-mail; + "mail.server.com" = mkServerZone "mail" nodes lib; + "ns.server.com" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + useOrigin = false; + + A = [ + nodes.name_server.networking.primaryIPAddress + ]; + AAAA = [ + nodes.name_server.networking.primaryIPv6Address + ]; + }; + "acme.test" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + useOrigin = false; + + A = [ + nodes.acme.networking.primaryIPAddress + ]; + AAAA = [ + nodes.acme.networking.primaryIPv6Address + ]; + }; + "server.com" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + + useOrigin = false; + NS = [ + "ns.server.com." + ]; + }; + }; + }; + }; +} diff --git a/tests/by-name/em/email-http/nodes/user.nix b/tests/by-name/em/email-http/nodes/user.nix new file mode 100644 index 0000000..73b9ff7 --- /dev/null +++ b/tests/by-name/em/email-http/nodes/user.nix @@ -0,0 +1,26 @@ +{ + pkgs, + vhackPackages, +}: { + mkUser = user: serverName: { + nodes, + lib, + ... + }: { + imports = [ + ../../../../common/acme/client.nix + ]; + + environment.systemPackages = [ + pkgs.bind + pkgs.openssl + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + users.users."${user}" = {isNormalUser = true;}; + }; +} diff --git a/tests/by-name/em/email-http/test.nix b/tests/by-name/em/email-http/test.nix new file mode 100644 index 0000000..2c7921d --- /dev/null +++ b/tests/by-name/em/email-http/test.nix @@ -0,0 +1,110 @@ +{ + nixos-lib, + pkgsUnstable, + nixpkgs-unstable, + vhackPackages, + pkgs, + extraModules, + nixLib, + ... +}: let + mail_server = import ./nodes/mail_server.nix {inherit extraModules pkgs vhackPackages;}; + inherit (mail_server) mkMailServer; + user = import ./nodes/user.nix {inherit pkgs vhackPackages;}; + inherit (user) mkUser; +in + nixos-lib.runTest { + hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs + + name = "email-http"; + + node = { + specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;}; + + # Use the nixpkgs as constructed by the `nixpkgs.*` options + pkgs = null; + }; + + nodes = { + acme = { + nodes, + lib, + ... + }: { + imports = [../../../common/acme]; + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + ]; + }; + + name_server = import ./nodes/name_server.nix {inherit extraModules;}; + + mail_server = mkMailServer "mail" null; + + bob = mkUser "bob" "mail"; + }; + + # TODO(@bpeetz): This test should also test the http JMAP features of stalwart-mail. <2025-04-12> + testScript = _: + /* + python + */ + '' + # Start dependencies for the other services + acme.start() + acme.wait_for_unit("pebble.service") + name_server.start() + name_server.wait_for_unit("nsd.service") + + # Start the actual testing machines + start_all() + + mail_server.wait_for_unit("stalwart-mail.service") + mail_server.wait_for_open_port(993) # imap + mail_server.wait_for_open_port(465) # smtp + + bob.wait_for_unit("multi-user.target") + + with subtest("Add pebble ca key to all services"): + for node in [name_server, mail_server, bob]: + node.succeed("${pkgs.writeShellScript "fetch-and-set-ca" '' + set -xe + + # Fetch the randomly generated ca certificate + curl https://acme.test:15000/roots/0 > /tmp/ca.crt + curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt + + # Append it to the various system stores + # The file paths are from <nixpgks>/modules/security/ca.nix + for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do + cert_path="/etc/$cert_path" + + mv "$cert_path" "$cert_path.old" + cat "$cert_path.old" > "$cert_path" + cat /tmp/ca.crt >> "$cert_path" + done + + export NIX_SSL_CERT_FILE=/tmp/ca.crt + export SSL_CERT_FILE=/tmp/ca.crt + + # TODO + # # P11-Kit trust source. + # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source"; + ''}") + + with subtest("The mailserver successfully started all services"): + import json + def all_services_running(host): + (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json") + host_failed = json.loads(output) + assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}" + all_services_running(mail_server) + + with subtest("Bob can use the self-service interface"): + bob.succeed("${pkgs.writeShellScript "check-self-service" '' + curl mail.server.com --location --output /home/bob/output.html; + ''}") + + bob.copy_from_vm("/home/bob", "") + ''; + } |