diff --git a/nix-system-configs/modules/system/mail-server.nix b/nix-system-configs/modules/system/mail-server.nix index fb8bcf1..58a81c3 100644 --- a/nix-system-configs/modules/system/mail-server.nix +++ b/nix-system-configs/modules/system/mail-server.nix @@ -5,6 +5,14 @@ ... }: let choose = paths: lib.findFirst builtins.pathExists null paths; + # Read secrets from sops-managed files and trim trailing newlines so the + # generated stalwart config contains the actual secret values (not the + # literal "$FOO" placeholders). Using builtins.readFile here ensures the + # values are placed into the TOML at build time. + adminPassword = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."admin-password".path); + boardPassword = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."board-member-password".path); + cloudflareUsername = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-username".path); + cloudflareToken = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-dns-token".path); in { options.local = { hostname = lib.mkOption { @@ -41,17 +49,18 @@ in { ]; config = { - # Pass secrets to Stalwart Mail service via environment variables - systemd.services.stalwart-mail = { - serviceConfig = { - Environment = [ - (let v = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-username".path); in "CLOUDFLARE_USERNAME=${v}") - (let v = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-dns-token".path); in "CLOUDFLARE_API_TOKEN=${v}") - (let v = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."admin-password".path); in "ADMIN_PASSWORD=${v}") - (let v = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."board-member-password".path); in "BOARD_PASSWORD=${v}") - ]; - }; + # Create /etc/stalwart secret files so the generated TOML can reference them + environment.etc = { + "stalwart/mail-pw1".text = boardPassword; # principal password (board) + "stalwart/mail-pw1".mode = "0400"; + "stalwart/admin-pw".text = adminPassword; # admin fallback password + "stalwart/admin-pw".mode = "0400"; + "stalwart/acme-secret".text = cloudflareToken; # API token for ACME (Cloudflare) + "stalwart/acme-secret".mode = "0400"; + "stalwart/cloudflare-username".text = cloudflareUsername; # contact email for ACME + "stalwart/cloudflare-username".mode = "0400"; }; + # Enable Tailscale for remote access to Traefik dashboard and configuration services.tailscale.enable = true; @@ -110,17 +119,18 @@ in { acme."letsencrypt" = { directory = "https://acme-v02.api.letsencrypt.org/directory"; challenge = "dns-01"; - contact = "$CLOUDFLARE_USERNAME"; + # reference the contact and secret via files under /etc/stalwart + contact = "%{file:/etc/stalwart/cloudflare-username}%"; domains = ["prg-radio.org" "mail.prg-radio.org"]; provider = "cloudflare"; - secret = "$CLOUDFLARE_API_TOKEN"; + secret = "%{file:/etc/stalwart/acme-secret}%"; }; session.auth = { - mechanisms = "[plain]"; - directory = "'in-memory'"; + mechanisms = ["plain"]; + directory = "in-memory"; }; storage.directory = "in-memory"; - session.rcpt.directory = "'in-memory'"; + session.rcpt.directory = "in-memory"; directory."imap".lookup.domains = ["prg-radio.org"]; directory."in-memory" = { type = "memory"; @@ -128,20 +138,20 @@ in { { class = "individual"; name = "Polyteknisk Radiogruppe Board Member"; - secret = "$BOARD_PASSWORD"; + secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["board@prg-radio.org"]; } { class = "individual"; name = "postmaster"; - secret = "$BOARD_PASSWORD"; + secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["postmaster@prg-radio.org"]; } ]; }; authentication.fallback-admin = { user = "admin"; - secret = "$ADMIN_PASSWORD"; + secret = "%{file:/etc/stalwart/admin-pw}%"; }; }; }; diff --git a/nix-system-configs/secrets/mail/secrets.yaml b/nix-system-configs/secrets/mail/secrets.yaml index 3cd54e0..fc3b5c2 100644 --- a/nix-system-configs/secrets/mail/secrets.yaml +++ b/nix-system-configs/secrets/mail/secrets.yaml @@ -1,5 +1,5 @@ -admin-password: ENC[AES256_GCM,data:GxV2THg8b6sa1B9kjoBpN5nPgIUIzGdCE3kUJx+Ik8mO9VGwLU//giTXrd983QDqNVc=,iv:0qbCOVT5z53gqEjFHAXLsyD+nCHGk+3Rn2Qt+ifgRJw=,tag:HDEir7oY5mpfpeyaVOUMJw==,type:str] -board-member-password: ENC[AES256_GCM,data:IHpjcweY0hQZObrrfbq67cQYJuYBS4zgcGoWcY487wqk2pPbNUOMAun9RzWXAk4X6N0=,iv:aVZArmVCSADLozsnDDhxKUizJ5CVmymKfaANcrniFvY=,tag:TGrpwrEUd5fdLyvYx8olZw==,type:str] +admin-password: ENC[AES256_GCM,data:036xX0rMzmQn0iFBYEU2u6Xa,iv:fsWHfakdNdNf8r0PrazuTbBeHmaeVqP+9l4Ip6Yx3nM=,tag:sbYB+wMOckVtBaac84zEow==,type:str] +board-member-password: ENC[AES256_GCM,data:e8jdc6FVBwB2Ieqty9s4d/aASdY=,iv:E09RhJOB84ad+L7zRXROLGD1RgDUYe/DOJCbz/zFN08=,tag:fboTZGEnrBz5pMGqwIXSTA==,type:str] cloudflare-dns-token: ENC[AES256_GCM,data:IpgU7An3IW/LFL+5OJ3oYH4c3eZjZFP+qK8/oFsNCorYKVaWPVOIVA==,iv:49UwRT8DfbC9ZIXgx7nCSxjHeIIAkiHD70ti3rWUexA=,tag:Vn9wVUhTgRlGG9E+i9NzGw==,type:str] cloudflare-username: ENC[AES256_GCM,data:AMgWBFP90f1ML/I6es0HIUoW,iv:64GGNMSdTrmurEsgdI82iTqD9FUW8leKu/JvT7Ls6Og=,tag:3rTWNF3NfTExCnV+RRwyVA==,type:str] sops: @@ -22,7 +22,7 @@ sops: OFE3aWxZNThlWUUrUWlwZmtGYjJGT2sKFkoNZt6ThwzwQ2MMFjncrVrLKEhJ1hxh uJuOfYFlQI80k3etChD64mTRMSK7Cr/BIc2625+jGJK4kOc+JpFDEQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-15T17:16:50Z" - mac: ENC[AES256_GCM,data:4FZe4zCBQY01TcsX8yU5cMz8C1C64L80QJFhhdC+3xxS0URw/QkpyevUnkT7gzmuHjwBbrdY/NpNTyNfzacxpw2dVt4MhvdDcKWqXV802DmBpaZcvfFlsjpSBIhXiudu428tQOwWgY3WmQfmg2wh46fRM8+QoZyaxOaX188Pu9U=,iv:EUuzxfz3cGK2yf555PQNpRCzOvlSVKLihBkUxNp8JjQ=,tag:L92aHzdQAY5K0QxkFpFvOQ==,type:str] + lastmodified: "2026-02-15T17:53:57Z" + mac: ENC[AES256_GCM,data:ZdDyFxbaUq/FPSe6z3+FhRJ6GeG4YjGc0fIVof/Sf/gsfKwZAStZs3CKxZtJXxUGBtkw93aguYjnNfxjUH+YxTemZPhWfJZo0pcR9YwWzXvB9fRLM95v/TzWY9qzqqbqVUSz+2SSfUJJG2+dKuzEzA3c1lE1pwOFXdfWOm0+uCE=,iv:mQp+yIVMDBY3tAPlDShk3IlIJ3cb4zrTn18x4HY+YsI=,tag:QLq34VwtxN5t66aE31LcYA==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0