mirror of
https://github.com/rubenhensen/k8scd.git
synced 2026-09-17 02:12:55 +02:00
265 lines
8.7 KiB
Nix
265 lines
8.7 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
let
|
|
# Public IP of the home network where the k8s cluster ingress lives.
|
|
# Keep in sync with dns/domains/rubenhensen.nl.yaml.
|
|
homeIP = "62.41.86.27";
|
|
|
|
# Hosts tunneled to the home k8s cluster. Each entry gets:
|
|
# * an SNI map entry for TCP passthrough on :443
|
|
# * an HTTP vhost on :80 that reverse-proxies to the cluster
|
|
# The cluster's nginx-ingress terminates TLS with cert-manager.
|
|
tunneledHosts = [
|
|
"rss.rubenhensen.nl"
|
|
"authentik.rubenhensen.nl"
|
|
"vault.rubenhensen.nl"
|
|
"ynab.rubenhensen.nl"
|
|
"argocd.rubenhensen.nl"
|
|
"ha.rubenhensen.nl"
|
|
"longhorn.rubenhensen.nl"
|
|
"lingo.rubenhensen.nl"
|
|
"blog.rubenhensen.nl"
|
|
"serpbear.rubenhensen.nl"
|
|
"git.rubenhensen.nl"
|
|
];
|
|
|
|
sniMapEntries =
|
|
lib.concatMapStringsSep "\n"
|
|
(h: " ${h} ${homeIP}:443;")
|
|
tunneledHosts;
|
|
|
|
tunneledVhosts = lib.listToAttrs (map (h: {
|
|
name = h;
|
|
value = {
|
|
listen = [
|
|
{ addr = "0.0.0.0"; port = 80; }
|
|
{ addr = "[::]"; port = 80; }
|
|
];
|
|
locations."/".proxyPass = "http://${homeIP}";
|
|
};
|
|
}) tunneledHosts);
|
|
in
|
|
{
|
|
# ──────────────────────────────────────────────
|
|
# Firewall
|
|
# ──────────────────────────────────────────────
|
|
networking.firewall.allowedTCPPorts = [
|
|
25 # SMTP
|
|
465 # SMTP submissions (implicit TLS)
|
|
587 # SMTP submission (STARTTLS)
|
|
993 # IMAP (implicit TLS)
|
|
4190 # ManageSieve
|
|
443 # HTTPS (webadmin)
|
|
80 # HTTP (ACME)
|
|
];
|
|
|
|
# ──────────────────────────────────────────────
|
|
# ACME / Let's Encrypt
|
|
# ──────────────────────────────────────────────
|
|
security.acme = {
|
|
acceptTerms = true;
|
|
defaults.email = "admin@rubenhensen.nl";
|
|
certs."mail.rubenhensen.nl" = {
|
|
group = "stalwart-mail";
|
|
reloadServices = [ "stalwart-mail" ];
|
|
webroot = "/var/lib/acme/acme-challenge";
|
|
extraDomainNames = [
|
|
"autoconfig.rubenhensen.nl"
|
|
"autodiscover.rubenhensen.nl"
|
|
"rubenhensen.nl"
|
|
];
|
|
};
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d /var/lib/acme/acme-challenge 0755 acme acme -"
|
|
];
|
|
|
|
# Serve ACME challenges via nginx on port 80.
|
|
# Also reverse-proxy tunneled hosts to the home k8s cluster, and do
|
|
# SNI-based TCP passthrough on 443 so the cluster's cert-manager keeps
|
|
# owning the TLS certificate for those hosts.
|
|
services.nginx = {
|
|
enable = true;
|
|
recommendedProxySettings = true;
|
|
|
|
# SNI passthrough on 443:
|
|
# - mail.rubenhensen.nl (and anything else) → local stalwart on 8443
|
|
# - tunneled hosts → home cluster ingress on 443
|
|
streamConfig = ''
|
|
map $ssl_preread_server_name $tunnel_upstream {
|
|
${sniMapEntries}
|
|
default 127.0.0.1:8443;
|
|
}
|
|
|
|
server {
|
|
listen 443;
|
|
listen [::]:443;
|
|
proxy_pass $tunnel_upstream;
|
|
ssl_preread on;
|
|
}
|
|
'';
|
|
|
|
# Port 80: tunneled hosts reverse-proxy to the home cluster so its
|
|
# nginx-ingress handles HTTP→HTTPS redirects and cert-manager
|
|
# HTTP-01 ACME challenges. Mail-related hosts serve ACME challenges
|
|
# locally for stalwart's cert and redirect everything else to HTTPS.
|
|
virtualHosts = tunneledVhosts // {
|
|
"mail.rubenhensen.nl" = {
|
|
listen = [
|
|
{ addr = "0.0.0.0"; port = 80; }
|
|
{ addr = "[::]"; port = 80; }
|
|
];
|
|
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
|
|
locations."/".return = "301 https://$host$request_uri";
|
|
};
|
|
"autoconfig.rubenhensen.nl" = {
|
|
listen = [
|
|
{ addr = "0.0.0.0"; port = 80; }
|
|
{ addr = "[::]"; port = 80; }
|
|
];
|
|
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
|
|
locations."/".return = "301 https://$host$request_uri";
|
|
};
|
|
"autodiscover.rubenhensen.nl" = {
|
|
listen = [
|
|
{ addr = "0.0.0.0"; port = 80; }
|
|
{ addr = "[::]"; port = 80; }
|
|
];
|
|
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
|
|
locations."/".return = "301 https://$host$request_uri";
|
|
};
|
|
"rubenhensen.nl" = {
|
|
listen = [
|
|
{ addr = "0.0.0.0"; port = 80; }
|
|
{ addr = "[::]"; port = 80; }
|
|
];
|
|
locations."/.well-known/acme-challenge/".root = "/var/lib/acme/acme-challenge";
|
|
locations."/".return = "301 https://$host$request_uri";
|
|
};
|
|
};
|
|
};
|
|
|
|
# ──────────────────────────────────────────────
|
|
# Stalwart mail server
|
|
# ──────────────────────────────────────────────
|
|
services.stalwart-mail = {
|
|
enable = true;
|
|
settings = {
|
|
server = {
|
|
hostname = "mail.rubenhensen.nl";
|
|
listener = {
|
|
smtp = {
|
|
bind = "[::]:25";
|
|
protocol = "smtp";
|
|
};
|
|
submission = {
|
|
bind = "[::]:587";
|
|
protocol = "smtp";
|
|
};
|
|
submissions = {
|
|
bind = "[::]:465";
|
|
protocol = "smtp";
|
|
tls.implicit = true;
|
|
};
|
|
imaptls = {
|
|
bind = "[::]:993";
|
|
protocol = "imap";
|
|
tls.implicit = true;
|
|
};
|
|
managesieve = {
|
|
bind = "[::]:4190";
|
|
protocol = "managesieve";
|
|
};
|
|
https = {
|
|
# nginx owns the public :443 and does SNI passthrough to here
|
|
# for the mail.rubenhensen.nl SNI. Stalwart still terminates TLS.
|
|
bind = "127.0.0.1:8443";
|
|
protocol = "http";
|
|
tls.implicit = true;
|
|
};
|
|
};
|
|
};
|
|
|
|
certificate.default = {
|
|
cert = "%{file:/var/lib/acme/mail.rubenhensen.nl/fullchain.pem}%";
|
|
private-key = "%{file:/var/lib/acme/mail.rubenhensen.nl/key.pem}%";
|
|
};
|
|
|
|
storage = {
|
|
data = "rocksdb";
|
|
fts = "rocksdb";
|
|
blob = "rocksdb";
|
|
lookup = "rocksdb";
|
|
directory = "internal";
|
|
};
|
|
|
|
store.rocksdb = {
|
|
type = "rocksdb";
|
|
path = "/var/lib/stalwart-mail/data";
|
|
compression = "lz4";
|
|
};
|
|
|
|
directory.internal = {
|
|
type = "internal";
|
|
store = "rocksdb";
|
|
};
|
|
|
|
tracer.stdout = {
|
|
type = "stdout";
|
|
level = "info";
|
|
ansi = false;
|
|
enable = true;
|
|
};
|
|
|
|
oauth.oidc = {
|
|
issuer-url = "https://authentik.rubenhensen.nl/application/o/stalwart/";
|
|
client-id = "%{file:/run/credentials/stalwart-mail.service/oidc-client-id}%";
|
|
client-secret = "%{file:/run/credentials/stalwart-mail.service/oidc-client-secret}%";
|
|
};
|
|
|
|
signature."rsa" = {
|
|
private-key = "%{file:/run/credentials/stalwart-mail.service/dkim-rsa.key}%";
|
|
domain = "rubenhensen.nl";
|
|
selector = "202603r2";
|
|
headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id"];
|
|
algorithm = "rsa-sha-256";
|
|
canonicalization = "relaxed/relaxed";
|
|
expire = "10d";
|
|
set-body-length = false;
|
|
report = true;
|
|
};
|
|
|
|
signature."ed25519" = {
|
|
private-key = "%{file:/run/credentials/stalwart-mail.service/dkim-ed25519.key}%";
|
|
domain = "rubenhensen.nl";
|
|
selector = "202603e2";
|
|
headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id"];
|
|
algorithm = "ed25519-sha256";
|
|
canonicalization = "relaxed/relaxed";
|
|
set-body-length = false;
|
|
report = false;
|
|
};
|
|
|
|
auth.dkim.sign = [
|
|
{ "if" = "listener != 'smtp'"; "then" = "['rsa', 'ed25519']"; }
|
|
{ "else" = false; }
|
|
];
|
|
|
|
authentication.fallback-admin = {
|
|
user = "admin";
|
|
secret = "%{file:/run/credentials/stalwart-mail.service/stalwart-admin-password}%";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.services.stalwart-mail.serviceConfig.LoadCredentialEncrypted = [
|
|
"stalwart-admin-password:/root/secrets/[%%secrets/stalwart-admin-password%%]"
|
|
"oidc-client-id:/root/secrets/[%%secrets/oidc-client-id%%]"
|
|
"oidc-client-secret:/root/secrets/[%%secrets/oidc-client-secret%%]"
|
|
"dkim-rsa.key:/root/secrets/[%%secrets/dkim-rsa.key%%]"
|
|
"dkim-ed25519.key:/root/secrets/[%%secrets/dkim-ed25519.key%%]"
|
|
];
|
|
|
|
users.users.stalwart-mail.extraGroups = [ "acme" ];
|
|
}
|