Compare commits

...

18 commits

Author SHA1 Message Date
cab31335ff
Remove dangling aliases. 2026-02-15 23:47:58 +01:00
84d2ce3d2f
Remove proxy.trusted-networks configuration from SMTP, SMTPS, and IMAPS services 2026-02-15 23:41:41 +01:00
acdd65a5fb
Renable certificates. 2026-02-15 23:32:23 +01:00
1bc85803a9
Diagnose certificate issues, add missing ServerAliases 2026-02-15 23:16:59 +01:00
ba7e32a2a7
Try to investiagte the header issue. 2026-02-15 22:18:51 +01:00
c884036b7a
Forgor... again... added SMTP properly. 2026-02-15 21:08:27 +01:00
e389ebb672
Forgor... again. 2026-02-15 21:04:33 +01:00
6f536f306f
Enable SMTP entrypoint... spooky. 2026-02-15 20:57:51 +01:00
8c6571727b
I forgot the bloody ports! 2026-02-15 20:17:02 +01:00
a3249aa479
Expose mail web admin UI via Traefik under /management and update file permissions 2026-02-15 20:06:23 +01:00
a8c4486c20
Typo... 2026-02-15 19:05:05 +01:00
64d59dd636
Fix the file permissions. 2026-02-15 19:03:14 +01:00
8963c8d048
Revamp password, change password... 2026-02-15 18:54:16 +01:00
5289fc4a20
Update password management... again. 2026-02-15 18:17:03 +01:00
e7d4f3d4a1
Update password management. 2026-02-15 17:40:37 +01:00
70f6cc7930
Update mail server secrets references to use direct secret values instead of file paths 2026-02-15 17:24:21 +01:00
1565b21d8b
Update keys, add Tailscale to the mailserver 2026-02-15 16:34:15 +01:00
4618c3f7fb
Add mail server to Grafana logs. 2026-02-15 16:20:58 +01:00
6 changed files with 130 additions and 48 deletions

View file

@ -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:
@ -39,3 +40,4 @@ creation_rules:
key_groups:
- age:
- *admin_christine
- *server_mail

View file

@ -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
# 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"
#```

View file

@ -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;
}
];
};
};

View file

@ -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}%";
};
};
};

View file

@ -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";
};

View file

@ -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