189 lines
6.9 KiB
Nix
189 lines
6.9 KiB
Nix
{
|
|
lib,
|
|
pkgs,
|
|
unstablePkgs,
|
|
...
|
|
}: let
|
|
# Caddy's dist dir (see tlsa-updater.nix): cert 0644, key 0640 root:root.
|
|
# The Stalwart service runs as "stalwart"; grant it read access to the key.
|
|
certDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
|
|
# sops-encrypted secrets (single file holds all service secrets), same as backup.nix.
|
|
secretsFile = ../../secrets/secrets.yaml;
|
|
# Root-only runtime file holding the fallback-admin password hash (0600 root).
|
|
adminHashFile = "/var/lib/stalwart/.admin-hash";
|
|
|
|
# Materialize the fallback-admin password hash from sops into a root-only file.
|
|
writeAdminHash = pkgs.writeShellScript "stalwart-write-admin-hash" ''
|
|
set -euo pipefail
|
|
install -d -o root -g root -m 0755 "$(dirname ${adminHashFile})"
|
|
${pkgs.sops}/bin/sops \
|
|
--decrypt --extract '["stalwart_admin_hash"]' \
|
|
--input-type yaml --output-type yaml ${secretsFile} \
|
|
| tr -d '\n' > "${adminHashFile}"
|
|
chmod 0600 "${adminHashFile}"
|
|
'';
|
|
in {
|
|
# The hetzner host is built with nixos-24.05, which ships its own
|
|
# `services.stalwart-mail` module (for the old 0.8.x package). We want the
|
|
# 0.15.5 module from the locked nixpkgs-unstable instead, so we must exclude
|
|
# the 24.05 default module (which defines the same option namespace) to avoid
|
|
# the rename-based infinite recursion, and import the unstable one in its place.
|
|
disabledModules = [
|
|
"services/mail/stalwart-mail.nix"
|
|
];
|
|
|
|
imports = [
|
|
"${unstablePkgs.path}/nixos/modules/services/mail/stalwart.nix"
|
|
];
|
|
|
|
services.stalwart = {
|
|
enable = true;
|
|
stateVersion = "26.05";
|
|
package = unstablePkgs.stalwart;
|
|
|
|
# Temporary internal listeners while docker-mailserver still owns 25/143/465/587/993.
|
|
openFirewall = false;
|
|
|
|
settings = {
|
|
# EHLO / hostname for the server (docs server.hostname).
|
|
server.hostname = "mail.severijnse.eu";
|
|
|
|
certificate."mail-severijnse-eu" = {
|
|
cert = "%{file:${certDir}/mail.severijnse.eu.crt}%";
|
|
private-key = "%{file:${certDir}/mail.severijnse.eu.key}%";
|
|
};
|
|
|
|
server.tls = {
|
|
certificate = "mail-severijnse-eu";
|
|
enable = true;
|
|
implicit = false;
|
|
};
|
|
|
|
# Temporary internal listeners (docs server/listener.md + protocol, tls.implicit override).
|
|
server.listener = {
|
|
"imap" = {
|
|
bind = ["127.0.0.1:1143"];
|
|
protocol = "imap";
|
|
};
|
|
"smtp-submission" = {
|
|
bind = ["127.0.0.1:1587"];
|
|
protocol = "smtp";
|
|
};
|
|
"smtp-submissions" = {
|
|
bind = ["127.0.0.1:1465"];
|
|
protocol = "smtp";
|
|
tls.implicit = true;
|
|
};
|
|
"http-management" = {
|
|
bind = ["127.0.0.1:8080"];
|
|
protocol = "http";
|
|
};
|
|
};
|
|
|
|
# Auth per inbound/auth.md: not required on the plain SMTP listener (port 25),
|
|
# required everywhere else (IMAP + submission). Directory is the module default "internal".
|
|
session.auth.mechanisms = "[plain]";
|
|
session.auth.directory = "'internal'";
|
|
session.auth.require = [
|
|
{
|
|
"if" = "listener != 'smtp'";
|
|
"then" = true;
|
|
}
|
|
{"else" = false;}
|
|
];
|
|
|
|
# Fallback admin (auth/authorization/administrator.md): bootstrap admin with
|
|
# every permission, used to create the internal-directory accounts via the
|
|
# management REST API / CLI. Secret is a SHA-512-crypt hash, injected via
|
|
# LoadCredential (services.stalwart.credentials) so no secret lands in the
|
|
# Nix store.
|
|
authentication."fallback-admin" = {
|
|
user = "admin";
|
|
secret = "%{file:/run/credentials/stalwart.service/stalwart-admin}%";
|
|
};
|
|
|
|
# Route docs routing: /strategy.md + /routing.md:
|
|
# local domains → local store, everything else → MX. local/mx are built-in.
|
|
queue.strategy.route = [
|
|
{
|
|
"if" = "is_local_domain('', rcpt_domain)";
|
|
"then" = "'local'";
|
|
}
|
|
{"else" = "'mx'";}
|
|
];
|
|
};
|
|
};
|
|
|
|
# The module's service runs as user/group "stalwart" (ProtectHome=true,
|
|
# ProtectSystem=strict). The TLS key tlsa-updater installs is 0640 root:root;
|
|
# regrant it to the stalwart group after every cert sync so stalwart can serve TLS.
|
|
systemd.services.stalwart = {
|
|
after = ["tlsa-update.service" "stalwart-admin-secret.service"];
|
|
requires = ["tlsa-update.service" "stalwart-admin-secret.service"];
|
|
};
|
|
|
|
# Make the management CLI available for account creation and maildir import
|
|
# (docs management/cli/). Version-pinned to the locked unstable nixpkgs.
|
|
environment.systemPackages = [unstablePkgs.stalwart-cli];
|
|
|
|
# Materialize the fallback-admin password hash from sops before stalwart starts.
|
|
# The admin hash is a SHA-512-crypt value, safe to pass through a root-only file.
|
|
systemd.services.stalwart-admin-secret = {
|
|
description = "Materialize Stalwart fallback-admin password hash from sops";
|
|
wantedBy = ["multi-user.target"];
|
|
before = ["stalwart.service"];
|
|
# The age key lives in /etc/age/keys.txt; the service must know where it is
|
|
# and needs a HOME for age to report its user config directory.
|
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
Environment = ["HOME=/root"];
|
|
ExecStart = "${writeAdminHash}";
|
|
};
|
|
};
|
|
|
|
# LoadCredential: expose the materialized hash to stalwart only at
|
|
# /run/credentials/stalwart.service/stalwart-admin (see `credentials` option
|
|
# in the upstream module; the value is the source path on disk).
|
|
services.stalwart.credentials.stalwart-admin = adminHashFile;
|
|
|
|
systemd.services.stalwart-cert-perm = {
|
|
description = "Grant stalwart read access to its TLS private key";
|
|
after = ["tlsa-update.service" "stalwart.service"];
|
|
partOf = ["tlsa-update.service"];
|
|
wantedBy = ["multi-user.target"];
|
|
path = [pkgs.coreutils];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${pkgs.coreutils}/bin/chgrp stalwart ${certDir}/mail.severijnse.eu.key";
|
|
ExecStartPost = "${pkgs.coreutils}/bin/chmod 0640 ${certDir}/mail.severijnse.eu.key";
|
|
};
|
|
};
|
|
|
|
# Bulwark webmail (self-hosted JMAP webmail for Stalwart). Serving on
|
|
# mail.severijnse.eu behind Caddy (see caddy.nix). It connects to Stalwart's
|
|
# JMAP endpoint at 127.0.0.1:8080, so uses host networking. Next.js defaults
|
|
# to POST_SIZE/etc via env; JMAP_SERVER_URL points at the Stalwart http
|
|
# listener which serves JMAP at /jmap.
|
|
virtualisation.oci-containers.containers.bulwark = {
|
|
image = "ghcr.io/bulwarkmail/webmail:latest";
|
|
autoStart = true;
|
|
volumes = [
|
|
"/var/lib/bulwark:/app/data:Z"
|
|
];
|
|
environment = {
|
|
JMAP_SERVER_URL = "http://127.0.0.1:8080";
|
|
HOSTNAME = "127.0.0.1";
|
|
PORT = "3002";
|
|
};
|
|
extraOptions = [
|
|
"--network=host"
|
|
"--label=com.centurylinklabs.watchtower.enable=true"
|
|
];
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/bulwark 0755 1001 1001 - -"
|
|
];
|
|
}
|