diff options
author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-03-05 19:06:53 +0100 |
---|---|---|
committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-03-09 13:44:40 +0100 |
commit | ef0b3f491e1868c7b3899aff3f53be0325313c2d (patch) | |
tree | 913ddeb99ca5ce3e10f49dfe858d37780aea3c12 /tests/by-name/em/email-dns | |
parent | pkgs/fetchmail-common-name: Patch fetchmail to accept certificates without co... (diff) | |
download | nixos-server-ef0b3f491e1868c7b3899aff3f53be0325313c2d.zip |
tests/email-dns: Init
This test is somewhat involved, but tries to exercise our full mail handling capabilities. It effectively only tests that alice can send a message to bob, but it checks nearly all security mechanisms (DNSSEC is currently still missing).
Diffstat (limited to 'tests/by-name/em/email-dns')
26 files changed, 1122 insertions, 0 deletions
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate b/tests/by-name/em/email-dns/nodes/acme/certs/generate new file mode 100755 index 0000000..0d6258e --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate @@ -0,0 +1,66 @@ +#! /usr/bin/env nix-shell +#! nix-shell -p gnutls -p dash -i dash --impure +# shellcheck shell=dash + +# For development and testing. +# Create a CA key and cert, and use that to generate a server key and cert. +# Creates: +# ca.key.pem +# ca.cert.pem +# server.key.pem +# server.cert.pem + +export SEC_PARAM=ultra +export EXPIRATION_DAYS=123456 +export ORGANIZATION="Vhack.eu Test Keys" +export COUNTRY=EU +export SAN="acme.test" +export KEY_TYPE="ed25519" + +BASEDIR="$(dirname "$0")" +GENERATION_LOCATION="$BASEDIR/output" +cd "$BASEDIR" || { + echo "(BUG?) No basedir ('$BASEDIR')" 1>&2 + exit 1 +} + +ca=false +clients=false + +usage() { + echo "Usage: $0 --ca|--clients" + exit 2 +} + +if [ "$#" -eq 0 ]; then + usage +fi + +for arg in "$@"; do + case "$arg" in + "--ca") + ca=true + ;; + "--clients") + clients=true + ;; + *) + usage + ;; + esac +done + +[ -d "$GENERATION_LOCATION" ] || mkdir --parents "$GENERATION_LOCATION" +cd "$GENERATION_LOCATION" || echo "(BUG?) No generation location fould!" 1>&2 + +[ "$ca" = true ] && ../generate.ca + +# Creates: +# <client_name>.key.pem +# <client_name>.cert.pem +# +[ "$clients" = true ] && ../generate.client "acme.test" + +echo "(INFO) Look for the keys at: $GENERATION_LOCATION" + +# vim: ft=sh diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca new file mode 100755 index 0000000..92832c5 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca @@ -0,0 +1,38 @@ +#! /usr/bin/env sh + +# Take the correct binary to create the certificates +CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null) +if [ -z "$CERTTOOL" ]; then + echo "ERROR: No certtool found" >&2 + exit 1 +fi + +# Create a CA key. +$CERTTOOL \ + --generate-privkey \ + --sec-param "$SEC_PARAM" \ + --key-type "$KEY_TYPE" \ + --outfile ca.key.pem + +chmod 600 ca.key.pem + +# Sign a CA cert. +cat <<EOF >ca.template +country = $COUNTRY +dns_name = "$SAN" +expiration_days = $EXPIRATION_DAYS +organization = $ORGANIZATION +ca +EOF +#state = $STATE +#locality = $LOCALITY + +$CERTTOOL \ + --generate-self-signed \ + --load-privkey ca.key.pem \ + --template ca.template \ + --outfile ca.cert.pem + +chmod 600 ca.cert.pem + +# vim: ft=sh diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.client b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client new file mode 100755 index 0000000..5930298 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client @@ -0,0 +1,44 @@ +#! /usr/bin/env sh + +# Take the correct binary to create the certificates +CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null) +if [ -z "$CERTTOOL" ]; then + echo "ERROR: No certtool found" >&2 + exit 1 +fi + +NAME=client +if [ $# -gt 0 ]; then + NAME="$1" +fi + +# Create a client key. +$CERTTOOL \ + --generate-privkey \ + --sec-param "$SEC_PARAM" \ + --key-type "$KEY_TYPE" \ + --outfile "$NAME".key.pem + +chmod 600 "$NAME".key.pem + +# Sign a client cert with the key. +cat <<EOF >"$NAME".template +dns_name = "$NAME" +dns_name = "$SAN" +expiration_days = $EXPIRATION_DAYS +organization = $ORGANIZATION +encryption_key +signing_key +EOF + +$CERTTOOL \ + --generate-certificate \ + --load-privkey "$NAME".key.pem \ + --load-ca-certificate ca.cert.pem \ + --load-ca-privkey ca.key.pem \ + --template "$NAME".template \ + --outfile "$NAME".cert.pem + +chmod 600 "$NAME".cert.pem + +# vim: ft=sh diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem new file mode 100644 index 0000000..687101d --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBjTCCAT+gAwIBAgIUfiDKld3eiPKuFhsaiHpPNmbMJU8wBQYDK2VwMCoxCzAJ +BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx +MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMB0xGzAZBgNVBAoTElZoYWNrLmV1IFRl +c3QgS2V5czAqMAUGAytlcAMhAHYq2cjrfrlslWxvcKjs2cD7THbpmtq+jf/dlrKW +UEo8o4GBMH8wDAYDVR0TAQH/BAIwADAfBgNVHREEGDAWgglhY21lLnRlc3SCCWFj +bWUudGVzdDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFN/1UyS0jnC3LoryMIL2 +/6cdsYBBMB8GA1UdIwQYMBaAFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytlcANB +ALz3u7lBreHeVZ0YXrwK3SDwlhWIH/SeUQwbxQlarzR47qu3cwQQ93Y1xjtOdu+h +hOM/ig3nLGVOT6qL8IsZrQk= +-----END CERTIFICATE----- diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem new file mode 100644 index 0000000..06195b8 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem @@ -0,0 +1,25 @@ +Public Key Info: + Public Key Algorithm: EdDSA (Ed25519) + Key Security Level: High (256 bits) + +curve: Ed25519 +private key: + 9d:25:38:89:f2:37:d7:65:41:f5:24:ba:4c:19:fb:0f + 86:c8:a3:cf:f7:08:57:69:cc:64:cf:55:2d:8e:99:3e + + +x: + 76:2a:d9:c8:eb:7e:b9:6c:95:6c:6f:70:a8:ec:d9:c0 + fb:4c:76:e9:9a:da:be:8d:ff:dd:96:b2:96:50:4a:3c + + + +Public Key PIN: + pin-sha256:NPwZitkDv4isUmdiicSsM1t1OtYoxqhdvBUnqSc4bFQ= +Public Key ID: + sha256:34fc198ad903bf88ac52676289c4ac335b753ad628c6a85dbc1527a927386c54 + sha1:dff55324b48e70b72e8af23082f6ffa71db18041 + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ0lOInyN9dlQfUkukwZ+w+GyKPP9whXacxkz1Utjpk+ +-----END PRIVATE KEY----- diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template new file mode 100644 index 0000000..320a170 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template @@ -0,0 +1,5 @@ +dns_name = "acme.test" +dns_name = "acme.test" +expiration_days = 123456 +organization = Vhack.eu Test Keys +encryption_key diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem new file mode 100644 index 0000000..0fa9d14 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBYDCCARKgAwIBAgIUdhVVcf+NgElqGuutU55FUDBtFVMwBQYDK2VwMCoxCzAJ +BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx +MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMCoxCzAJBgNVBAYTAkVVMRswGQYDVQQK +ExJWaGFjay5ldSBUZXN0IEtleXMwKjAFBgMrZXADIQCkO1LhHINvJjt41JD6UEc4 +ZKKUubB8lKPxSOyTkFBOgqNIMEYwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTAL +gglhY21lLnRlc3QwHQYDVR0OBBYEFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytl +cANBAFMFFy5tjuQtp5GVEN6qM50L4lteQuxfhlQqmOOfl06HV6153wJnrlKaTOYO +t0dKlSqKROMYUYeU39xDp07MLAc= +-----END CERTIFICATE----- diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem new file mode 100644 index 0000000..64263bc --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem @@ -0,0 +1,25 @@ +Public Key Info: + Public Key Algorithm: EdDSA (Ed25519) + Key Security Level: High (256 bits) + +curve: Ed25519 +private key: + 82:0d:fc:f0:d6:82:89:63:e5:bc:23:78:ba:98:38:83 + 09:2d:e0:78:4c:53:92:e3:db:5b:2f:e4:39:ce:96:3d + + +x: + a4:3b:52:e1:1c:83:6f:26:3b:78:d4:90:fa:50:47:38 + 64:a2:94:b9:b0:7c:94:a3:f1:48:ec:93:90:50:4e:82 + + + +Public Key PIN: + pin-sha256:jpzYZMOHDPCeSXxfL+YUXgSPcbO9MAs8foGMP5CJiD8= +Public Key ID: + sha256:8e9cd864c3870cf09e497c5f2fe6145e048f71b3bd300b3c7e818c3f9089883f + sha1:b51970bff382e1e5ba51e0e466326215e656dffa + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIIIN/PDWgolj5bwjeLqYOIMJLeB4TFOS49tbL+Q5zpY9 +-----END PRIVATE KEY----- diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template new file mode 100644 index 0000000..a2295d8 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template @@ -0,0 +1,5 @@ +country = EU +dns_name = "acme.test" +expiration_days = 123456 +organization = Vhack.eu Test Keys +ca diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix new file mode 100644 index 0000000..aeb6dfc --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix @@ -0,0 +1,13 @@ +let + domain = "acme.test"; +in { + inherit domain; + ca = { + cert = ./output/ca.cert.pem; + key = ./output/ca.key.pem; + }; + "${domain}" = { + cert = ./output/. + "/${domain}.cert.pem"; + key = ./output/. + "/${domain}.key.pem"; + }; +} diff --git a/tests/by-name/em/email-dns/nodes/acme/client.nix b/tests/by-name/em/email-dns/nodes/acme/client.nix new file mode 100644 index 0000000..2b870e8 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/client.nix @@ -0,0 +1,21 @@ +{ + nodes, + lib, + ... +}: let + inherit (nodes.acme.test-support.acme) caCert; + inherit (nodes.acme.test-support.acme) caDomain; +in { + security = { + acme = { + acceptTerms = true; + defaults = { + server = "https://${caDomain}/dir"; + }; + }; + + pki = { + certificateFiles = lib.mkForce [caCert]; + }; + }; +} diff --git a/tests/by-name/em/email-dns/nodes/acme/default.nix b/tests/by-name/em/email-dns/nodes/acme/default.nix new file mode 100644 index 0000000..236ba6a --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/acme/default.nix @@ -0,0 +1,114 @@ +# The certificate for the ACME service is exported as: +# +# config.test-support.acme.caCert +# +# This value can be used inside the configuration of other test nodes to inject +# the test certificate into security.pki.certificateFiles or into package +# overlays. +# +# { +# acme = { nodes, lib, ... }: { +# imports = [ ./common/acme/server ]; +# networking.nameservers = lib.mkForce [ +# nodes.mydnsresolver.networking.primaryIPAddress +# ]; +# }; +# +# dnsmyresolver = ...; +# } +# +# Keep in mind, that currently only _one_ resolver is supported, if you have +# more than one resolver in networking.nameservers only the first one will be +# used. +# +# Also make sure that whenever you use a resolver from a different test node +# that it has to be started _before_ the ACME service. +{ + config, + pkgs, + lib, + ... +}: let + testCerts = import ./certs/snakeoil-certs.nix; + inherit (testCerts) domain; + + pebbleConf.pebble = { + listenAddress = "0.0.0.0:443"; + managementListenAddress = "0.0.0.0:15000"; + + # The cert and key are used only for the Web Front End (WFE) + certificate = testCerts.${domain}.cert; + privateKey = testCerts.${domain}.key; + + httpPort = 80; + tlsPort = 443; + ocspResponderURL = "http://${domain}:4002"; + strict = true; + }; + + pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf); +in { + options.test-support.acme = { + caDomain = lib.mkOption { + type = lib.types.str; + default = domain; + readOnly = true; + description = '' + A domain name to use with the `nodes` attribute to + identify the CA server in the `client` config. + ''; + }; + caCert = lib.mkOption { + type = lib.types.path; + readOnly = true; + default = testCerts.ca.cert; + description = '' + A certificate file to use with the `nodes` attribute to + inject the test CA certificate used in the ACME server into + {option}`security.pki.certificateFiles`. + ''; + }; + }; + + config = { + networking = { + # This has priority 140, because modules/testing/test-instrumentation.nix + # already overrides this with priority 150. + nameservers = lib.mkOverride 140 ["127.0.0.1"]; + firewall.allowedTCPPorts = [ + 80 + 443 + 15000 + 4002 + ]; + + extraHosts = '' + 127.0.0.1 ${domain} + ${config.networking.primaryIPAddress} ${domain} + ''; + }; + + systemd.services = { + pebble = { + enable = true; + description = "Pebble ACME server"; + wantedBy = ["network.target"]; + environment = { + # We're not testing lego, we're just testing our configuration. + # No need to sleep. + PEBBLE_VA_NOSLEEP = "1"; + }; + + serviceConfig = { + RuntimeDirectory = "pebble"; + WorkingDirectory = "/run/pebble"; + + # Required to bind on privileged ports. + AmbientCapabilities = ["CAP_NET_BIND_SERVICE"]; + + ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}"; + }; + }; + }; + }; +} diff --git a/tests/by-name/em/email-dns/nodes/mail_server.nix b/tests/by-name/em/email-dns/nodes/mail_server.nix new file mode 100644 index 0000000..ba554ac --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/mail_server.nix @@ -0,0 +1,57 @@ +{ + extraModules, + pkgs, + vhackPackages, +}: { + mkMailServer = serverName: principal: { + config, + lib, + nodes, + ... + }: { + imports = + extraModules + ++ [ + ../../../../../modules + ./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 = ["${../secrets/hostKey}"]; + + vhack = { + stalwart-mail = { + enable = true; + fqdn = "${serverName}.server.com"; + admin = "admin@${serverName}.server.com"; + security = { + dkimKeys = let + loadKey = name: { + dkimPublicKey = builtins.readFile (../secrets/dkim + "/${name}/public"); + dkimPrivateKeyPath = ../secrets/dkim + "/${name}/private.age"; + keyAlgorithm = "ed25519-sha256"; + }; + in { + "mail1.server.com" = loadKey "mail1.server.com"; + "mail2.server.com" = loadKey "mail2.server.com"; + "alice.com" = loadKey "alice.com"; + "bob.com" = loadKey "bob.com"; + }; + verificationMode = "strict"; + allowInsecureSmtp = false; + }; + openFirewall = true; + principals = [principal]; + }; + }; + }; +} diff --git a/tests/by-name/em/email-dns/nodes/name_server.nix b/tests/by-name/em/email-dns/nodes/name_server.nix new file mode 100644 index 0000000..ef657f4 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/name_server.nix @@ -0,0 +1,320 @@ +{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 = "quarantine"; + 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 + ./acme/client.nix + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + services.nginx = { + logError = "stderr debug"; + virtualHosts = let + mkStsHost = mx: { + forceSSL = true; + enableACME = true; + root = pkgs.runCommandLocal "mkPolicy" {} '' + mkdir --parents $out/.well-known/ + + # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.2 + cat << EOF > $out/.well-known/mta-sts.txt + version: STSv1 + mode: enforce + mx: ${mx} + max_age: 604800 + EOF + ''; + }; + in { + "mta-sts.alice.com" = mkStsHost "mail2.server.com"; + "mta-sts.bob.com" = mkStsHost "mail1.server.com"; + }; + }; + + vhack = { + nginx = { + enable = true; + }; + dns = { + enable = true; + openFirewall = true; + interfaces = [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + zones = let + stsZone = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + + useOrigin = false; + + A = [ + nodes.name_server.networking.primaryIPAddress + ]; + AAAA = [ + nodes.name_server.networking.primaryIPv6Address + ]; + }; + in { + "arpa" = { + SOA = { + nameServer = "ns"; + adminEmail = "admin@server.com"; + serial = 2025012301; + }; + useOrigin = false; + + PTR = [ + { + name = "acme.test"; + ip.v4 = nodes.acme.networking.primaryIPAddress; + } + { + name = "acme.test"; + ip.v6 = nodes.acme.networking.primaryIPv6Address; + } + + { + name = "alice.com"; + ip.v4 = nodes.alice.networking.primaryIPAddress; + } + { + name = "alice.com"; + ip.v6 = nodes.alice.networking.primaryIPv6Address; + } + + { + name = "bob"; + ip.v4 = nodes.bob.networking.primaryIPAddress; + } + { + name = "bob"; + ip.v6 = nodes.bob.networking.primaryIPv6Address; + } + + { + name = "mail1.server.com"; + ip.v4 = nodes.mail1_server.networking.primaryIPAddress; + } + { + name = "mail1.server.com"; + ip.v6 = nodes.mail1_server.networking.primaryIPv6Address; + } + + { + name = "mail2.server.com"; + ip.v4 = nodes.mail2_server.networking.primaryIPAddress; + } + { + name = "mail2.server.com"; + ip.v6 = nodes.mail2_server.networking.primaryIPv6Address; + } + + { + name = "ns.server.com"; + ip.v4 = nodes.name_server.networking.primaryIPAddress; + } + { + name = "ns.server.com"; + ip.v6 = nodes.name_server.networking.primaryIPv6Address; + } + ]; + }; + + "alice.com" = mkZone "alice" nodes lib nodes.mail2_server.vhack.stalwart-mail; + "mta-sts.alice.com" = stsZone; + "bob.com" = mkZone "bob" nodes lib nodes.mail1_server.vhack.stalwart-mail; + "mta-sts.bob.com" = stsZone; + "mail1.server.com" = mkServerZone "mail1" nodes lib; + "mail2.server.com" = mkServerZone "mail2" 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-dns/nodes/user.nix b/tests/by-name/em/email-dns/nodes/user.nix new file mode 100644 index 0000000..e4db347 --- /dev/null +++ b/tests/by-name/em/email-dns/nodes/user.nix @@ -0,0 +1,74 @@ +{ + pkgs, + vhackPackages, +}: { + mkUser = user: serverName: { + nodes, + lib, + ... + }: { + imports = [ + ./acme/client.nix + ]; + + environment.systemPackages = [ + vhackPackages.fetchmail-common-name + pkgs.msmtp + pkgs.procmail + + pkgs.bind + pkgs.openssl + ]; + + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + nodes.name_server.networking.primaryIPv6Address + ]; + + users.users."${user}" = {isNormalUser = true;}; + + systemd.tmpfiles.rules = [ + "d /home/${user}/mail 0700 ${user} users - -" + "L /home/${user}/.fetchmailrc - - - - /etc/homeSetup/.fetchmailrc" + "L /home/${user}/.procmailrc - - - - /etc/homeSetup/.procmailrc" + "L /home/${user}/.msmtprc - - - - /etc/homeSetup/.msmtprc" + ]; + + environment.etc = { + "homeSetup/.fetchmailrc" = { + text = '' + poll "${serverName}.server.com" protocol IMAP + username "${user}" + password "${user}-password" + ssl + mda procmail; + ''; + mode = "0600"; + inherit user; + }; + "homeSetup/.procmailrc" = { + text = '' + DEFAULT=$HOME/mail + ''; + mode = "0600"; + inherit user; + }; + "homeSetup/.msmtprc" = { + text = '' + account ${user} + host ${serverName}.server.com + domain ${user}.com + port 465 + from ${user}@${user}.com + user ${user} + password ${user}-password + auth on + tls on + tls_starttls off + ''; + mode = "0600"; + inherit user; + }; + }; + }; +} diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age new file mode 100644 index 0000000..97b9be7 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age @@ -0,0 +1,11 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSAxanpG +VWxCYjF3aWFUUEM0b2Q2bGJUNEJZMGFlVkJpTFg3eUc1UGdyblF3CnlJVXY2Ti9z +SmltMmIzM25jajl3WE5kMkVsY05NUkpDdzlJVHdJYXByZjQKLT4gU0t5SSxMLWdy +ZWFzZSB6Tgova0Juc0x3RlFrR1NSVzBIMllYRmZiRXlzN2hSdHZQaFVDS3FPUFNr +NzBiM0Q5dExOREgydFpKWm1MaGQ5QkxBCgotLS0gNC81ZHQ5eFBrUGJxWXF6dWF4 +Mlg0WHBXS2RqeW1uY1hGUVJXbHpUaDhlWQpih0QTGjejnwIQ2lvDFS1wbNiiOJ+c +awJ2tX8chzWm+wOECaIZAqwW2NwVZj5Sj+Vzv6LQ1BVaQAiEN41GRvjyP/u3X+d+ +LKI3bPa8DWxQNd7/zAhFjSB1KEIBrqGb2GtW/Yv8Mu07V8IV/MaGUwpDOXgvFQVH +UQ1qpM0R1r190IuV2Y7M558J42crH9/5mIvMH5rW++Ru +-----END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/public b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public new file mode 100644 index 0000000..0f3c3b2 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public @@ -0,0 +1 @@ +cLWzd3zg51ITME1Fnu16/h07lXIUxfhdLivktUMoVQs= \ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age new file mode 100644 index 0000000..6bd9e28 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age @@ -0,0 +1,13 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBtQ1lC +bmtXZWlqeE1SWmR2d1JTcEZMZFRKZW1jVnRGSHpUMEM1cnpvTkdFCkRVbmlNV0ZS +SDdobTFFQUcyU3dMVXlTditvZXN0L0pPN1ZHaWtxOFZSL1EKLT4gfSM6JGpjOlot +Z3JlYXNlICdRYiBLKlV6CmZmczdSa2U5cWl6OG5QL0VaUGsyNUlCVFJ1UjJxVnpV +ZE1sN2lSRTgzVjI1S3pJVzdqN05WUVZmaTRYMXptb0kKczJEOG9EM2xtMFRHd3Vt +TUpiK2RzZkRwZTZqb3lEOGpKNy80Vk9BVDlSNjhYSkROYlVGQ1ZESGhIV3ZJWVEK +LS0tIEk0MXVEci9ITERYRzZFbTJJQWxSQzhFV3NqV2o3M0NvVlNhLzhhVkJYcTQK +GJtIH4AxSSwZhnLn5IUhOihz9Ai2lLnf00uhvF6+i29TtyEgxgWhisBJtzShB/Aq +Bct5em093jryJPNQBNDJpImEViP9WS/kTqQG0bnu2i/Nr5+vZyRcK8qv75guMxki +p7sUirbzCNtA+5JGGJb30PqOAWpflBPL0fkC5L7JyAjhNRCOgIL+QQS3mosU1AYJ +izFOdod2DA== +-----END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/public b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public new file mode 100644 index 0000000..ddea670 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public @@ -0,0 +1 @@ +3yrKD52yd5hBA6ue5uQVl7FXGK8UOlUE9Y+yCdBRfVQ= \ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh new file mode 100755 index 0000000..1e090f4 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh @@ -0,0 +1,33 @@ +#! /usr/bin/env nix-shell +#! nix-shell -p rage -p openssl -p dash -i dash --impure + +cd "$(dirname "$0")" || { + echo "No basedir?!" + exit 1 +} + +key_name="$1" +[ -z "$key_name" ] && { + echo "Usage: $0 KEY_NAME" + exit 2 +} + +[ -d "$key_name" ] || mkdir "$key_name" +cd "$key_name" || { + echo "Just created." + exit 1 +} + +openssl genpkey -algorithm ed25519 -out "private" +openssl pkey -in "private" -pubout -out "public.tmp" + +openssl asn1parse -in "public.tmp" -offset 12 -noout -out /dev/stdout | base64 --wrap 0 >"public" +rm "public.tmp" + +rage --encrypt \ + --armor \ + --recipient "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILxdvBk/PC9fC7B5vqe9TvygZKY6LgDQ2mXRdVrthBM/" \ + "private" >"private.age" +rm "private" + +# vim: ft=sh diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age new file mode 100644 index 0000000..03bb0b1 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age @@ -0,0 +1,10 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBiZmcz +OVlacDhjS3pCWlZCYlpyVVFoQWxFN1Z0eTJIY2ovNVg2MjQ1SXpVClRZUnhkblFI +c0VyelMxYzBsZ2NMUEVzWmtTSzJuVTdoVHFiZEc5cEd3aHMKLT4gJyctZ3JlYXNl +IG1tIFt2YSAwCkQvY2VnMlBTSHhPbXJ2NE55ck0KLS0tIGkxWHdQb0NIVmZyaTdW +bmorU2NLMjByakpTMlo1NUtFQ0NFd1YvOC9EaFEKtDNLHVtnsFiyhsREJOPq1xlk +74MURNlYnlF1IMrUaA3oUQSR5M34Crg7rHtjF54OsRhm79Y1dGHWeeC3evVNVpY3 +1dn/q/12aWIzT/TgGcSi3bK5fPkv+nMs/WPKTREHJ1HcWLGDeH6e8uTV7lAwiSuP +PjYhDbnNUCMMyaBsgbtCnMe8HuHdTwXQWuh0dApS5iL3z8qoSQ== +-----END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public new file mode 100644 index 0000000..4941b85 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public @@ -0,0 +1 @@ +quDd9+ogqiIUWybfegosFFkG7jAsblij2VrkuUXEzzY= \ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age new file mode 100644 index 0000000..6768973 --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age @@ -0,0 +1,13 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBBZ3Ex +ZWNUU01JK0ZGOEhEWUpEM3JLdXpjc3doMVA2RUI5TVVpblQxS0JJCnlHRmdVVk05 +MW56cEt4M00raE1nU3JZaTMvTXdUQXdzbTYvVElqVjdvNWcKLT4gRnJFLWdyZWFz +ZSA5bEhvWn4Kb01McHBBekVTalcwM0tob3VUd3NuVFlvZUpnSFQxbXVBaEJNMVlQ +K1BiSjRCL1YrZDZoSnFBNU9aQkQyNjRoSwpqcnBnd2NJQlMxaHdoa0pPWGR0SEZO +SU5DNjFxb3JQTTZITVZNRGF1VUR4Zm9laWhYd3lHZityRTNJVVF1bXdwCnhGYzMK +LS0tIFFsN0Q3V1pxWUduSU9xd21uVEF2R0tJcURYa1FOTS9kMDh6RGkwNS9SMUEK +Ni+1WbmAiavBCwLg8r1nvVipXQJ2/cItN1MgWlYe0+UrgLxRU5VLhoWi9BEulGEV +KHkNWyMCK4Tl/NJt1PAQVJ6QBVHYYxIYQWY1QkNCqXe1YdaJ5jDcWGSZdhbCrzMN +3tx3EPhigU2DiQZB6l4OOaHLjAw2a+POVwwsCavnRp7vEhs/5O2t5Lo2vCoDGCot +6o+Sdr86mw== +-----END AGE ENCRYPTED FILE----- diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public new file mode 100644 index 0000000..5c4406d --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public @@ -0,0 +1 @@ +th9exwaYvoAjxW1tAj3k/VNLl5jKzSC/dxKrxM2mTZE= \ No newline at end of file diff --git a/tests/by-name/em/email-dns/secrets/hostKey b/tests/by-name/em/email-dns/secrets/hostKey new file mode 100644 index 0000000..79c9d6c --- /dev/null +++ b/tests/by-name/em/email-dns/secrets/hostKey @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPwAAAJjFZPqHxWT6 +hwAAAAtzc2gtZWQyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPw +AAAEA9D5AP+Uqhrg8rPx2DjgucjfnJknkk7lkeKHMV04ZZv7xdvBk/PC9fC7B5vqe9Tvyg +ZKY6LgDQ2mXRdVrthBM/AAAAFSAnUHVibGljIHRlc3Rpbmcga2V5Jw== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/by-name/em/email-dns/test.nix b/tests/by-name/em/email-dns/test.nix new file mode 100644 index 0000000..32447ae --- /dev/null +++ b/tests/by-name/em/email-dns/test.nix @@ -0,0 +1,203 @@ +{ + 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-dns"; + + node = { + specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;}; + + # Use the nixpkgs as constructed by the `nixpkgs.*` options + pkgs = null; + }; + + nodes = { + acme = { + nodes, + lib, + ... + }: { + imports = [./nodes/acme]; + networking.nameservers = lib.mkForce [ + nodes.name_server.networking.primaryIPAddress + ]; + }; + + name_server = import ./nodes/name_server.nix {inherit extraModules;}; + + mail1_server = + mkMailServer "mail1" + { + class = "individual"; + name = "bob"; + secret = "bob-password"; + email = ["bob@bob.com"]; + }; + + mail2_server = + mkMailServer "mail2" + { + class = "individual"; + name = "alice"; + secret = "alice-password"; + email = ["alice@alice.com"]; + }; + + bob = mkUser "bob" "mail1"; + alice = mkUser "alice" "mail2"; + }; + + testScript = {...}: let + checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" '' + set -xe + + # fetchmail returns EXIT_CODE 1 when no new mail + fetchmail --verbose >&2 || [ "$?" -eq 1 ] || { + echo "Mail was not empty" >&2 + exit 1 + } + ''; + checkEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" '' + set -xe + + # fetchmail returns EXIT_CODE 1 when no new mail + fetchmail --verbose >&2 || [ "$?" -ne 1 ] || { + echo "No new mail" >&2 + exit 1 + } + ''; + checkSpamEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" '' + set -xe + + # fetchmail returns EXIT_CODE 1 when no new mail + fetchmail --folder JUNK --verbose >&2 || [ "$?" -ne 1 ] || { + echo "No new mail" >&2 + exit 1 + } + ''; + inherit (pkgs) lib; + in + /* + python + */ + '' + from time import sleep + + # 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() + + mail1_server.wait_for_unit("stalwart-mail.service") + mail1_server.wait_for_open_port(993) # imap + mail1_server.wait_for_open_port(465) # smtp + mail2_server.wait_for_unit("stalwart-mail.service") + mail2_server.wait_for_open_port(993) # imap + mail2_server.wait_for_open_port(465) # smtp + + alice.wait_for_unit("multi-user.target") + bob.wait_for_unit("multi-user.target") + + name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.alice.com/cert.pem") + name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.bob.com/cert.pem") + + with subtest("Add pebble ca key to all services"): + for node in [name_server, mail1_server, mail2_server, alice, 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("Both 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(mail1_server) + all_services_running(mail2_server) + + with subtest("Both start without mail"): + alice.succeed("sudo -u alice ${checkEmailEmpty}") + bob.succeed("sudo -u bob ${checkEmailEmpty}") + + with subtest("Alice can send an empty email to bob"): + alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" '' + set -xe + + echo "" | msmtp --debug --account alice bob@bob.com >&2 + ''}") + + # Give `mail2_server` some time to send the email. + sleep(160) + + bob.succeed("sudo -u bob ${checkSpamEmailNotEmpty}") + + with subtest("Alice can send an non-empty email to bob"): + alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" '' + set -xe + + cat << EOF | msmtp --debug --account alice bob@bob.com >&2 + Subject: Hi bob, I'm Alice! + + Good day, Bob! + + This is an email. + It contains a subject and a body. + I also assert utf8 support by including my last name in this very message. + + XOXO + Alice van DÃ¥ligen. + + . + EOF + ''}") + + # Give `mail2_server` some time to send the email. + sleep(120) + + bob.succeed("sudo -u bob ${checkEmailNotEmpty}") + + mail1_server.copy_from_vm("/var/lib/", "server1") + mail2_server.copy_from_vm("/var/lib/", "server2") + bob.copy_from_vm("/home/bob/mail", "bob") + ''; + } |