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
|
|
@ -34,4 +34,8 @@ creation_rules:
|
||||||
key_groups:
|
key_groups:
|
||||||
- age:
|
- age:
|
||||||
- *admin_christine
|
- *admin_christine
|
||||||
- *server_build_machine
|
- *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-traefik"
|
||||||
"nixos-build-machine"
|
"nixos-build-machine"
|
||||||
"nixos-logs"
|
"nixos-logs"
|
||||||
|
"nixos-mail"
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ -n "$1" ]]; then
|
if [[ -n "$1" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,11 @@
|
||||||
inherit system;
|
inherit system;
|
||||||
modules = [./nix-system-configs/modules/system/gramethus.nix];
|
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/bootloader/seabios-assigned-iso-at-birth.nix ../bootloader/seabios-assigned-iso-at-birth.nix])
|
||||||
(choose [./modules/lix-default.nix ../lix-default.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/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 = {
|
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 = {
|
teamspeak-data = {
|
||||||
address = "[::]:30033/tcp";
|
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 = {
|
log = {
|
||||||
level = "INFO";
|
level = "INFO";
|
||||||
|
|
@ -350,6 +364,14 @@ in {
|
||||||
tls = {};
|
tls = {};
|
||||||
middlewares = ["anubisForwardAuth"];
|
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 = {
|
http.services = {
|
||||||
|
|
@ -387,6 +409,11 @@ in {
|
||||||
{url = "http://10.1.1.10:3000";}
|
{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
|
# TCP routing for TeamSpeak
|
||||||
|
|
@ -424,6 +451,30 @@ in {
|
||||||
passthrough = true;
|
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 = {
|
tcp.services = {
|
||||||
|
|
@ -442,6 +493,19 @@ in {
|
||||||
{address = "10.1.1.248:12244";}
|
{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
|
# 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