Working secrets with nix

This commit is contained in:
Ruben Hensen
2026-03-17 21:00:21 +01:00
parent 0ed06f311c
commit d962b158e6
+34 -220
View File
@@ -1,83 +1,19 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let
k8sUpstreamConf = "/run/nginx/k8s-upstream.conf";
k8sHttpUpstreamConf = "/run/nginx/k8s-http-upstream.conf";
updateK8sIp = pkgs.writeShellScriptBin "update-k8s-ip" ''
NEW_IP="$SSH_ORIGINAL_COMMAND"
if ! echo "$NEW_IP" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "Invalid IP: $NEW_IP"
exit 1
fi
CONF="${k8sUpstreamConf}"
CURRENT_IP=$(${pkgs.gnugrep}/bin/grep -oP 'server \K[0-9.]+' "$CONF" 2>/dev/null | head -1)
if [ "$CURRENT_IP" = "$NEW_IP" ]; then
echo "IP unchanged: $NEW_IP"
exit 0
fi
printf 'upstream k8s_tls {\n server %s:443;\n}\n\nupstream k8s_ldap {\n server %s:3389;\n}\n' "$NEW_IP" "$NEW_IP" > "$CONF"
printf 'upstream k8s_http {\n server %s:80;\n}\n' "$NEW_IP" > ${k8sHttpUpstreamConf}
${pkgs.nginx}/bin/nginx -t && ${pkgs.systemd}/bin/systemctl reload nginx
echo "Updated K8s backend IP to $NEW_IP"
'';
in
{ {
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
# Firewall (replaces UFW) # Firewall
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [
25 # SMTP 25 # SMTP
465 # SMTP submissions (implicit TLS) 465 # SMTP submissions (implicit TLS)
587 # SMTP submission (STARTTLS) 587 # SMTP submission (STARTTLS)
993 # IMAP (implicit TLS) 993 # IMAP (implicit TLS)
443 # HTTPS 443 # HTTPS (webadmin)
80 # HTTP (ACME + redirect) 80 # HTTP (ACME)
]; ];
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
# Sysctl hardening (replaces base role) # ACME / Let's Encrypt
# ──────────────────────────────────────────────
boot.kernel.sysctl = {
"net.ipv4.conf.all.rp_filter" = 1;
"net.ipv4.conf.default.rp_filter" = 1;
"net.ipv4.conf.all.accept_redirects" = 0;
"net.ipv4.conf.default.accept_redirects" = 0;
"net.ipv4.conf.all.send_redirects" = 0;
"net.ipv4.conf.default.send_redirects" = 0;
"net.ipv4.tcp_syncookies" = 1;
"net.ipv4.icmp_echo_ignore_broadcasts" = 1;
"net.ipv6.conf.all.accept_redirects" = 0;
"net.ipv6.conf.default.accept_redirects" = 0;
};
# ──────────────────────────────────────────────
# Fail2ban
# ──────────────────────────────────────────────
services.fail2ban = {
enable = true;
maxretry = 5;
bantime = "1h";
jails = {
sshd = {
settings = {
enabled = true;
port = "ssh";
maxretry = 3;
bantime = "1h";
findtime = "10m";
};
};
};
};
# ──────────────────────────────────────────────
# ACME / Let's Encrypt (replaces certbot)
# ────────────────────────────────────────────── # ──────────────────────────────────────────────
security.acme = { security.acme = {
acceptTerms = true; acceptTerms = true;
@@ -89,6 +25,26 @@ in
}; };
}; };
systemd.tmpfiles.rules = [
"d /var/lib/acme/acme-challenge 0755 acme acme -"
];
# Serve ACME challenges via nginx on port 80
services.nginx = {
enable = true;
virtualHosts."stalwart.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 # Stalwart mail server
@@ -98,7 +54,6 @@ in
settings = { settings = {
server = { server = {
hostname = "stalwart.rubenhensen.nl"; hostname = "stalwart.rubenhensen.nl";
max-connections = 8192;
listener = { listener = {
smtp = { smtp = {
bind = "[::]:25"; bind = "[::]:25";
@@ -119,14 +74,10 @@ in
tls.implicit = true; tls.implicit = true;
}; };
https = { https = {
bind = "127.0.0.1:8443"; bind = "[::]:443";
protocol = "http"; protocol = "http";
tls.implicit = true; tls.implicit = true;
}; };
http = {
bind = "127.0.0.1:8080";
protocol = "http";
};
}; };
}; };
@@ -140,7 +91,7 @@ in
fts = "rocksdb"; fts = "rocksdb";
blob = "rocksdb"; blob = "rocksdb";
lookup = "rocksdb"; lookup = "rocksdb";
directory = "ldap"; directory = "internal";
}; };
store.rocksdb = { store.rocksdb = {
@@ -149,22 +100,9 @@ in
compression = "lz4"; compression = "lz4";
}; };
directory.ldap = { directory.internal = {
type = "ldap"; type = "internal";
url = "ldap://127.0.0.1:3389"; store = "rocksdb";
base-dn = "DC=ldap,DC=goauthentik,DC=io";
bind.dn = "cn=ldapservice,ou=users,DC=ldap,DC=goauthentik,DC=io";
bind.secret = "%{file:/run/secrets/stalwart-ldap-password}%";
filter.name = "(&(objectClass=user)(cn=?))";
filter.email = "(&(objectClass=user)(mail=?))";
filter.verify = "(&(objectClass=user)(|(mail=*?*)(cn=*?*)))";
filter.expand = "(&(objectClass=group)(cn=?))";
attribute.name = "cn";
attribute.type = "class";
attribute.email = "mail";
attribute.secret = "userPassword";
attribute.groups = "memberOf";
attribute.description = "displayName";
}; };
tracer.stdout = { tracer.stdout = {
@@ -174,140 +112,16 @@ in
enable = true; enable = true;
}; };
tracer.log = {
type = "log";
level = "info";
path = "/var/lib/stalwart-mail/logs";
prefix = "stalwart.log";
rotate = "daily";
ansi = false;
enable = true;
};
authentication.fallback-admin = { authentication.fallback-admin = {
user = "admin"; user = "admin";
secret = "%{file:/run/secrets/stalwart-admin-password}%"; secret = "%{file:/run/credentials/stalwart-mail.service/stalwart-admin-password}%";
}; };
}; };
}; };
# Grant stalwart and nginx access to ACME certs systemd.services.stalwart-mail.serviceConfig.LoadCredentialEncrypted = [
"stalwart-admin-password:/root/secrets/[%%secrets/stalwart-admin-password%%]"
];
users.users.stalwart-mail.extraGroups = [ "acme" ]; users.users.stalwart-mail.extraGroups = [ "acme" ];
users.users.nginx.extraGroups = [ "stalwart-mail" ];
# ──────────────────────────────────────────────
# Nginx (reverse proxy + stream proxy to K8s)
# ──────────────────────────────────────────────
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedProxySettings = true;
eventsConfig = "worker_connections 4096;";
# HTTP upstream for K8s (included from mutable file)
appendHttpConfig = ''
include /run/nginx/k8s-http-upstream.conf;
'';
# Stream config for TLS SNI routing + LDAP proxy
streamConfig = ''
log_format stream '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$ssl_preread_server_name"';
access_log /var/log/nginx/stream.log stream;
map $ssl_preread_server_name $tls_backend {
stalwart.rubenhensen.nl local_tls;
default k8s_tls;
}
upstream local_tls {
server 127.0.0.1:8443;
}
include /run/nginx/k8s-upstream.conf;
server {
listen 443;
listen [::]:443;
ssl_preread on;
proxy_pass $tls_backend;
}
# LDAP proxy to K8s Authentik LDAP outpost
server {
listen 127.0.0.1:3389;
proxy_pass k8s_ldap;
}
'';
};
# Create stream.d directory and initial upstream config
systemd.tmpfiles.rules = [
"d /run/secrets 0700 root root -"
"d /var/lib/acme/acme-challenge 0755 acme acme -"
];
# ──────────────────────────────────────────────
# K8s IP update script (called via SSH)
# ──────────────────────────────────────────────
# Allow nginx to read/write mutable upstream configs
systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/run/nginx" ];
systemd.services.nginx.serviceConfig.LimitNOFILE = 65536;
systemd.services.nginx.preStart = lib.mkBefore ''
mkdir -p /run/nginx
test -f /run/nginx/k8s-upstream.conf || printf 'upstream k8s_tls {\n server 127.0.0.1:1;\n}\n\nupstream k8s_ldap {\n server 127.0.0.1:1;\n}\n' > /run/nginx/k8s-upstream.conf
test -f /run/nginx/k8s-http-upstream.conf || printf 'upstream k8s_http {\n server 127.0.0.1:80;\n}\n' > /run/nginx/k8s-http-upstream.conf
'';
# Stalwart ACME HTTP-01 challenge
services.nginx.virtualHosts."stalwart.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";
};
};
# Catch-all port 80 — proxy to K8s for ACME challenges + redirect
services.nginx.virtualHosts."_" = {
default = true;
listen = [
{ addr = "0.0.0.0"; port = 80; }
{ addr = "[::]"; port = 80; }
];
locations."/" = {
proxyPass = "http://k8s_http";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
'';
};
};
# IP update script (as a proper Nix package)
environment.systemPackages = [ updateK8sIp pkgs.openssl ];
# SSH authorized key for K8s IP updater (add the actual pubkey)
users.users.root.openssh.authorizedKeys.keys = [
# nix-infra will set the main SSH key via configuration.nix
# Add the IP updater key with command restriction:
''command="${updateK8sIp}/bin/update-k8s-ip",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII5cMc73rlUCn3mS5FXlu3nO+AUeW2L28jRh22VYIPY4 k8s-ip-updater''
];
# ──────────────────────────────────────────────
# Automatic updates
# ──────────────────────────────────────────────
system.autoUpgrade = {
enable = true;
allowReboot = false;
};
} }