Add mail server configuration with Stalwart Mail, secrets management, and Traefik integration
All checks were successful
Build Nix modules (dry-run) / build-modules (push) Successful in 4m0s
All checks were successful
Build Nix modules (dry-run) / build-modules (push) Successful in 4m0s
This commit is contained in:
parent
a91e60eb70
commit
fe289e0600
8 changed files with 288 additions and 1 deletions
|
|
@ -35,3 +35,7 @@ creation_rules:
|
|||
- age:
|
||||
- *admin_christine
|
||||
- *server_build_machine
|
||||
- path_regex: nix-system-configs/secrets/mail/[^/]+\.(yaml|json|env|ini)$
|
||||
key_groups:
|
||||
- age:
|
||||
- *admin_christine
|
||||
|
|
@ -16,6 +16,7 @@ configs=(
|
|||
"nixos-traefik"
|
||||
"nixos-build-machine"
|
||||
"nixos-logs"
|
||||
"nixos-mail"
|
||||
)
|
||||
|
||||
if [[ -n "$1" ]]; then
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@
|
|||
inherit system;
|
||||
modules = [./nix-system-configs/modules/system/gramethus.nix];
|
||||
};
|
||||
|
||||
"nixos-mail" = nixpkgs.lib.nixosSystem {
|
||||
inherit system;
|
||||
modules = [./nix-system-configs/modules/system/mail-server.nix];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
58
nix-system-configs/modules/secrets-config/sops-mail.nix
Normal file
58
nix-system-configs/modules/secrets-config/sops-mail.nix
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
imports = let
|
||||
# replace this with an actual commit id or tag
|
||||
commit = "17eea6f3816ba6568b8c81db8a4e6ca438b30b7c";
|
||||
in [
|
||||
"${builtins.fetchTarball {
|
||||
url = "https://github.com/Mic92/sops-nix/archive/${commit}.tar.gz";
|
||||
# replace this with an actual hash
|
||||
sha256 = "ktjWTq+D5MTXQcL9N6cDZXUf9kX8JBLLBLT0ZyOTSYY=";
|
||||
}}/modules/sops"
|
||||
];
|
||||
|
||||
# This will add secrets.yml to the nix store
|
||||
# You can avoid this by adding a string to the full path instead, i.e.
|
||||
# sops.defaultSopsFile = "/root/.sops/secrets/example.yaml";
|
||||
sops.defaultSopsFile = ../../secrets/mail/secrets.yaml;
|
||||
# This will automatically import SSH keys as age keys
|
||||
#sops.age.sshKeyPaths = ["/home/nixosdd/.ssh/id_ed25519"];
|
||||
# This is using an age key that is expected to already be in the filesystem
|
||||
sops.age.keyFile = "/var/lib/sops-nix/key.txt";
|
||||
# This will generate a new key if the key specified above does not exist
|
||||
sops.age.generateKey = true;
|
||||
|
||||
# Export individual WireGuard keys from the SOPS YAML as text secrets so they
|
||||
# are available both as strings and as files (.path)
|
||||
sops.secrets."admin-password" = {
|
||||
format = "yaml";
|
||||
sopsFile = ../../secrets/mail/secrets.yaml;
|
||||
owner = "root";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."board-member-password" = {
|
||||
format = "yaml";
|
||||
sopsFile = ../../secrets/mail/secrets.yaml;
|
||||
owner = "root";
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
sops.secrets."cloudflare-dns-token" = {
|
||||
format = "yaml";
|
||||
sopsFile = ../../secrets/mail/secrets.yaml;
|
||||
owner = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
|
||||
sops.secrets."cloudflare-username" = {
|
||||
format = "yaml";
|
||||
sopsFile = ../../secrets/mail/secrets.yaml;
|
||||
owner = "root";
|
||||
mode = "0444";
|
||||
};
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ in {
|
|||
(choose [./modules/bootloader/seabios-assigned-iso-at-birth.nix ../bootloader/seabios-assigned-iso-at-birth.nix])
|
||||
(choose [./modules/lix-default.nix ../lix-default.nix])
|
||||
(choose [./modules/secrets-config/sops-the-blank-system.nix ../secrets-config/sops-the-blank-system.nix])
|
||||
(choose [./modules/toolsets/grafana_metric.nix ../toolsets/grafana_metric.nix])
|
||||
];
|
||||
|
||||
config = {
|
||||
|
|
|
|||
135
nix-system-configs/modules/system/mail-server.nix
Normal file
135
nix-system-configs/modules/system/mail-server.nix
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
choose = paths: lib.findFirst builtins.pathExists null paths;
|
||||
in {
|
||||
options.local = {
|
||||
hostname = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nixos-default";
|
||||
description = "System hostname";
|
||||
};
|
||||
username = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "user";
|
||||
description = "Primary user username";
|
||||
};
|
||||
userDescription = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "NixOS User";
|
||||
description = "Primary user description";
|
||||
};
|
||||
address = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "10.1.1.100";
|
||||
description = "Static IP address";
|
||||
};
|
||||
};
|
||||
|
||||
imports = lib.filter (x: x != null) [
|
||||
(choose [./modules/desktop-manager/sway_greetd_homemanager.nix ../desktop-manager/sway_greetd_homemanager.nix])
|
||||
(choose [./modules/local/hostname_username.nix ../local/hostname_username.nix])
|
||||
(choose [./modules/local/networking_local.nix ../local/networking_local.nix])
|
||||
(choose [./modules/bootloader/seabios-assigned-proxmox-at-birth.nix ../bootloader/seabios-assigned-proxmox-at-birth.nix])
|
||||
(choose [./modules/lix-default.nix ../lix-default.nix])
|
||||
(choose [./modules/secrets-config/sops-the-blank-system.nix ../secrets-config/sops-the-blank-system.nix])
|
||||
(choose [./modules/toolsets/grafana_metric.nix ../toolsets/grafana_metric.nix])
|
||||
(choose [./modules/secrets-config/sops-mail.nix ../secrets-config/sops-mail.nix])
|
||||
];
|
||||
|
||||
config = {
|
||||
local.hostname = "nixos-mailserver";
|
||||
local.username = "mailprg";
|
||||
local.userDescription = "NixOS PRG Mailing Service";
|
||||
local.address = "10.1.1.15";
|
||||
system.stateVersion = "25.11";
|
||||
|
||||
services.stalwart-mail = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
settings = {
|
||||
server = {
|
||||
hostname = "mail.prg-radio.org";
|
||||
tls = {
|
||||
enable = true;
|
||||
implicit = true;
|
||||
};
|
||||
listener = {
|
||||
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";
|
||||
url = "https://mail.prg-radio.org";
|
||||
protocol = "http";
|
||||
};
|
||||
management = {
|
||||
bind = ["127.0.0.1:8080"];
|
||||
protocol = "http";
|
||||
};
|
||||
};
|
||||
};
|
||||
lookup.default = {
|
||||
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;
|
||||
};
|
||||
session.auth = {
|
||||
mechanisms = "[plain]";
|
||||
directory = "'in-memory'";
|
||||
};
|
||||
storage.directory = "in-memory";
|
||||
session.rcpt.directory = "'in-memory'";
|
||||
directory."imap".lookup.domains = ["prg-radio.org"];
|
||||
directory."in-memory" = {
|
||||
type = "memory";
|
||||
principals = [
|
||||
{
|
||||
class = "individual";
|
||||
name = "Polyteknisk Radiogruppe Board Member";
|
||||
secret = config.sops.secrets."board-member-password".path;
|
||||
email = ["board@prg-radio.org"];
|
||||
}
|
||||
{
|
||||
class = "individual";
|
||||
name = "postmaster";
|
||||
secret = config.sops.secrets."board-member-password".path;
|
||||
email = ["postmaster@prg-radio.org"];
|
||||
}
|
||||
];
|
||||
};
|
||||
authentication.fallback-admin = {
|
||||
user = "admin";
|
||||
secret = config.sops.secrets."admin-password".path;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -265,6 +265,20 @@ in {
|
|||
teamspeak-data = {
|
||||
address = "[::]:30033/tcp";
|
||||
};
|
||||
|
||||
# Mail entrypoints: plain SMTP, implicit SMTPS (465) and IMAPS (993)
|
||||
#smtp = {
|
||||
# # plain SMTP (port 25) - TCP passthrough to backend
|
||||
# address = "[::]:25";
|
||||
# };
|
||||
smtps = {
|
||||
# implicit TLS SMTP (port 465) - passthrough to backend
|
||||
address = "[::]:465";
|
||||
};
|
||||
imaps = {
|
||||
# implicit TLS IMAP (port 993) - passthrough to backend
|
||||
address = "[::]:993";
|
||||
};
|
||||
};
|
||||
log = {
|
||||
level = "INFO";
|
||||
|
|
@ -350,6 +364,14 @@ in {
|
|||
tls = {};
|
||||
middlewares = ["anubisForwardAuth"];
|
||||
};
|
||||
|
||||
# Mail HTTP (JMAP / web) - terminate TLS at Traefik and forward to Stalwart JMAP HTTP listener
|
||||
mail = {
|
||||
rule = "Host(`mail.prg-radio.org`)";
|
||||
service = "mail-jmap";
|
||||
entryPoints = ["websecure"];
|
||||
tls = {certresolver = "acme";};
|
||||
};
|
||||
};
|
||||
|
||||
http.services = {
|
||||
|
|
@ -387,6 +409,11 @@ in {
|
|||
{url = "http://10.1.1.10:3000";}
|
||||
];
|
||||
};
|
||||
|
||||
# Mail JMAP HTTP backend
|
||||
mail-jmap.loadBalancer = {
|
||||
servers = [{url = "http://10.1.1.15:8080";}];
|
||||
};
|
||||
};
|
||||
|
||||
# TCP routing for TeamSpeak
|
||||
|
|
@ -424,6 +451,30 @@ in {
|
|||
passthrough = true;
|
||||
};
|
||||
};
|
||||
|
||||
# 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-smtps = {
|
||||
# SMTPS implicit TLS - passthrough to backend
|
||||
rule = "HostSNI(`*`)";
|
||||
service = "mail-smtps";
|
||||
entryPoints = ["smtps"];
|
||||
tls = {passthrough = true;};
|
||||
};
|
||||
|
||||
mail-imaps = {
|
||||
# IMAPS implicit TLS - passthrough to backend
|
||||
rule = "HostSNI(`*`)";
|
||||
service = "mail-imaps";
|
||||
entryPoints = ["imaps"];
|
||||
tls = {passthrough = true;};
|
||||
};
|
||||
};
|
||||
|
||||
tcp.services = {
|
||||
|
|
@ -442,6 +493,19 @@ in {
|
|||
{address = "10.1.1.248:12244";}
|
||||
];
|
||||
};
|
||||
|
||||
# Mail TCP services
|
||||
# mail-smtp.loadBalancer = {
|
||||
# servers = [ { address = "10.1.1.15:25"; } ];
|
||||
# };
|
||||
|
||||
mail-smtps.loadBalancer = {
|
||||
servers = [{address = "10.1.1.15:465";}];
|
||||
};
|
||||
|
||||
mail-imaps.loadBalancer = {
|
||||
servers = [{address = "10.1.1.15:993";}];
|
||||
};
|
||||
};
|
||||
|
||||
# UDP routing for TeamSpeak voice
|
||||
|
|
|
|||
19
nix-system-configs/secrets/mail/secrets.yaml
Normal file
19
nix-system-configs/secrets/mail/secrets.yaml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
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]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age1746rvsvsc3snxfl7cndm222wd5kck4aqj3x7nednlegq0gdjhfcqx0qv7m
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBseTM3bTJtamFBVzRkOVpp
|
||||
bEZNQlJoNUVjTFd0NGl3VktmUUdHWi9wMTM0CnE3bTh5QmM1KzVqTnI3RHdmbXhN
|
||||
a0NWRkthbG9OZ3pZMEZkU3hjWUNyd1UKLS0tIE5KdXgzQmRRWTVBbDFTSEkzQW9o
|
||||
ZWE0ci9wSmhpWmc1OHZwb09aTjk1TUUKitmILkhief6sapPh3gZAEDsaHqcv3se0
|
||||
+6w+hs05ChkXHQ+JlXOTznd5ZNS4hwOAk5KOconNbauBaKWDplnHhA==
|
||||
-----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]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.11.0
|
||||
Loading…
Add table
Add a link
Reference in a new issue