aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/README.md2
-rw-r--r--tests/by-name/at/atuin-sync/test.nix189
-rw-r--r--tests/by-name/ba/back/test.nix150
-rw-r--r--tests/by-name/dn/dns/test.nix129
-rw-r--r--tests/by-name/em/email-dns/nodes/mail_server.nix52
-rw-r--r--tests/by-name/em/email-dns/nodes/name_server.nix292
-rw-r--r--tests/by-name/em/email-dns/nodes/user.nix70
-rw-r--r--tests/by-name/em/email-dns/test.nix167
-rw-r--r--tests/by-name/em/email-http/nodes/mail_server.nix57
-rw-r--r--tests/by-name/em/email-http/nodes/user.nix26
-rw-r--r--tests/by-name/em/email-http/test.nix100
-rw-r--r--tests/by-name/em/email-ip/test.nix174
-rw-r--r--tests/by-name/gi/git-server/test.nix11
-rw-r--r--tests/by-name/ro/rocie/secrets/login.age16
-rw-r--r--tests/by-name/ro/rocie/test.nix106
-rw-r--r--tests/by-name/ru/rust-motd/test.nix63
-rw-r--r--tests/by-name/sh/sharkey-cpu/test.nix81
-rw-r--r--tests/by-name/sh/sharkey/test.nix102
-rw-r--r--tests/by-name/ta/taskchampion-sync/test.nix150
-rwxr-xr-xtests/common/acme/certs/generate66
-rwxr-xr-xtests/common/acme/certs/generate.ca38
-rwxr-xr-xtests/common/acme/certs/generate.client (renamed from system/services/taskserver/certs/generate.client)46
-rw-r--r--tests/common/acme/certs/output/acme.test.cert.pem11
-rw-r--r--tests/common/acme/certs/output/acme.test.key.pem25
-rw-r--r--tests/common/acme/certs/output/acme.test.template5
-rw-r--r--tests/common/acme/certs/output/ca.cert.pem10
-rw-r--r--tests/common/acme/certs/output/ca.key.pem25
-rw-r--r--tests/common/acme/certs/output/ca.template5
-rw-r--r--tests/common/acme/certs/snakeoil-certs.nix13
-rw-r--r--tests/common/acme/client.nix21
-rw-r--r--tests/common/acme/default.nix47
-rw-r--r--tests/common/acme/server.nix91
-rw-r--r--tests/common/dns/client.nix10
-rw-r--r--tests/common/dns/server.nix43
-rw-r--r--tests/common/email/dkim/alice.com/private.age15
-rw-r--r--tests/common/email/dkim/alice.com/public1
-rw-r--r--tests/common/email/dkim/bob.com/private.age14
-rw-r--r--tests/common/email/dkim/bob.com/public1
-rwxr-xr-xtests/common/email/dkim/gen_key.sh35
-rw-r--r--tests/common/email/dkim/mail1.server.com/private.age15
-rw-r--r--tests/common/email/dkim/mail1.server.com/public1
-rw-r--r--tests/common/email/dkim/mail2.server.com/private.age15
-rw-r--r--tests/common/email/dkim/mail2.server.com/public1
-rw-r--r--tests/common/email/hostKey7
44 files changed, 2431 insertions, 67 deletions
diff --git a/tests/README.md b/tests/README.md
index 7811f32..aaa76b4 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -1,6 +1,6 @@
# Tests
This directory tree mirrors the modules defined in the
-[modules](%60../modules/%60) directory. Each module should have at least
+[modules](%60../modules/%60) directory. Each module should have at least
one test in the mirrored directory, effectively replacing the module's
`module.nix` file.
diff --git a/tests/by-name/at/atuin-sync/test.nix b/tests/by-name/at/atuin-sync/test.nix
new file mode 100644
index 0000000..d65c216
--- /dev/null
+++ b/tests/by-name/at/atuin-sync/test.nix
@@ -0,0 +1,189 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs;
+ name = "atuin-sync";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = let
+ atuinSession = "01969ec6b8d07e30a9d2df0911fbfe2a";
+ in {
+ acme = {
+ imports = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "atuin-sync.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ atuin-sync = {
+ enable = true;
+ fqdn = "atuin-sync.server";
+ };
+ };
+ };
+
+ client1 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+ environment.systemPackages = [
+ pkgs.atuin
+ pkgs.sqlite-interactive
+ ];
+ };
+ client2 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.sessionVariables.ATUIN_SESSION = atuinSession;
+
+ environment.systemPackages = [
+ pkgs.atuin
+ pkgs.sqlite-interactive
+ ];
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ syncLogin = pkgs.writeShellScript "login-atuin-sync-account" ''
+ atuin login --username syncy --password password1234 --key "$1"
+ '';
+
+ syncRegister = pkgs.writeShellScript "register-atuin-sync-account" ''
+ atuin register --username syncy --email syncy@email.com --password password1234
+ '';
+
+ mkSyncConfig = pkgs.writeShellScript "register-atuin-sync-account" ''
+ mkdir --parents ~/.config/atuin/
+ cat << EOF > ~/.config/atuin/config.toml
+ sync_address = "https://atuin-sync.server"
+
+ # Use the v2 sync
+ [sync]
+ records = true
+ EOF
+ '';
+
+ runCommandAndRecordInAtuin = pkgs.writeShellScript "run-command-and-record-in-atuin" ''
+ # SPDX-SnippetBegin
+ # SPDX-SnippetCopyrightText: 2023 mentalisttraceur (https://github.com/mentalisttraceur)
+ # Source: https://github.com/atuinsh/atuin/issues/1188#issuecomment-1698354107
+ run_and_record_in_atuin()
+ {
+ local id
+ local status
+ local escaped_command="$(printf '%q ' "$@")"
+ id="$(atuin history start -- "$escaped_command")"
+ "$@"
+ status=$?
+ atuin history end --exit $status "$id"
+ return $status
+ }
+ # SPDX-SnippetEnd
+
+ run_and_record_in_atuin "$@"
+ '';
+
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["server" "client1" "client2"]
+ # Python
+ ''
+ server.wait_for_unit("atuin.service")
+ server.wait_for_open_port(443)
+
+ # Wait for the server to acquire the acme certificate
+ client1.wait_until_succeeds("curl https://atuin-sync.server")
+
+ with subtest("Setup client syncing"):
+ # See https://docs.atuin.sh/guide/sync/
+ for client in [client1, client2]:
+ client.succeed("${mkSyncConfig}")
+
+ client1.succeed("${syncRegister}")
+ client2.succeed(f"${syncLogin} '{client1.succeed("atuin key")}'")
+
+ with subtest("Can import shell history"):
+ client1.succeed("${runCommandAndRecordInAtuin} echo hi - client 1")
+ client2.succeed("${runCommandAndRecordInAtuin} echo hi - client 2")
+
+ with subtest("Can sync tasks"):
+ for client in [client1, client2]:
+ client.succeed("atuin sync --force")
+ client1.succeed("atuin sync --force")
+
+
+ with subtest("Have correct tasks"):
+ hist1 = client1.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+ hist2 = client2.succeed("atuin history list --session --format '{command}'").strip().split('\n')
+
+ hist1.sort()
+ hist2.sort()
+
+ canonicalHistory = [
+ "echo hi - client 1",
+ "echo hi - client 2"
+ ]
+
+ assert hist1 == hist2, f"The clients don't have the same amount of history items, client1: '{hist1}', client2: '{hist2}'"
+ assert hist1 == canonicalHistory, f"The history is not correct: '{hist1}' vs. '{canonicalHistory}'"
+ '';
+}
diff --git a/tests/by-name/ba/back/test.nix b/tests/by-name/ba/back/test.nix
index 63f2837..41d6c8e 100644
--- a/tests/by-name/ba/back/test.nix
+++ b/tests/by-name/ba/back/test.nix
@@ -8,9 +8,19 @@
nixLib,
...
}: let
- gitRepoPath = "/srv/test/repo";
-
domain = "server";
+
+ sshKeys =
+ import ../../gi/git-server/ssh_keys.nix {inherit pkgs;};
+
+ gitoliteAdminConfSnippet = pkgs.writeText "gitolite-admin-conf-snippet" ''
+ repo CREATOR/[a-zA-Z0-9].*
+ C = @all
+ RW+ = CREATOR
+ RW = WRITERS
+ R = READERS
+ option user-configs = cgit\.owner cgit\.desc cgit\.section cgit\.homepage
+ '';
in
nixos-lib.runTest {
hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
@@ -26,7 +36,7 @@ in
nodes = {
server = {config, ...}: {
- environment.systemPackages = [pkgs.git pkgs.git-bug pkgs.gawk];
+ environment.systemPackages = [pkgs.git];
imports =
extraModules
@@ -35,28 +45,41 @@ in
];
vhack = {
+ persist.enable = true;
+ openssh.enable = true;
nginx = {
enable = true;
selfsign = true;
};
git-server = {
enable = true;
+ domain = "git.${domain}";
+ gitolite.adminPubkey = sshKeys.admin.pub;
};
- back = {
+ git-back = {
enable = true;
- repositories = {
- "${gitRepoPath}" = {
- enable = true;
- domain = "${domain}";
- port = 9220;
- };
- };
+ domain = "issues.${domain}";
};
};
};
- client = {...}: {
- environment.systemPackages = [pkgs.curl];
+ client = {nodes, ...}: {
+ environment.systemPackages = [pkgs.git pkgs.curl pkgs.git-bug pkgs.gawk];
+ programs.ssh.extraConfig = ''
+ Host *
+ UserKnownHostsFile /dev/null
+ StrictHostKeyChecking no
+ # there's nobody around that can input password
+ PreferredAuthentications publickey
+ '';
+ users.users.alice = {isNormalUser = true;};
+ networking.hosts = {
+ "${nodes.server.networking.primaryIPAddress}" = [
+ "git.${domain}"
+ "issues.${domain}"
+ "${domain}"
+ ];
+ };
};
};
@@ -67,55 +90,114 @@ in
''
start_all()
- with subtest("can setup git-bug issues on server"):
- server.succeed("${pkgs.writeShellScript "setup-git-repo" ''
- set -ex
+ with subtest("can setup ssh keys on client"):
+ client.succeed(
+ "mkdir -p ~root/.ssh",
+ "cp ${sshKeys.admin.priv} ~root/.ssh/id_ed25519",
+ "chmod 600 ~root/.ssh/id_ed25519",
+ )
+ client.succeed(
+ "sudo -u alice mkdir -p ~alice/.ssh",
+ "sudo -u alice cp ${sshKeys.alice.priv} ~alice/.ssh/id_ed25519",
+ "sudo -u alice chmod 600 ~alice/.ssh/id_ed25519",
+ )
+
+ with subtest("gitolite server starts"):
+ server.wait_for_unit("gitolite-init.service")
+ server.wait_for_unit("sshd.service")
+ client.succeed("ssh -n git@git.${domain} info")
+
+
+ with subtest("admin can clone and configure gitolite-admin.git"):
+ server.succeed("sudo -u git ${pkgs.writeShellScript "delete_main_branch_on_server" ''
+ set -xe
+
+ cd ~git/repositories/gitolite-admin.git
+ git branch --move --force main master
+ ''}")
+ client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" ''
+ set -xe
+
+ git clone git@git.${domain}:gitolite-admin.git
+ git config --global user.name 'System Administrator'
+ git config --global user.email root\@domain.example
- mkdir --parents "${gitRepoPath}"
- cd "${gitRepoPath}"
+ cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub
- git init
+ (cd gitolite-admin && git add . && git commit -m 'Add keys for alice' && git push -u origin master)
+ cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf
+ (cd gitolite-admin && git add . && git commit -m 'Add support for wild repos' && git push)
+ ''}")
+
+ with subtest("alice can create a repo"):
+ client.succeed("sudo -u alice ${pkgs.writeShellScript "alice-create-repo" ''
+ set -xe
+
+ mkdir --parents ./alice/repo1 && cd alice/repo1;
+
+ git init --initial-branch main
+ echo "# Alice's Repo" > README.md
+ git add README.md
+ git -c user.name=Alice -c user.email=alice@domain.example commit -m 'Add readme'
+
+ git remote add origin git@git.${domain}:alice/repo1.git
+ git push --set-upstream origin main
+ ''}")
- git bug user create --avatar "" --email "test@email.org" --name "test user" --non-interactive
+ with subtest("can setup git-bug issues in alice/repo1"):
+ client.succeed("sudo -u alice ${pkgs.writeShellScript "setup-git-repo" ''
+ set -ex
+
+ cd alice/repo1
- git bug add \
+ git bug user new --avatar "" --email "alice@server.org" --name "alice" --non-interactive
+
+ git bug bug new \
--title "Some bug title" \
--message "A long description of the bug. Probably has some code segments, maybe even *markdown* mark_up_ or other things" \
--non-interactive
- git bug add \
+ git bug bug new \
--title "Second bug title" \
--message "" \
--non-interactive
- git bug add \
+ git bug bug new \
--title "Third bug title" \
--message "" \
--non-interactive
- git bug select "$(git bug ls --format plain | awk '{print $1}' | head -n 1)"
+ git bug bug select "$(git bug bug --format plain | awk '{print $1}' | head -n 1)"
+
+ git bug bug comment new --message "Some comment message" --non-interactive
+ git bug bug comment new --message "Second comment message" --non-interactive
- git bug comment add --message "Some comment message" --non-interactive
- git bug comment add --message "Second comment message" --non-interactive
+ # TODO: This should use `git bug push`, but their ssh implementation is just
+ # too special to work in a VM test <2025-03-08>
+ git push origin +refs/bugs/*
+ git push origin +refs/identities/*
- # NOTE(@bpeetz): Currently, the `back` module assumes that the git user can write
- # to the repository, as such we need to provide write access here <2024-12-24>
- chown --recursive git:git "${gitRepoPath}"
+ ssh git@git.${domain} -- config alice/repo1 --add cgit.owner Alice
+ ssh git@git.${domain} -- perms alice/repo1 + READERS @all
''}")
with subtest("back server starts"):
- server.wait_for_unit("${builtins.replaceStrings ["/"] ["_"] "back-${domain}.service"}")
+ server.wait_for_unit("back.service")
with subtest("client can access the server"):
client.succeed("${pkgs.writeShellScript "curl-back" ''
set -xe
- curl --insecure --silent --fail --show-error "https://${domain}/issues/open" --output /root/issues.html
-
- grep -- '- 2 comments' /root/issues.html
+ curl --insecure --fail --show-error "https://issues.${domain}/alice/repo1/issues/?query=status:open" --output /root/issues.html
grep -- 'Second bug title' /root/issues.html
- ''}")
+
+ curl --insecure --fail --show-error "https://issues.${domain}/" --output /root/repos.html
+ grep -- 'repo' /root/repos.html
+ grep -- "&lt;No description&gt;" /root/repos.html
+ grep -- '<span class="user-name">Alice</span>' /root/repos.html
+ ''} >&2")
client.copy_from_vm("/root/issues.html", "");
+ client.copy_from_vm("/root/repos.html", "");
'';
}
diff --git a/tests/by-name/dn/dns/test.nix b/tests/by-name/dn/dns/test.nix
new file mode 100644
index 0000000..01d8833
--- /dev/null
+++ b/tests/by-name/dn/dns/test.nix
@@ -0,0 +1,129 @@
+# Inspired by this file: /nixpkgs/nixos/tests/nsd.nix
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}: let
+ common = {...}: {
+ networking.firewall.enable = false;
+ networking.dhcpcd.enable = false;
+ };
+
+ mkClient = version: {
+ lib,
+ nodes,
+ ...
+ }: {
+ environment.systemPackages = [pkgs.dig pkgs.dig.dnsutils];
+
+ imports = [common];
+ networking.nameservers = lib.mkForce [
+ (lib.head nodes.server.networking.interfaces.eth1."${version}".addresses).address
+ ];
+ };
+in
+ nixos-lib.runTest {
+ hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+ name = "dns";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ server = {
+ config,
+ lib,
+ ...
+ }: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ common
+ ];
+
+ vhack = {
+ dns = {
+ enable = true;
+ interfaces = [
+ (lib.head config.networking.interfaces.eth1.ipv4.addresses).address
+ (lib.head config.networking.interfaces.eth1.ipv6.addresses).address
+ ];
+ zones = {
+ "example.com" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@example.com";
+ serial = 2024012301;
+ };
+
+ useOrigin = false;
+ NS = [
+ "ns.example.com."
+ ];
+
+ subdomains = {
+ ns = {
+ A = ["192.168.1.3"];
+ };
+ ipv4 = {
+ A = ["1.2.3.4"];
+ };
+ ipv6 = {
+ AAAA = ["dead:beef::1"];
+ };
+ openpgpkey = {
+ TXT = ["Hi!"];
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+
+ clientV4 = mkClient "ipv4";
+ clientV6 = mkClient "ipv6";
+ };
+
+ testScript = {nodes, ...}:
+ /*
+ python
+ */
+ ''
+ start_all()
+
+ clientV4.wait_for_unit("network.target")
+ clientV6.wait_for_unit("network.target")
+ server.wait_for_unit("nsd.service")
+
+ def assert_host(ipVersion, dnsRecordType, dnsQuery, expected):
+ self = clientV4 if ipVersion == 4 else clientV6
+ out = self.succeed(f"host -{ipVersion} -t {dnsRecordType} {dnsQuery}").rstrip()
+ self.log(f"output: {out}")
+ import re
+ assert re.search(expected, out), f"DNS IPv{ipVersion} dnsQuery on {dnsQuery} gave '{out}' instead of '{expected}'"
+
+
+ for ipv in 4, 6:
+ with subtest(f"IPv{ipv}"):
+ assert_host(ipv, "a", "example.com", "has no [^ ]+ record")
+ assert_host(ipv, "aaaa", "example.com", "has no [^ ]+ record")
+
+ assert_host(ipv, "soa", "example.com", "SOA.*?admin\\.example\\.com")
+ assert_host(ipv, "a", "ipv4.example.com", "address 1.2.3.4$")
+ assert_host(ipv, "aaaa", "ipv6.example.com", "address dead:beef::1$")
+
+ assert_host(ipv, "txt", "openpgpkey.example.com", "descriptive text \"Hi!\"$")
+ '';
+ }
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..279d289
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/mail_server.nix
@@ -0,0 +1,52 @@
+{
+ extraModules,
+ pkgs,
+ vhackPackages,
+}: {
+ mkMailServer = serverName: principal: {
+ config,
+ lib,
+ nodes,
+ ...
+ }: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../../modules
+ ../../../../common/acme/client.nix
+ ../../../../common/dns/client.nix
+ ];
+
+ environment.systemPackages = [
+ pkgs.bind
+ pkgs.openssl
+ ];
+
+ 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 {
+ "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";
+ };
+ 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..bde1a16
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/name_server.nix
@@ -0,0 +1,292 @@
+{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 = "reject";
+ 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
+ ../../../../common/acme/client.nix
+ ../../../../common/dns/server.nix
+ ];
+
+ 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.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
+ ];
+ };
+ "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..fba02ce
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/user.nix
@@ -0,0 +1,70 @@
+{
+ pkgs,
+ vhackPackages,
+}: {
+ mkUser = user: serverName: {
+ nodes,
+ lib,
+ ...
+ }: {
+ imports = [
+ ../../../../common/acme/client.nix
+ ../../../../common/dns/client.nix
+ ];
+
+ environment.systemPackages = [
+ vhackPackages.fetchmail-common-name
+ pkgs.msmtp
+ pkgs.procmail
+
+ pkgs.bind
+ pkgs.openssl
+ ];
+
+ 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/test.nix b/tests/by-name/em/email-dns/test.nix
new file mode 100644
index 0000000..c7ba3b3
--- /dev/null
+++ b/tests/by-name/em/email-dns/test.nix
@@ -0,0 +1,167 @@
+{
+ 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 = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+
+ name_server = import ./nodes/name_server.nix {inherit extraModules;};
+
+ mail1_server =
+ mkMailServer "mail1"
+ {
+ class = "individual";
+ name = "bob";
+ secret = "{PLAIN}bob-password";
+ email = ["bob@bob.com"];
+ };
+
+ mail2_server =
+ mkMailServer "mail2"
+ {
+ class = "individual";
+ name = "alice";
+ secret = "{PLAIN}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
+ }
+ '';
+
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["mail1_server" "mail2_server" "alice" "bob"]
+ # Python
+ ''
+ from time import sleep
+
+ 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("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")
+ '';
+ }
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/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..82b4c45
--- /dev/null
+++ b/tests/by-name/em/email-http/test.nix
@@ -0,0 +1,100 @@
+{
+ 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/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "mail.server.com" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.mail_server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.mail_server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ 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 = _: let
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["mail_server" "bob"]
+ # Python
+ ''
+ 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("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", "")
+ '';
+ }
diff --git a/tests/by-name/em/email-ip/test.nix b/tests/by-name/em/email-ip/test.nix
new file mode 100644
index 0000000..dabc404
--- /dev/null
+++ b/tests/by-name/em/email-ip/test.nix
@@ -0,0 +1,174 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}: let
+ domain = "mail.server.test";
+
+ scripts = {
+ checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+ set -xe
+
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ fetchmail --nosslcertck --verbose >&2 || [ "$?" -eq 1 ] || {
+ echo "Expected exit code 1." >&2
+ exit 1
+ }
+ '';
+ };
+
+ mkUser = user: {nodes, ...}: let
+ domainIp = nodes.server.networking.primaryIPAddress;
+ in {
+ environment.systemPackages = with pkgs; [
+ fetchmail
+ msmtp
+ procmail
+ ];
+
+ 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 "${domainIp}" 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 ${domainIp}
+ port 465
+ from ${user}@${domain}
+ user ${user}
+ password ${user}-password
+ auth on
+ tls on
+ tls_starttls off
+ '';
+ mode = "0600";
+ inherit user;
+ };
+ };
+ };
+in
+ nixos-lib.runTest {
+ hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+ name = "email";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ];
+
+ vhack = {
+ nginx = {
+ enable = true;
+ selfsign = true;
+ };
+
+ stalwart-mail = {
+ enable = true;
+ fqdn = domain;
+ admin = "mailto:admin@${domain}";
+ security = null;
+ openFirewall = true;
+ principals = [
+ {
+ class = "individual";
+ name = "alice";
+ secret = "{PLAIN}alice-password";
+ email = ["alice@${domain}"];
+ }
+ {
+ class = "individual";
+ name = "bob";
+ secret = "{PLAIN}bob-password";
+ email = ["bob@${domain}"];
+ }
+ ];
+ };
+ };
+ };
+
+ alice = mkUser "alice";
+ bob = mkUser "bob";
+ };
+
+ testScript = {...}:
+ /*
+ python
+ */
+ ''
+ start_all()
+
+ server.wait_for_unit("stalwart-mail.service")
+ server.wait_for_open_port(993) # imap
+ server.wait_for_open_port(465) # smtp
+
+ with subtest("Both start without mail"):
+ alice.succeed("sudo -u alice ${scripts.checkEmailEmpty}")
+ bob.succeed("sudo -u bob ${scripts.checkEmailEmpty}")
+
+ with subtest("Alice can send an email to bob"):
+ alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+ set -xe
+
+ cat << EOF | msmtp --debug --account alice --tls-certcheck=off bob@${domain} >&2
+ Hi Bob!
+
+ This is an email.
+ It contains a subject and a body.
+
+ ALICE
+ EOF
+ ''}")
+ bob.succeed("sudo -u bob ${pkgs.writeShellScript "bob-receive" ''
+ set -xe
+
+ fetchmail --nosslcertck --verbose >&2 || {
+ echo New Mail did not arrive
+ exit 1
+ }
+ ''}")
+
+ server.copy_from_vm("/var/lib/", "server")
+ bob.copy_from_vm("/home/bob/mail", "bob")
+ '';
+ }
diff --git a/tests/by-name/gi/git-server/test.nix b/tests/by-name/gi/git-server/test.nix
index 5cd8c33..4e503b6 100644
--- a/tests/by-name/gi/git-server/test.nix
+++ b/tests/by-name/gi/git-server/test.nix
@@ -122,6 +122,12 @@ in
with subtest("admin can clone and configure gitolite-admin.git"):
+ server.succeed("sudo -u git ${pkgs.writeShellScript "delete_main_branch_on_server" ''
+ set -xe
+
+ cd ~git/repositories/gitolite-admin.git
+ git branch --move --force main master
+ ''}")
client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" ''
set -xe
@@ -132,12 +138,9 @@ in
cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub
cp ${sshKeys.bob.pub} gitolite-admin/keydir/bob.pub
- (cd gitolite-admin && git switch -c master && git branch -D main)
-
(cd gitolite-admin && git add . && git commit -m 'Add keys for alice, bob' && git push -u origin master)
cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf
(cd gitolite-admin && git add . && git commit -m 'Add support for wild repos' && git push)
- (cd gitolite-admin && git push -d origin main)
''}")
server.succeed("${pkgs.writeShellScript "verify gitolite-admin.conf" ''
@@ -202,7 +205,7 @@ in
cd ~bob
# Disable ssl verification, as the certs are self-signed
- git -c http.sslVerify=false clone https://server/alice/alice-project.git
+ git -c http.sslVerify=false clone https://server/alice/alice-project
''}")
with subtest("Alice can change settings in her repo"):
diff --git a/tests/by-name/ro/rocie/secrets/login.age b/tests/by-name/ro/rocie/secrets/login.age
new file mode 100644
index 0000000..33d63be
--- /dev/null
+++ b/tests/by-name/ro/rocie/secrets/login.age
@@ -0,0 +1,16 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzMWE5dTBiU0hDUC9jUi93
+Y1phYllHRk9YSHBzUGQ2YmF5ZC9ydXNGV0JrClRpTjZZUHZ5MEFFa0VrYVVhTkE2
+eCtSaEU1YVlhNjFNYlRRYzNCdjhYRWMKLT4gWDI1NTE5IDUrZWdDUmpQcFBOcE0x
+UE5QRDR5NXVXUHdQOVk3UGV1S3lCc0pUQmZIZ00KUldVSVF3TzB0cHFVaDZuNlZR
+b2FoT0lVSTZydHFTNHhnQ3U0NGdSR1k1MAotPiBzc2gtZWQyNTUxOSBSc2dXcFEg
+dldGZU15UXgrRTMxRkp2MEVKUllWQ3VFdnJDMnM4OS8wc202WW9lNW5BYwpPWjV0
+cmNuaDlPZndtUVVScm5TaGlvVUhHa0JiN1MvbDhCTTUxYzNhM3RRCi0+IDxXeEhv
+cC1ncmVhc2UKM3N5OHRLNTJEY1NIeGlWYm9yR096Y1NpSlVOM1lYQk9jOHkxU3N2
+K2c3QitDYnR6QTJOOWczV0xBa2dEUE1PTQpYU2Z1elZwRzU0Tm1RVDE2VWVqekUw
+bFROLzU0c2NNTXYwL2N5QkxTaGtXUWxKVVF6SE0KLS0tIGlYMHIvUkJpZUR0SHo4
+cldLSTdnbU90SGJTcGZGaHkyOTZON0hka3BLdlEKeP4nHmKWvJfqgEXuiLBMzldi
+n1qIsnlF3IU1EA0abJg/RK1BFwWlx4wBlLmViw6UTL+VEw8lv23PuZl2t7UtXVzQ
+smXDapW8nInNmTaElBPdwJ072/dD0Ly+KF95Qr0FDDv+jlKG/D/Mw+xD4jvuJHSo
+2HQnPF6MLTjCxpyPPggleWgKrBQggHBjm/pHtOKmPC5qfp+LAjmQoJXny/0X6cA=
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/ro/rocie/test.nix b/tests/by-name/ro/rocie/test.nix
new file mode 100644
index 0000000..c2ba97a
--- /dev/null
+++ b/tests/by-name/ro/rocie/test.nix
@@ -0,0 +1,106 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+ name = "rocie";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ acme = {...}: {
+ imports = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "rocie.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ age.identityPaths = ["${../../../common/email/hostKey}"];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ rocie = {
+ enable = true;
+ domain = "rocie.server";
+ loginSecret = ./secrets/login.age;
+ };
+ };
+ };
+
+ client = {...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["server" "client"]
+ # Python
+ ''
+ server.wait_for_unit("rocie.service")
+
+ with subtest("All services running"):
+ 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(server)
+
+ client.wait_until_succeeds("curl --verbose https://rocie.server/api/can-be-provisioned > out.file")
+ client.copy_from_vm("out.file")
+ '';
+}
diff --git a/tests/by-name/ru/rust-motd/test.nix b/tests/by-name/ru/rust-motd/test.nix
new file mode 100644
index 0000000..6623c0c
--- /dev/null
+++ b/tests/by-name/ru/rust-motd/test.nix
@@ -0,0 +1,63 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs;
+
+ name = "rust-motd";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ];
+
+ vhack = {
+ rust-motd.enable = true;
+ };
+ };
+ };
+
+ testScript = {nodes, ...}:
+ /*
+ python
+ */
+ ''
+ from time import sleep
+
+ start_all()
+
+ # Give the service time to run.
+ sleep(3)
+
+ with subtest("All services running"):
+ 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(server)
+
+ with subtest("Motd generated"):
+ sleep(1)
+ server.succeed("cat /var/lib/rust-motd/motd | tee /dev/stderr | grep --invert-match Error")
+
+ server.copy_from_vm("/var/lib/rust-motd/motd")
+ '';
+}
diff --git a/tests/by-name/sh/sharkey-cpu/test.nix b/tests/by-name/sh/sharkey-cpu/test.nix
new file mode 100644
index 0000000..47c16ff
--- /dev/null
+++ b/tests/by-name/sh/sharkey-cpu/test.nix
@@ -0,0 +1,81 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+ name = "sharkey-cpu";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ sharkey = {
+ enable = true;
+ fqdn = "sharkey.server";
+ };
+ };
+ systemd.services = {
+ # Avoid an error from this service.
+ "acme-sharkey.server".enable = false;
+
+ # Test that sharkey's hardening still allows access to the CPUs.
+ sharkey.serviceConfig.ExecStart = let
+ nodejs = pkgs.lib.getExe pkgs.nodejs;
+ script = pkgs.writeTextFile {
+ name = "script.js";
+ text = ''
+ import * as os from 'node:os';
+
+ console.log(os.cpus()[0].model)
+ console.log(os.cpus().length)
+ '';
+ };
+ in
+ pkgs.lib.mkForce "${nodejs} ${script}";
+ };
+ };
+ };
+
+ testScript = {nodes, ...}:
+ /*
+ python
+ */
+ ''
+ from time import sleep
+
+ start_all()
+
+ # Give the service time to run.
+ sleep(3)
+
+ with subtest("All services running"):
+ 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(server)
+ '';
+}
diff --git a/tests/by-name/sh/sharkey/test.nix b/tests/by-name/sh/sharkey/test.nix
new file mode 100644
index 0000000..0d79cd2
--- /dev/null
+++ b/tests/by-name/sh/sharkey/test.nix
@@ -0,0 +1,102 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+ name = "sharkey";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable extraModules vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = {
+ acme = {...}: {
+ imports = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "sharkey.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ sharkey = {
+ enable = true;
+ fqdn = "sharkey.server";
+ };
+ };
+ };
+
+ client = {...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["server" "client"]
+ # Python
+ ''
+ server.wait_for_unit("sharkey.service")
+
+ with subtest("All services running"):
+ 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(server)
+
+ client.wait_until_succeeds("curl --silent https://sharkey.server | grep 'Thank you for using Sharkey!'")
+ '';
+}
diff --git a/tests/by-name/ta/taskchampion-sync/test.nix b/tests/by-name/ta/taskchampion-sync/test.nix
new file mode 100644
index 0000000..4bca4e0
--- /dev/null
+++ b/tests/by-name/ta/taskchampion-sync/test.nix
@@ -0,0 +1,150 @@
+{
+ nixos-lib,
+ pkgsUnstable,
+ nixpkgs-unstable,
+ vhackPackages,
+ pkgs,
+ extraModules,
+ nixLib,
+ ...
+}:
+nixos-lib.runTest {
+ hostPkgs = pkgs;
+ name = "taskchampion-sync";
+
+ node = {
+ specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+ # Use the nixpkgs as constructed by the `nixpkgs.*` options
+ pkgs = null;
+ };
+
+ nodes = let
+ taskwarriorPackage = pkgs.taskwarrior3.overrideAttrs (final: prev: {
+ cmakeFlags = (prev.cmakeFlags or []) ++ ["-DENABLE_TLS_NATIVE_ROOTS=true"];
+ });
+ in {
+ acme = {
+ imports = [
+ ../../../common/acme/server.nix
+ ../../../common/dns/client.nix
+ ];
+ };
+ name_server = {nodes, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../common/acme/client.nix
+ ../../../common/dns/server.nix
+ ];
+
+ vhack.dns.zones = {
+ "taskchampion.server" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.server.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.server.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+
+ server = {config, ...}: {
+ imports =
+ extraModules
+ ++ [
+ ../../../../modules
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ vhack = {
+ persist.enable = true;
+ nginx.enable = true;
+ taskchampion-sync = {
+ enable = true;
+ fqdn = "taskchampion.server";
+ };
+ };
+ };
+
+ task_client1 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.systemPackages = [
+ taskwarriorPackage
+ ];
+ };
+ task_client2 = {config, ...}: {
+ imports = [
+ ../../../common/acme/client.nix
+ ../../../common/dns/client.nix
+ ];
+
+ environment.systemPackages = [
+ taskwarriorPackage
+ ];
+ };
+ };
+
+ testScript = {nodes, ...}: let
+ # Generated with uuidgen
+ uuid = "bf01376e-04a4-435a-9263-608567531af3";
+ password = "nixos-test";
+
+ mkSyncConfig = path:
+ pkgs.writeShellScript "setup-config-file" ''
+ set -xe
+
+ mkdir --parents "$(dirname "${path}")"
+ echo 'sync.server.url=https://taskchampion.server' >> "${path}"
+ echo 'sync.server.client_id=${uuid}' >> "${path}"
+ echo 'sync.encryption_secret=${password}' >> "${path}"
+ '';
+
+ acme = import ../../../common/acme {inherit pkgs;};
+ in
+ acme.prepare ["server" "task_client1" "task_client2"]
+ # Python
+ ''
+ server.wait_for_unit("taskchampion-sync-server.service")
+ server.wait_for_open_port(443)
+
+ with subtest("Setup task syncing"):
+ for task in [task_client1, task_client2]:
+ # See man task-sync(5)
+ task.succeed("mkdir ~/.task")
+ task.succeed("${mkSyncConfig "$HOME/.taskrc"}")
+
+ with subtest("Can create tasks"):
+ task_client1.succeed("task add 'First task -- task_client1'")
+ task_client2.succeed("task add 'First task -- task_client2'")
+
+ # Wait for the server to acquire the acme certificate
+ task_client1.wait_until_succeeds("curl https://taskchampion.server")
+
+ with subtest("Can sync tasks"):
+ for task in [task_client1, task_client2]:
+ task.succeed("task sync")
+ task_client1.succeed("task sync")
+
+
+ with subtest("Have correct tasks"):
+ count1 = task_client1.succeed("task count")
+ count2 = task_client2.succeed("task count")
+
+ assert int(count1) == 2, f"We don't have exactly 2 tasks, but {count1}"
+ assert count1 == count2, f"The clients don't have the same amount of tasks, client1: {count1}, client2: {count2}"
+ '';
+}
diff --git a/tests/common/acme/certs/generate b/tests/common/acme/certs/generate
new file mode 100755
index 0000000..0d6258e
--- /dev/null
+++ b/tests/common/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/common/acme/certs/generate.ca b/tests/common/acme/certs/generate.ca
new file mode 100755
index 0000000..92832c5
--- /dev/null
+++ b/tests/common/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/system/services/taskserver/certs/generate.client b/tests/common/acme/certs/generate.client
index 4f0e503..5930298 100755
--- a/system/services/taskserver/certs/generate.client
+++ b/tests/common/acme/certs/generate.client
@@ -1,54 +1,44 @@
-#!/bin/sh
+#! /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
+if [ -z "$CERTTOOL" ]; then
+ echo "ERROR: No certtool found" >&2
+ exit 1
fi
-. ./vars
-
NAME=client
-if [ $# -gt 0 ]
-then
- NAME=$1
+if [ $# -gt 0 ]; then
+ NAME="$1"
fi
-if ! [ -f "$NAME".key.pem ]
-then
- # Create a client key.
- $CERTTOOL \
+# Create a client key.
+$CERTTOOL \
--generate-privkey \
- --sec-param $SEC_PARAM \
+ --sec-param "$SEC_PARAM" \
+ --key-type "$KEY_TYPE" \
--outfile "$NAME".key.pem
-fi
chmod 600 "$NAME".key.pem
-if ! [ -f "$NAME".template ]
-then
- # Sign a client cert with the key.
- cat <<EOF >"$NAME".template
-organization = $ORGANIZATION
-cn = $CN
+# Sign a client cert with the key.
+cat <<EOF >"$NAME".template
+dns_name = "$NAME"
+dns_name = "$SAN"
expiration_days = $EXPIRATION_DAYS
-tls_www_client
+organization = $ORGANIZATION
encryption_key
signing_key
EOF
-fi
-if ! [ -f "$NAME".cert.pem ]
-then
- $CERTTOOL \
+$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
-fi
chmod 600 "$NAME".cert.pem
+
+# vim: ft=sh
diff --git a/tests/common/acme/certs/output/acme.test.cert.pem b/tests/common/acme/certs/output/acme.test.cert.pem
new file mode 100644
index 0000000..687101d
--- /dev/null
+++ b/tests/common/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/common/acme/certs/output/acme.test.key.pem b/tests/common/acme/certs/output/acme.test.key.pem
new file mode 100644
index 0000000..06195b8
--- /dev/null
+++ b/tests/common/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/common/acme/certs/output/acme.test.template b/tests/common/acme/certs/output/acme.test.template
new file mode 100644
index 0000000..320a170
--- /dev/null
+++ b/tests/common/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/common/acme/certs/output/ca.cert.pem b/tests/common/acme/certs/output/ca.cert.pem
new file mode 100644
index 0000000..0fa9d14
--- /dev/null
+++ b/tests/common/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/common/acme/certs/output/ca.key.pem b/tests/common/acme/certs/output/ca.key.pem
new file mode 100644
index 0000000..64263bc
--- /dev/null
+++ b/tests/common/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/common/acme/certs/output/ca.template b/tests/common/acme/certs/output/ca.template
new file mode 100644
index 0000000..a2295d8
--- /dev/null
+++ b/tests/common/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/common/acme/certs/snakeoil-certs.nix b/tests/common/acme/certs/snakeoil-certs.nix
new file mode 100644
index 0000000..aeb6dfc
--- /dev/null
+++ b/tests/common/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/common/acme/client.nix b/tests/common/acme/client.nix
new file mode 100644
index 0000000..2b870e8
--- /dev/null
+++ b/tests/common/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/common/acme/default.nix b/tests/common/acme/default.nix
new file mode 100644
index 0000000..c756a4f
--- /dev/null
+++ b/tests/common/acme/default.nix
@@ -0,0 +1,47 @@
+{pkgs}: let
+ add_pebble_ca_certs = 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";
+ '';
+in {
+ prepare = clients: extra:
+ # The parens are needed for the syntax highlighting to work.
+ ( # 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 actual test
+ start_all()
+
+ with subtest("Add pebble ca key to all services"):
+ for node in [name_server, ${builtins.concatStringsSep "," clients}]:
+ node.wait_until_succeeds("curl https://acme.test:15000/roots/0")
+ node.succeed("${add_pebble_ca_certs}")
+ ''
+ )
+ + extra;
+}
diff --git a/tests/common/acme/server.nix b/tests/common/acme/server.nix
new file mode 100644
index 0000000..997c944
--- /dev/null
+++ b/tests/common/acme/server.nix
@@ -0,0 +1,91 @@
+# Add this node as acme server.
+# This also needs a DNS server.
+{
+ 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/common/dns/client.nix b/tests/common/dns/client.nix
new file mode 100644
index 0000000..52f3267
--- /dev/null
+++ b/tests/common/dns/client.nix
@@ -0,0 +1,10 @@
+{
+ lib,
+ nodes,
+ ...
+}: {
+ networking.nameservers = lib.mkForce [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+}
diff --git a/tests/common/dns/server.nix b/tests/common/dns/server.nix
new file mode 100644
index 0000000..0c8d72c
--- /dev/null
+++ b/tests/common/dns/server.nix
@@ -0,0 +1,43 @@
+{
+ lib,
+ nodes,
+ ...
+}: {
+ imports = [
+ ../../../modules
+ ];
+
+ networking.nameservers = lib.mkForce [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+
+ vhack = {
+ dns = {
+ enable = true;
+ openFirewall = true;
+ interfaces = [
+ nodes.name_server.networking.primaryIPAddress
+ nodes.name_server.networking.primaryIPv6Address
+ ];
+
+ zones = {
+ "acme.test" = {
+ SOA = {
+ nameServer = "ns";
+ adminEmail = "admin@server.com";
+ serial = 2025012301;
+ };
+ useOrigin = false;
+
+ A = [
+ nodes.acme.networking.primaryIPAddress
+ ];
+ AAAA = [
+ nodes.acme.networking.primaryIPv6Address
+ ];
+ };
+ };
+ };
+ };
+}
diff --git a/tests/common/email/dkim/alice.com/private.age b/tests/common/email/dkim/alice.com/private.age
new file mode 100644
index 0000000..5415fdc
--- /dev/null
+++ b/tests/common/email/dkim/alice.com/private.age
@@ -0,0 +1,15 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3dkJqTFNjOUphUWpBTmUv
+MldLUUVtUWJWY2p4NGNDVlluWUdJNUVGTDBnCkRMZ3VsVGkvUUJsbUgrUHA5d1pY
+d3lvQkdsSnRxYWxSbGlkQUlnMEFiWncKLT4gWDI1NTE5IGljWDF2a3JqZHBzZC9J
+ZjlxSVdKQ0ErRGc1VVRTNE91RC8rR2ZhMlN5d28KYVNyMWZWSnZFNS9zWUdLS01T
+UkJLaGxqWmwySzdxRk5GVUZ4NFR1aXcxcwotPiBzc2gtZWQyNTUxOSBSc2dXcFEg
+alJlckZsT21YUUQrS05Xb25TeFJXUmFFTFl1U3NYOXZoWEZwa0Qrd2dnWQoxZVla
+TFV3VURsaUxZdUZVRkZhdXcyV3AzMTJCc1hXUDJlK2FPc0pIU2hvCi0+IFN6Nlc4
+R0dqLWdyZWFzZSBfIQpUdERFc2xwZGkwR2p3QW0vNlIyR0pYTkkyeU02ckw2RnEv
+bHE5TFQ4TlBaL3lNd1ZFbDRPZXo1RmpSeStWcjljCmFpV28KLS0tIEE2SURxTHdl
+RWt5NXZRT3hFUHpjSm1BdTlwZGM2NmQvNm5Pb0s4dW5pc2sKTKoHvkHEkuPkMTDw
+Un+0mSGY82ZxfpNuYH6YEjUYsXwAw4y0HFvsObc0XuYzCmQrBX8vLos0Sg5kzboT
+EU81RdjI1AQPwhd+jBIDNFpUzYZ/IaQsz7Yp3G1WDfkeYPk10IuzOJoeRjLC9HPa
+wfYCKZDhegV6n//kAxBGM0Q3MzyxO3eYJ+k3TaWnUPLnw+nFMAANgkYqfg==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/alice.com/public b/tests/common/email/dkim/alice.com/public
new file mode 100644
index 0000000..0f3c3b2
--- /dev/null
+++ b/tests/common/email/dkim/alice.com/public
@@ -0,0 +1 @@
+cLWzd3zg51ITME1Fnu16/h07lXIUxfhdLivktUMoVQs= \ No newline at end of file
diff --git a/tests/common/email/dkim/bob.com/private.age b/tests/common/email/dkim/bob.com/private.age
new file mode 100644
index 0000000..c07c997
--- /dev/null
+++ b/tests/common/email/dkim/bob.com/private.age
@@ -0,0 +1,14 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmN0FHQ2ZhVEVpNnB1S2Zu
+T3JBS0xLUklKNUxKeENYeis3MUZHdUF1STFFCmdkTFQvcG9qWk55QjlEUDN2NGFa
+Ump1TW1xRUZ3K251bFJsaEMrTXd3VzgKLT4gWDI1NTE5IFF6ZGNJbWkrc1Z5M3Rk
+blpNUGpuNU0rZi9Ed2dicnM4WXhrbjdQVDFzeGsKd2V6clA0UjZWYmJxZytaa3B3
+T2drNkhiQVBKU2tOL2gxeGtjQnAra2NxawotPiBzc2gtZWQyNTUxOSBSc2dXcFEg
+Y2xrRDduWXo1Qk81VFMxMHZ0T3FhaXZXUm1VMDNhVGZscEJ6Q2owWXFCMApsMTQ4
+K2hGNm4ybGNkSTFOeHdwZnI5YjRZQm1uVC9xUEtRSjBxV0JKdERRCi0+IFlzLWdy
+ZWFzZSBbZztrCk1sWFhaZXNZOUhMbAotLS0gbkNRK2hkZ0dEZEkxeFVJKysyUUY3
+UWVBRjZoVXJCWk5YTEh4ZktpM1IwNArrxTAoCB+Ubb79nB53ZnbakCaDfrfLx7Oi
+Khq4CZh64r2LaQWFP0m5dhHWDF/8Fg+E42zbXN1KQwggz2h59EqI2ouOXmjiZeHN
+O50mfruF9xybdAFVIThEsjRTRQDPfO5qq6PI+g+w4s+S8kl6yp61t6z3y+zI3GGw
+RxCsY2uEn5naMuBMqkYL5dhA/G5deNMpUQ8rcRZw
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/bob.com/public b/tests/common/email/dkim/bob.com/public
new file mode 100644
index 0000000..ddea670
--- /dev/null
+++ b/tests/common/email/dkim/bob.com/public
@@ -0,0 +1 @@
+3yrKD52yd5hBA6ue5uQVl7FXGK8UOlUE9Y+yCdBRfVQ= \ No newline at end of file
diff --git a/tests/common/email/dkim/gen_key.sh b/tests/common/email/dkim/gen_key.sh
new file mode 100755
index 0000000..48b4434
--- /dev/null
+++ b/tests/common/email/dkim/gen_key.sh
@@ -0,0 +1,35 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p rage -p openssl -p dash -i dash --impure
+
+# shellcheck shell=dash
+
+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/common/email/dkim/mail1.server.com/private.age b/tests/common/email/dkim/mail1.server.com/private.age
new file mode 100644
index 0000000..8c5d3c3
--- /dev/null
+++ b/tests/common/email/dkim/mail1.server.com/private.age
@@ -0,0 +1,15 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOYXRiK29XallqZG1vc1Bl
+TlpFbk9WZmZIK1FrUHRIbmx6bEI5UkhJZUZ3Ck8zcHVGWHJjdG5wZnh0SDljc2F5
+WU1DMUNxdFdVQmRUMFNpa0VCSUkxZmsKLT4gWDI1NTE5IFhXREJyTVNueGp6YVg1
+b3FUemlodUMraWRMVnEyQTZqRlpKanhxdDBSMlkKYVNxcXFockpleGJBaU1iUUQ4
+bml2azlPSE9Jc0N0dDU2d1hYOXFpcnV2ZwotPiBzc2gtZWQyNTUxOSBSc2dXcFEg
+ZUVuOHZyM3pRcTZHKzFsV2VheDkrdml4NUVIWldQY1BqcGNpTG5lU2VrVQoydnVZ
+UHdpa1FmT3hNbHA1dll1dVFxS0VFanV3N3Y5Z1E5ejBiMkhRSHhRCi0+ICVuPzx1
+Ky1ncmVhc2UgYXY5PE43Om4KcWplaklGSXlMSk0yNktld21ucUdVS1dFVmt4NFhi
+VG5WYUNMUGdxZ0dta1F6RVpRRTdoek1WT0hwaC9CVEcyMworUQotLS0genJEN2Jq
+Y1JVbzc3NVFoSXAxSUxEQUZzZkRCcXNmSXBwZDB0eDMybE5NMApv3ghQF9tC00yI
+v7Sa9ZKrA8HDb/wpUn0X+D+ShLC95rW8+oPonN6gt+z+PoVUFvXwKsP/1I+D6z+B
+PRMurlvpspkIGlgi5S5H6brj9UJ7Pt61+Ld2/gaLevzCPy1QCmlAlqRKvZuZMUUR
+hWbmEXi8j56ClcxpVe62p+4GCI41T2cjAogi2C33dtviIcGedq9byD5tbVt4
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/mail1.server.com/public b/tests/common/email/dkim/mail1.server.com/public
new file mode 100644
index 0000000..4941b85
--- /dev/null
+++ b/tests/common/email/dkim/mail1.server.com/public
@@ -0,0 +1 @@
+quDd9+ogqiIUWybfegosFFkG7jAsblij2VrkuUXEzzY= \ No newline at end of file
diff --git a/tests/common/email/dkim/mail2.server.com/private.age b/tests/common/email/dkim/mail2.server.com/private.age
new file mode 100644
index 0000000..d39631a
--- /dev/null
+++ b/tests/common/email/dkim/mail2.server.com/private.age
@@ -0,0 +1,15 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCRkl1SThtVnN3bUg4cmJL
+aXBBUU4yRE1DOGdYclg1UXVhS1Y0UTJPVVIwCjlSOXZJdVB0R0VxbGdUWHd5U2RO
+RGpFdUw4c3EyUFZNZUx4Mm5yeGNIeVUKLT4gWDI1NTE5IHc5K05EeE1xNnBHd1dS
+WDJwSTdzLys3MnRncmN0WWZjTWkvM0tqeWRRU1EKek13RGRsNUhadncxQ3V6ZFcv
+Ni9aMTFtRllFK0lvd2tidnRwUjE0V0d2OAotPiBzc2gtZWQyNTUxOSBSc2dXcFEg
+Mml5VEt5b3R3d09Gc1hzR2pRZzJFY3VHT0JwUnBaTTNIWG5QaGdsWkh4bwp1Nzdx
+dWxpbjdNRGZ6aThxM0d6a2MraklkSW03ek15YXRkSXVyaS96QlFjCi0+IDAkRDEp
+bC4tZ3JlYXNlCi9lUnduUFJreTdpMEU5ejZjS1JyWmJzRkZNRVdLbnRRZC81aldN
+c2FqTTgrNnFCSEYxQUhSQWxyMW9icCtQYkgKTUEKLS0tIDNmQkwwbjJiSldraDVw
+ZUs3SHNpU0pGWXRLSVBtZ0Y1QW5uT0N5bnpjek0KiujqGmiYB3hCso3u4uCtZuO3
+EmPPJkbHKPNyUQy1V/Bv+sjgPDQi/0y7UR759G91iNIYQB0fWg7njWsl8GNdJwXI
+YO+2utDKkrm0DowWNKpHX1KE1g93e0H5ZoptygLFtx8TWDP2i8R7JJqJ9QevSz6r
+Nwjj+d3HGTxA7WvAZnNdvyfHbRRdvW+6q0uIjS5zHXAs8YmepA==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/mail2.server.com/public b/tests/common/email/dkim/mail2.server.com/public
new file mode 100644
index 0000000..5c4406d
--- /dev/null
+++ b/tests/common/email/dkim/mail2.server.com/public
@@ -0,0 +1 @@
+th9exwaYvoAjxW1tAj3k/VNLl5jKzSC/dxKrxM2mTZE= \ No newline at end of file
diff --git a/tests/common/email/hostKey b/tests/common/email/hostKey
new file mode 100644
index 0000000..79c9d6c
--- /dev/null
+++ b/tests/common/email/hostKey
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPwAAAJjFZPqHxWT6
+hwAAAAtzc2gtZWQyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPw
+AAAEA9D5AP+Uqhrg8rPx2DjgucjfnJknkk7lkeKHMV04ZZv7xdvBk/PC9fC7B5vqe9Tvyg
+ZKY6LgDQ2mXRdVrthBM/AAAAFSAnUHVibGljIHRlc3Rpbmcga2V5Jw==
+-----END OPENSSH PRIVATE KEY-----