diff --git a/.sops.yaml b/.sops.yaml index fc66233..a519d2d 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -5,6 +5,7 @@ keys: - &server_database age1k9ddvzypz986a7dt403ja6evql2agz0gehll79mx64zceteya38smxph8m - &server_build_machine age1xtsm7kmql8794756ls53eu8pusyadk4lauadxx867tgsjhd38y5sj7elca - &server_christine age1kzsrlz86y5nqe4vaufv8chupq2hrf3avlmxsp3d5p9t5xj593deszslkmp + - &server_mail age1pkn38zzyckxqgeud7s7y3uuhd7pau3csh56rn3ue4altzkressdsaxs4et creation_rules: - path_regex: nix-system-configs/secrets/songsheet/[^/]+\.(yaml|json|env|ini)$ key_groups: @@ -38,4 +39,5 @@ creation_rules: - path_regex: nix-system-configs/secrets/mail/[^/]+\.(yaml|json|env|ini)$ key_groups: - age: - - *admin_christine \ No newline at end of file + - *admin_christine + - *server_mail \ No newline at end of file diff --git a/nix-system-configs/modules/scripts/compose_two_nix.zsh b/nix-system-configs/modules/scripts/compose_two_nix.zsh index b0e71b0..1e01a26 100755 --- a/nix-system-configs/modules/scripts/compose_two_nix.zsh +++ b/nix-system-configs/modules/scripts/compose_two_nix.zsh @@ -1,3 +1,11 @@ ## Todo, make a proper script for this EDITOR=micro nix-shell -p sops --run "SOPS_AGE_KEY_FILE=$HOME/.config/sops/age/keys.txt sops nix-system-configs/secrets/songsheet/secrets.yaml" -compose2nix --inputs nix-system-configs/modules/songsheet/wavelog/docker-compose.yml --sops_file nix-system-configs/secrets/songsheet/secrets.yaml --project=wavelog --output nix-system-configs/modules/songsheet/wavelog/docker-compose.nix --root_path nix-system-configs/modules/songsheet/wavelog \ No newline at end of file +compose2nix --inputs nix-system-configs/modules/songsheet/wavelog/docker-compose.yml --sops_file nix-system-configs/secrets/songsheet/secrets.yaml --project=wavelog --output nix-system-configs/modules/songsheet/wavelog/docker-compose.nix --root_path nix-system-configs/modules/songsheet/wavelog + + +# Todo make a proper script for this: +NIXOS_CONFIG="/home/nixosbm/the_prg_server_configuration/nix-system-configs/modules/system/mail-server.nix" nixos-rebuild build-image --image-variant proxmox +#Mind the ``` +# - In `/home/nixosbm/the_prg_server_configuration/nix-system-configs/modules/bootloader/seabios-assigned-proxmox-at-birth.nix': "/dev/vda1" +# - In `/nix/var/nix/profiles/per-user/root/channels/nixos/nixos/modules/virtualisation/proxmox-image.nix': "/dev/disk/by-label/nixos" +#``` \ No newline at end of file diff --git a/nix-system-configs/modules/system/gramethus.nix b/nix-system-configs/modules/system/gramethus.nix index d26361f..0f3a636 100644 --- a/nix-system-configs/modules/system/gramethus.nix +++ b/nix-system-configs/modules/system/gramethus.nix @@ -130,6 +130,22 @@ in { isDefault = false; editable = true; } + + { + name = "Prometheus - Mail Server"; + type = "prometheus"; + url = "http://10.1.1.15:9001"; + isDefault = false; + editable = true; + } + + { + name = "Loki - Mail Server"; + type = "loki"; + url = "http://10.1.1.15:3100"; + isDefault = false; + editable = true; + } ]; }; }; diff --git a/nix-system-configs/modules/system/mail-server.nix b/nix-system-configs/modules/system/mail-server.nix index 257f6a7..0b18ed0 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,6 +49,26 @@ in { ]; config = { + # 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 = "0777"; + "stalwart/admin-pw".text = adminPassword; # admin fallback password + "stalwart/admin-pw".mode = "0777"; + "stalwart/acme-secret".text = cloudflareToken; # API token for ACME (Cloudflare) + "stalwart/acme-secret".mode = "0777"; + "stalwart/cloudflare-username".text = cloudflareUsername; # contact email for ACME + "stalwart/cloudflare-username".mode = "0777"; + }; + + systemd.tmpfiles.rules = [ + # z = create/modify file or directory, set mode and owner + "z /etc/stalwart 0555 root root - -" + ]; + + # Enable Tailscale for remote access to Traefik dashboard and configuration + services.tailscale.enable = true; + local.hostname = "nixos-mailserver"; local.username = "mailprg"; local.userDescription = "NixOS PRG Mailing Service"; @@ -61,22 +89,16 @@ in { smtp = { protocol = "smtp"; bind = "[::]:25"; - proxy.trusted-networks = [ - "10.1.1.250/32" - ]; }; submissions = { bind = "[::]:465"; protocol = "smtp"; tls.implicit = true; - # Also trust proxy for SMTPS - proxy.trusted-networks = ["10.1.1.250/32"]; }; imaps = { bind = "[::]:993"; protocol = "imap"; tls.implicit = true; - proxy.trusted-networks = ["10.1.1.250/32"]; }; jmap = { bind = "[::]:8080"; @@ -84,7 +106,7 @@ in { protocol = "http"; }; management = { - bind = ["127.0.0.1:8080"]; + bind = ["127.0.0.1:8080" "[::]:8081"]; protocol = "http"; }; }; @@ -93,20 +115,21 @@ in { hostname = "mail.prg-radio.org"; domain = "prg-radio.org"; }; - acme."letsencrypt" = { - directory = "https://acme-v02.api.letsencrypt.org/directory"; - challenge = "dns-01"; - contact = config.sops.secrets."cloudflare-username".path; - domains = ["prg-radio.org" "mail.prg-radio.org"]; - provider = "cloudflare"; - secret = config.sops.secrets."cloudflare-dns-token".path; + acme."letsencrypt" = { + directory = "https://acme-v02.api.letsencrypt.org/directory"; + challenge = "dns-01"; + # 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 = "%{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"; @@ -114,20 +137,20 @@ in { { class = "individual"; name = "Polyteknisk Radiogruppe Board Member"; - secret = config.sops.secrets."board-member-password".path; + secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["board@prg-radio.org"]; } { class = "individual"; name = "postmaster"; - secret = config.sops.secrets."board-member-password".path; + secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["postmaster@prg-radio.org"]; } ]; }; authentication.fallback-admin = { user = "admin"; - secret = config.sops.secrets."admin-password".path; + secret = "%{file:/etc/stalwart/admin-pw}%"; }; }; }; diff --git a/nix-system-configs/modules/system/traefik.nix b/nix-system-configs/modules/system/traefik.nix index 7275a45..5c20f91 100644 --- a/nix-system-configs/modules/system/traefik.nix +++ b/nix-system-configs/modules/system/traefik.nix @@ -267,10 +267,10 @@ in { }; # Mail entrypoints: plain SMTP, implicit SMTPS (465) and IMAPS (993) - #smtp = { - # # plain SMTP (port 25) - TCP passthrough to backend - # address = "[::]:25"; - # }; + smtp = { + # plain SMTP (port 25) - TCP passthrough to backend + address = "[::]:25"; + }; smtps = { # implicit TLS SMTP (port 465) - passthrough to backend address = "[::]:465"; @@ -309,6 +309,10 @@ in { scheme = "https"; }; }; + # Middleware to strip the /management prefix when proxying to the mail admin + mail-strip-management = { + stripPrefix = {prefixes = ["/management"];}; + }; }; http.routers = { @@ -372,6 +376,16 @@ in { entryPoints = ["websecure"]; tls = {certresolver = "acme";}; }; + + # Mail web administration UI (Stalwart management) - exposed under /management + mail-webadmin = { + rule = "Host(`mail.prg-radio.org`) && PathPrefix(`/management`)"; + service = "mail-webadmin"; + entryPoints = ["websecure"]; + tls = {certresolver = "acme";}; + # strip the /management prefix before proxying to the backend + middlewares = ["mail-strip-management"]; + }; }; http.services = { @@ -414,6 +428,13 @@ in { mail-jmap.loadBalancer = { servers = [{url = "http://10.1.1.15:8080";}]; }; + + # Mail webadmin backend (management). Traefik will strip the /management + # prefix and forward the remainder to the management listener on the mail + # server which is bound to port 8081. + mail-webadmin.loadBalancer = { + servers = [{url = "http://10.1.1.15:8081";}]; + }; }; # TCP routing for TeamSpeak @@ -453,12 +474,12 @@ in { }; # Mail TCP routers: SMTP (25), SMTPS (465 implicit TLS), IMAPS (993 implicit TLS) - #mail-smtp = { - # # catch-all TCP router for port 25 - # rule = "HostSNI(`*`)"; - # service = "mail-smtp"; - # entryPoints = ["smtp"]; - # }; + mail-smtp = { + # catch-all TCP router for port 25 + rule = "HostSNI(`*`)"; + service = "mail-smtp"; + entryPoints = ["smtp"]; + }; mail-smtps = { # SMTPS implicit TLS - passthrough to backend @@ -495,15 +516,18 @@ in { }; # Mail TCP services - # mail-smtp.loadBalancer = { - # servers = [ { address = "10.1.1.15:25"; } ]; - # }; + mail-smtp.loadBalancer = { + proxyProtocol = {version = 2;}; # Add this line + servers = [{address = "10.1.1.15:25";}]; + }; mail-smtps.loadBalancer = { + proxyProtocol = {version = 2;}; # Add this line servers = [{address = "10.1.1.15:465";}]; }; mail-imaps.loadBalancer = { + proxyProtocol = {version = 2;}; # Add this line servers = [{address = "10.1.1.15:993";}]; }; }; @@ -546,8 +570,8 @@ in { wants = ["acme-finished-prg-radio.org.target"]; }; - networking.firewall.allowedTCPPorts = [80 443 10022 30033 8448 9001 3001]; - networking.firewall.allowedUDPPorts = [80 443 9987 9001 3001]; + networking.firewall.allowedTCPPorts = [80 443 10022 30033 8448 9001 3001 993 465 25]; + networking.firewall.allowedUDPPorts = [80 443 9987 9001 3001 993 465 25]; system.stateVersion = "25.11"; }; diff --git a/nix-system-configs/secrets/mail/secrets.yaml b/nix-system-configs/secrets/mail/secrets.yaml index a085774..fc3b5c2 100644 --- a/nix-system-configs/secrets/mail/secrets.yaml +++ b/nix-system-configs/secrets/mail/secrets.yaml @@ -1,19 +1,28 @@ -admin-password: ENC[AES256_GCM,data:o8nD9CDNzufxVjfH7TqUY4QO6Xkz1xbpFu74jMVRR57NDKpKsg3ijRmBvvq1/QuJ2TM=,iv:xVGjzrRFpCyjriW6yB/UjuGiNhK4oeBiDA+Pk/BTOWc=,tag:H/ZuqffTDGsGTWTTKplvvA==,type:str] -board-member-password: ENC[AES256_GCM,data:8KLyqyGFi/ehvbNU6HsWilMO42UnXYL6puc/OOPgMDiU2fuDM2o7TibUCn1BXi9UyiE=,iv:UyLOFSARK/5p/zin7vqQic9C5qlryd/O/mjcw3eZ3ao=,tag:N2RJC6nZC+V3XoAw/eWYfA==,type:str] -cloudflare-dns-token: ENC[AES256_GCM,data:W0S9WWmhQMwjKbKI60vBjYlSb0chZUYcVs3A4DsVRfJwErcIqDWoJg==,iv:nRH8hLvCavydVC16M93N+r/t9WwNY4339CE4zGSINsk=,tag:yIUKs/XB3arhApGeik6p1A==,type:str] -cloudflare-username: ENC[AES256_GCM,data:5QCWOZcv6RKlk1/5LWjjCW0m,iv:9VzhRDlLq36pMfv0tu7POxheLFKdXP8tLLiKlTOr/30=,tag:MiijON1+mKJj6VMdnaJTkA==,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: age: - recipient: age1746rvsvsc3snxfl7cndm222wd5kck4aqj3x7nednlegq0gdjhfcqx0qv7m enc: | -----BEGIN AGE ENCRYPTED FILE----- - YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBseTM3bTJtamFBVzRkOVpp - bEZNQlJoNUVjTFd0NGl3VktmUUdHWi9wMTM0CnE3bTh5QmM1KzVqTnI3RHdmbXhN - a0NWRkthbG9OZ3pZMEZkU3hjWUNyd1UKLS0tIE5KdXgzQmRRWTVBbDFTSEkzQW9o - ZWE0ci9wSmhpWmc1OHZwb09aTjk1TUUKitmILkhief6sapPh3gZAEDsaHqcv3se0 - +6w+hs05ChkXHQ+JlXOTznd5ZNS4hwOAk5KOconNbauBaKWDplnHhA== + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYRFU5S1EvUmFTeFoxQ0dZ + Ymx5Tm92U2NtS2JsaGlzS3Y2Z3dsRGpkOW5BCkxCRXJWMjBkTjdiejg0bGpRcXg0 + UzBzSHUwajRSM2s4bjFWRGNCVDVXclEKLS0tICtxYnZJQm9KcCs1eURCYTUyRHYx + SFQxT0hyZ3A5d1BPbEhWUVJJNFVFM1EKI7Srn4G6r0mKIfPd+VsCCAQGlshirxLS + cvMHhG3WqMV/esuHfi/0xpQuKoCQpBcS2sW/vtJk5Au+uV3dVfv/jg== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-15T14:13:19Z" - mac: ENC[AES256_GCM,data:7hFjuvictSbcXLqXwG0VgWErKJpFsy1PfDyepQQXpszpMT4Z/BwvXlk4ppKo8C0PaCLv2qi86yBmFm/O6xUBhsMEFWYHQ+mJpYtqLX0GDvj1cn4LwDEnRa+2SiHkkZeHSwrtOHCBw8vE2R2sXBaNMkUoSXkcQ4lPS6YjulpO1vw=,iv:5aZxEnPymcvNpsUyGvvRI3o7hnfExSFWlBrzoIhQkFQ=,tag:6JJGvBL9XeiPw+TdN2qEgA==,type:str] + - recipient: age1pkn38zzyckxqgeud7s7y3uuhd7pau3csh56rn3ue4altzkressdsaxs4et + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvOFYyWGYrUWNDdzBkajlh + RVdENTloUDcvVnhPeHgvcDIzRnNIWUNnS21BCmFhMlVSSk1kNmpUek0yZDh3cGcx + WHltUWVqREhZdlJ2bWJtczJjTXZ3dHcKLS0tIDRweTY1RVdVZVQ1ZlNsc3FwdVdL + OFE3aWxZNThlWUUrUWlwZmtGYjJGT2sKFkoNZt6ThwzwQ2MMFjncrVrLKEhJ1hxh + uJuOfYFlQI80k3etChD64mTRMSK7Cr/BIc2625+jGJK4kOc+JpFDEQ== + -----END AGE ENCRYPTED FILE----- + 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