Added hetzner server configs
CI / Formatting check (x86_64-linux) (push) Has been cancelled
CI / Flake check (aarch64-linux) (push) Has been cancelled
CI / Flake check (x86_64-linux) (push) Has been cancelled
CI / Formatting check (aarch64-linux) (push) Has been cancelled

This commit is contained in:
2026-07-11 20:28:51 +02:00
parent 0457d618e1
commit c5f771bd53
51 changed files with 1800 additions and 309 deletions
@@ -0,0 +1,46 @@
{
config,
pkgs,
lib,
...
}: let
backupScript = pkgs.writeShellScript "weekly-backup" ''
BACKUP_DIR="/home/admin/backups"
SRC="/home/admin"
DATE=$(date +%Y-%m-%dT%H-%M-%S)
FILENAME="weekly-backup-$DATE.tar.gz"
mkdir -p "$BACKUP_DIR"
# Backup everything under /home/admin EXCEPT:
# - The backups dir itself (infinite loop)
# - DMS mail data (GBs of email, backed up separately)
# - NixOS-managed service data (at their own paths below)
tar czf "$BACKUP_DIR/$FILENAME" \
--exclude="$BACKUP_DIR" \
--exclude="/home/admin/backups" \
--exclude="/home/admin/dms/mail-data" \
--exclude="/home/admin/dms/mail-state" \
"$SRC"
# Prune backups older than 14 days
find "$BACKUP_DIR" -name "weekly-backup-*" -mtime +14 -delete
'';
in {
systemd.services.weekly-backup = {
description = "Weekly backup of home directory";
path = with pkgs; [coreutils gnutar findutils];
serviceConfig = {
Type = "oneshot";
ExecStart = "${backupScript}";
User = "root";
};
};
systemd.timers.weekly-backup = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = "Mon *-*-* 03:00:00";
Persistent = true;
};
};
}
+248
View File
@@ -0,0 +1,248 @@
{
config,
pkgs,
lib,
...
}: let
domain = "severijnse.eu";
antiScrape = ''
@bad_bot {
header_regexp User-Agent "(?i)(scrapy|cpython-requests|python-requests|curl|wget|go-http-client|ltx71|petalbot|bytespider|dotbot|ahrefsbot|semrushbot|mj12bot|dataforseo|facebookexternalhit|claudebot|anthropic-ai|perplexity|gptbot|chatgpt-user|omnisci|imgproxy|ccbot|exabot|360spider|baiduspider|sogou|duckduckgo|amazonbot|cohere-ai|diffbot|imagesiftbot).*"
}
respond @bad_bot "" 444
header {
X-Robots-Tag "noindex, nofollow, noai, noimageai"
}
'';
in {
services.caddy = {
enable = true;
group = "caddy";
dataDir = "/var/lib/caddy";
logDir = "/var/log/caddy";
globalConfig = ''
email jory@severijnse.eu
servers {
trusted_proxies static private_ranges
}
'';
# Global Caddyfile snippets (shared across all virtual hosts).
extraConfig = ''
(admin_gate) {
@notvpn not remote_ip 10.8.0.0/24
respond @notvpn "Forbidden" 403
}
# Security headers applied to every response of every site that
# imports this snippet (covers all current and future hosts).
# For proxied hosts whose upstream sets its own copies, strip them
# with `header_down` inside the reverse_proxy block instead.
(security_headers) {
header {
X-Content-Type-Options "nosniff"
X-XSS-Protection "0"
X-Frame-Options "SAMEORIGIN"
}
}
(csp) {
header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; connect-src 'self' wss: ws:"
}
'';
virtualHosts = {
"severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
@logo path /logo.svg
handle @logo {
root * /srv
file_server
header Content-Type image/svg+xml
header Cache-Control "public, immutable, max-age=31536000"
header X-Content-Type-Options nosniff
}
handle {
redir https://jory.severijnse.eu{uri} permanent
}
'';
};
"www.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
redir https://jory.severijnse.eu{uri} permanent
'';
};
"jory.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
root * /srv/jory
file_server
try_files {path} /index.html
encode zstd gzip
header Strict-Transport-Security "max-age=31536000;"
@robots path /robots.txt
handle @robots {
header Content-Type text/plain
respond `User-agent: *
Allow: /
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: GPTBot
User-agent: ChatGPT-User
User-agent: OAI-SearchBot
User-agent: ClaudeBot
User-agent: Claude-Web
User-agent: anthropic-ai
User-agent: PerplexityBot
User-agent: Bytespider
User-agent: Amazonbot
User-agent: CCBot
User-agent: Google-Extended
User-agent: Applebot-Extended
Disallow: /
` 200
}
'';
};
"mta-sts.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
root * /srv
file_server
header Content-Type text/plain
header Cache-Control "public, max-age=300"
header X-Content-Type-Options nosniff
'';
};
"vault.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
import admin_gate
header Strict-Transport-Security "max-age=31536000;"
reverse_proxy 127.0.0.1:1001
encode zstd gzip
'';
};
"git.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
reverse_proxy 127.0.0.1:3000
encode zstd gzip
'';
};
"mail.severijnse.eu" = {
extraConfig = ''
import security_headers
${antiScrape}
reverse_proxy 127.0.0.1:8888 {
# Strip copies set by the upstream SnappyMail container so we
# emit exactly one correct value of each security header.
header_down -X-Frame-Options
header_down -X-XSS-Protection
header_down -X-Content-Type-Options
}
encode zstd gzip
'';
};
"vpn.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
reverse_proxy 127.0.0.1:51821
encode zstd gzip
'';
};
"mine.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
reverse_proxy 127.0.0.1:81
'';
};
"music.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
reverse_proxy 127.0.0.1:4321
'';
};
"fail2ban.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
import admin_gate
reverse_proxy 127.0.0.1:8080
'';
};
"automate.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
import admin_gate
reverse_proxy 127.0.0.1:5678
encode zstd gzip
'';
};
"pay.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
${antiScrape}
import admin_gate
reverse_proxy 127.0.0.1:5000
encode zstd gzip
'';
};
"http://ip.severijnse.eu" = {
extraConfig = ''
import security_headers
import csp
header Content-Type text/plain
respond {client_ip} 200
'';
};
};
};
systemd.tmpfiles.rules = [
"d /srv 0755 caddy caddy -"
"d /srv/jory 0755 caddy caddy -"
];
}
@@ -0,0 +1,144 @@
{
config,
pkgs,
lib,
...
}: let
zoneFile = pkgs.writeText "severijnse.eu.db" ''
$ORIGIN severijnse.eu.
$TTL 3600
severijnse.eu. 3600 IN SOA ns1.severijnse.eu. abuse.severijnse.eu. 2026071003 3600 1800 1209600 86400
IN NS ns1.severijnse.eu.
IN NS ns2.severijnse.eu.
@ IN A 49.13.92.205
www IN A 49.13.92.205
ns1 IN A 49.13.92.205
ns2 IN A 49.13.92.205
mail IN A 49.13.92.205
@ IN AAAA 2a01:4f8:c014:2585::1
www IN AAAA 2a01:4f8:c014:2585::1
ns1 IN AAAA 2a01:4f8:c014:2585::1
ns2 IN AAAA 2a01:4f8:c014:2585::1
mail IN AAAA 2a01:4f8:c014:2585::1
*.severijnse.eu. IN A 49.13.92.205
*.severijnse.eu. IN AAAA 2a01:4f8:c014:2585::1
@ IN MX 10 mail.severijnse.eu.
mail._domainkey.severijnse.eu. 300 IN TXT (
"v=DKIM1; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAskpG7m4kninxRE4JF5KzpnBhLbOlGJL2RO/iDfzdz6sHEvxe78q9c5UnZ0OQddOSuQo4q48dJkXR/XzqY7Ak109lhoAx+Kr1neYsi8/"
"JaoTC8OURk365+/aBSmWXUCCBphCx43QWfC9h8GMQ6PUIqawkz5CcGPT7X7hPdwHQcd5Vn3CqmABptxdwshdkBjZs"
"oi79BOo9ZrQSTY7iiLcOP7hVVC9Ad+ydlZ4MWGfy5BxgyTGrrtuSuLcM219oqdovIvr2EtXs8AMx5fyXplKE3R/"
"YlwF2Jcy50Gmb5y/E9pOaFjVv8HXUmKsvuhA2b8K+rt0WVHNc3dvbgZUl8bGAQIDAQAB"
)
_dmarc.severijnse.eu. IN TXT "v=DMARC1; p=reject; rua=mailto:abuse@severijnse.eu"
severijnse.eu. IN TXT "v=spf1 mx ip4:49.13.92.205 -all"
default._bimi.severijnse.eu. IN TXT "v=BIMI1; l=https://severijnse.eu/logo.svg; avp=personal;"
severijnse.eu. IN CAA 0 issue "letsencrypt.org"
severijnse.eu. IN CAA 0 issuewild "letsencrypt.org"
severijnse.eu. IN CAA 0 iodef "mailto:abuse@severijnse.eu"
_smtp._tls.severijnse.eu. IN TXT "v=TLSRPTv1; rua=mailto:abuse@severijnse.eu"
_mta-sts.severijnse.eu. IN TXT "v=STSv1; id=2024120501"
severijnse.eu. 300 IN TXT "google-site-verification=H0HHB7zNQ10uom1zH5f8CEtHcVVcWiuu41ZQv348T5U"
; TLSA records updated dynamically by mail-cert-sync service.
; The placeholders below mirror the current live certificate so the zone is
; correct even if the sync service has not yet run (e.g. a failed boot).
_25._tcp.mail.severijnse.eu. 3600 IN TLSA 3 1 1 15ec4d8823874c8363af004188338f76e6e6c5878faf25cdf40556b44b26677d
_465._tcp.mail.severijnse.eu. 3600 IN TLSA 3 1 1 15ec4d8823874c8363af004188338f76e6e6c5878faf25cdf40556b44b26677d
_993._tcp.mail.severijnse.eu. 3600 IN TLSA 3 1 1 15ec4d8823874c8363af004188338f76e6e6c5878faf25cdf40556b44b26677d
severijnse.eu. IN DS 20930 13 2 B0D9B13DCE5FA0D41589239EB5166D124B0C9D3060A538726DDCCBFC91E8DBD8
@ IN DNSKEY 257 3 13 kOc88RGHKdWa7YLjNs7mljux7cT9/9wVNgcp+8jaVe6zle4cHAk6+Sub9wKEVa7Q4FlQYzS1KFL2HOW05Azq1A==
@ IN DNSKEY 256 3 13 QTFplRlXes5NExKLvuCJmX0l8FhulFNS+sJw+pvcAq4+0T3cCzT6hmcJPjd4BVl6KvvyS78Vij2f7HjrcjyHfw==
@ IN CDS 20930 13 2 B0D9B13DCE5FA0D41589239EB5166D124B0C9D3060A538726DDCCBFC91E8DBD8
@ IN CDNSKEY 257 3 13 kOc88RGHKdWa7YLjNs7mljux7cT9/9wVNgcp+8jaVe6zle4cHAk6+Sub9wKEVa7Q4FlQYzS1KFL2HOW05Azq1A==
severijnse.eu IN SSHFP 1 1 7f5235b2997e0c621fe773acaaae45b9551049ce
severijnse.eu IN SSHFP 1 2 2ec1d37df094915253c0b059762052ae19e66cd97e82d7a58ac1a0d1bd5d5e52
severijnse.eu IN SSHFP 3 1 240418eb7cc2a3969257c30363d16103dbfd5822
severijnse.eu IN SSHFP 3 2 43dbfdd8b131efda4c98844e1434e4f35e773f472bdb355ede94f8a9f7001c35
severijnse.eu IN SSHFP 4 1 efbd7836208684d110f3a72543eba74ad8e54b44
severijnse.eu IN SSHFP 4 2 c02f4b13aef78579f3466851fcc741c0169eaa63237463ceacc9bb72a2be0519
'';
in {
# Decrypt DNSSEC keys from sops-encrypted file using the age key at /etc/age/keys.txt
# Uses sops CLI directly instead of sops-nix's sops-install-secrets (avoids Go 1.25 build dep)
systemd.services.decrypt-coredns-keys = {
description = "Decrypt CoreDNS DNSSEC signing keys";
before = ["coredns.service"];
wantedBy = ["coredns.service"];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
Environment = "SOPS_AGE_KEY_FILE=/etc/age/keys.txt";
};
script = ''
SOPS_FILE=${../../secrets/coredns-keys.yaml}
KEYS_DIR=/var/lib/coredns/keys
mkdir -p "$KEYS_DIR"
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.key"
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_ksk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+20930.private"
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_key"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.key"
${pkgs.sops}/bin/sops --decrypt --extract '["coredns_zsk_private"]' "$SOPS_FILE" > "$KEYS_DIR/Kseverijnse.eu.+013+38678.private"
chmod 644 "$KEYS_DIR/"*
'';
};
# Ensure coredns waits for key decryption
systemd.services.coredns = {
after = ["decrypt-coredns-keys.service"];
requires = ["decrypt-coredns-keys.service"];
};
services.coredns = {
enable = true;
config = ''
severijnse.eu {
bind 127.0.0.1 49.13.92.205 2a01:4f8:c014:2585::1
file /var/lib/coredns/zones/severijnse.eu.db {
reload 300s
}
dnssec {
key file /var/lib/coredns/keys/Kseverijnse.eu.+013+38678
key file /var/lib/coredns/keys/Kseverijnse.eu.+013+20930
}
log
errors
}
. {
bind 127.0.0.1
forward . 1.1.1.1 8.8.8.8
log
errors
}
'';
};
# Copy zone file from Nix store to writable location on service start
# TLSA updater will modify the writable copy at runtime
systemd.services.coredns = {
preStart = ''
cp -f ${zoneFile} /var/lib/coredns/zones/severijnse.eu.db
chown coredns:coredns /var/lib/coredns/zones/severijnse.eu.db
'';
};
systemd.tmpfiles.rules = [
"d /var/lib/coredns 0750 coredns coredns -"
"d /var/lib/coredns/zones 0750 coredns coredns -"
"d /var/lib/coredns/keys 0750 coredns coredns -"
];
}
@@ -0,0 +1,27 @@
{
config,
pkgs,
lib,
...
}: {
# Keep fail2ban as OCI container to preserve the web UI
virtualisation.oci-containers.containers.fail2ban = {
image = "crazymax/fail2ban:latest";
autoStart = true;
volumes = [
"/home/admin/dms/mail-logs:/var/log/mail:ro"
"/home/admin/fail2ban/data:/data:Z"
];
environment = {
TZ = "Europe/Amsterdam";
};
extraOptions = [
"--network=host"
"--cap-add=NET_ADMIN"
];
};
systemd.tmpfiles.rules = [
"d /var/log/mail 0755 root root -"
];
}
@@ -0,0 +1,64 @@
{
config,
pkgs,
lib,
unstablePkgs,
...
}: {
services.postgresql = {
enable = true;
package = pkgs.postgresql_14;
ensureDatabases = ["gitea"];
ensureUsers = [
{
name = "gitea";
ensureDBOwnership = true;
}
];
};
# Gitea connects to local Postgres via Unix socket (peer auth).
# No password needed — the socket is at /run/postgresql by default.
# createDatabase = true ensures the DB + user are set up automatically.
services.gitea = {
enable = true;
package = unstablePkgs.gitea;
database.type = "postgres";
database.name = "gitea";
database.user = "gitea";
appName = "Jory's Git";
lfs.enable = true;
settings = {
server = {
DOMAIN = "git.severijnse.eu";
ROOT_URL = "https://git.severijnse.eu/";
HTTP_PORT = 3000;
SSH_PORT = 222;
SSH_LISTEN_PORT = 2222;
START_SSH_SERVER = true;
SSH_USER = "git";
BUILTIN_SSH_SERVER_USER = "git";
LANDING_PAGE = "explore";
};
service = {
DISABLE_REGISTRATION = true;
REQUIRE_SIGNIN_VIEW = false;
};
repository = {
DEFAULT_BRANCH = "main";
};
};
};
# Gitea built-in SSH server: listens on high port 2222 (no privileged-cap needed),
# while clone URLs advertise port 222. Firewall redirects 222 -> 2222.
networking.firewall.allowedTCPPorts = [222 2222];
# Redirect external git SSH (222) to Gitea's internal listener (2222)
networking.firewall.extraCommands = ''
${pkgs.nftables}/bin/nft add table inet gitea-redirect 2>/dev/null || true
${pkgs.nftables}/bin/nft flush chain inet gitea-redirect prerouting 2>/dev/null || true
${pkgs.nftables}/bin/nft add chain inet gitea-redirect prerouting '{ type nat hook prerouting priority dstnat; }' 2>/dev/null || true
${pkgs.nftables}/bin/nft add rule inet gitea-redirect prerouting tcp dport 222 redirect to :2222 2>/dev/null || true
'';
}
@@ -0,0 +1,46 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.mailserver = {
image = "ghcr.io/docker-mailserver/docker-mailserver:latest";
autoStart = true;
hostname = "mail.severijnse.eu";
ports = [
"25:25"
"[::]:25:25"
"143:143"
"[::]:143:143"
"465:465"
"[::]:465:465"
"587:587"
"[::]:587:587"
"993:993"
"[::]:993:993"
];
volumes = [
"/home/admin/dms/mail-data/:/var/mail/:Z"
"/home/admin/dms/mail-state/:/var/mail-state/:Z"
"/home/admin/dms/mail-logs/:/var/log/mail/:Z"
"/home/admin/dms/config/:/tmp/docker-mailserver/:Z"
"/etc/localtime:/etc/localtime:ro"
# On the running Debian, Caddy v2 stores certs at /home/admin/caddy/data/caddy/certificates/...
# On NixOS, Caddy stores certs at /var/lib/caddy/certificates/...
"/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu/:/etc/letsencrypt/live/mail.severijnse.eu/:ro"
];
environment = {
OVERRIDE_HOSTNAME = "";
LOG_LEVEL = "info";
SPOOF_PROTECTION = "1";
MOVE_SPAM_TO_JUNK = "1";
};
environmentFiles = ["/home/admin/mailserver.env"];
extraOptions = [
"--label=com.centurylinklabs.watchtower.enable=true"
"--dns=1.1.1.1"
"--dns=8.8.8.8"
];
};
}
@@ -0,0 +1,32 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers = {
hbbr = {
image = "rustdesk/rustdesk-server:latest";
cmd = ["hbbr"];
autoStart = true;
volumes = [
"/home/admin/rustdesk:/root:Z"
];
extraOptions = [
"--network=host"
];
};
hbbs = {
image = "rustdesk/rustdesk-server:latest";
cmd = ["hbbs"];
autoStart = true;
volumes = [
"/home/admin/rustdesk:/root:Z"
];
dependsOn = ["hbbr"];
extraOptions = [
"--network=host"
];
};
};
}
@@ -0,0 +1,23 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.shkeeper = {
image = "vsyshost/shkeeper:2.5.29";
autoStart = true;
ports = ["5000:5000"];
volumes = [
"/home/admin/shkeeper-data:/app/data:Z"
];
environment = {
SHKEEPER_HOST = "0.0.0.0";
SHKEEPER_PORT = "5000";
};
extraOptions = [
"--env-file=/home/admin/shkeeper.env"
"--label=com.centurylinklabs.watchtower.enable=true"
];
};
}
@@ -0,0 +1,28 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.snappymail = {
image = "djmaze/snappymail:latest";
autoStart = true;
ports = ["127.0.0.1:8888:8888"];
volumes = [
"/home/admin/snappymail-data:/var/lib/snappymail:Z"
];
environment = {
TZ = "Europe/Berlin";
};
extraOptions = [
"--label=com.centurylinklabs.watchtower.enable=true"
];
};
# Ensure the persistent data dir exists so podman's :Z relabel (statfs) succeeds on first boot.
# Owned by 82:82 (www-data) because the container's PHP worker runs as UID 82 and must be
# able to write to /var/lib/snappymail (SnappyMail checks is_writable on that path).
systemd.tmpfiles.rules = [
"d /home/admin/snappymail-data 0755 82 82 - -"
];
}
@@ -0,0 +1,99 @@
{
config,
pkgs,
lib,
...
}: let
# Caddy's canonical certificate storage (XDG data dir). Renewals land here,
# owned caddy:caddy 0600 — the mail server's non-root Postfix/Dovecot cannot
# read it directly, so we copy it into a world-readable distribution dir.
caddyCertDir = "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
# World-readable distribution dir mounted (RO) into the mail server container.
distCertDir = "/var/lib/caddy/certificates/acme-v02.api.letsencrypt.org-directory/mail.severijnse.eu";
zoneFile = "/var/lib/coredns/zones/severijnse.eu.db";
syncScript = pkgs.writeShellScript "tlsa-update" ''
set -euo pipefail
OPENSSL="${pkgs.openssl}/bin/openssl"
SRC_CERT="${caddyCertDir}/mail.severijnse.eu.crt"
SRC_KEY="${caddyCertDir}/mail.severijnse.eu.key"
DST_CERT="${distCertDir}/mail.severijnse.eu.crt"
DST_KEY="${distCertDir}/mail.severijnse.eu.key"
# Caddy has not obtained the certificate yet (e.g. first boot before HTTP-01).
if [ ! -f "$SRC_CERT" ] || [ ! -f "$SRC_KEY" ]; then
echo "tlsa-update: certificate not found at $caddyCertDir, skipping" >&2
exit 0
fi
# 1) Propagate Caddy's renewed certificate into the distribution dir the
# mail server mounts. Caddy stores certs 0600 caddy:caddy. The cert is
# world-readable (Postfix/Dovecot read it as root before dropping
# privileges); the private key is restricted to root (0640) so it is not
# exposed to other local users.
install -D -m 0644 "$SRC_CERT" "$DST_CERT"
install -D -m 0640 "$SRC_KEY" "$DST_KEY"
# 2) TLSA 3 1 1 = SHA-256 of the certificate's SubjectPublicKeyInfo (SPKI),
# NOT the whole certificate. Matching type 1 = SHA-256 of the SPKI DER.
HEX=$("$OPENSSL" x509 -in "$DST_CERT" -noout -pubkey 2>/dev/null | "$OPENSSL" pkey -pubin -outform DER 2>/dev/null | "$OPENSSL" dgst -sha256 | cut -d' ' -f2)
# 3) Update the CoreDNS zone. coredns.service regenerates this file from the
# Nix store on every start, so this unit (which is partOf coredns and runs
# after it) re-applies the correct TLSA after each rebuild/restart.
if [ -f "${zoneFile}" ]; then
sed -i -E "s/^(_25\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}"
sed -i -E "s/^(_465\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}"
sed -i -E "s/^(_993\._tcp\.mail.*TLSA 3 1 1).*/\1 $HEX/" "${zoneFile}"
chown coredns:coredns "${zoneFile}"
chmod 0640 "${zoneFile}"
fi
# 4) Reload services so the changes take effect immediately.
systemctl reload coredns.service || true
podman exec mailserver postfix reload || true
podman exec mailserver dovecot reload || true
echo "tlsa-update: TLSA set to $HEX"
'';
in {
# Ensure the distribution dir exists (Caddy does not write here).
systemd.tmpfiles.rules = [
"d ${distCertDir} 0755 root root - -"
];
systemd.services.tlsa-update = {
description = "Sync Caddy TLS certificate to mail server and update DANE/TLSA records";
after = ["caddy.service" "coredns.service"];
partOf = ["coredns.service"];
wantedBy = ["multi-user.target"];
path = with pkgs; [openssl coreutils gnused podman systemd];
serviceConfig = {
Type = "oneshot";
ExecStart = "${syncScript}";
User = "root";
Group = "root";
};
};
# Fire as soon as Caddy rewrites the certificate on renewal (the atomic rewrite
# changes the directory mtime), eliminating the up-to-24h DANE drift window.
systemd.paths.tlsa-update = {
description = "Watch Caddy certificate directory for renewal";
wantedBy = ["paths.target"];
pathConfig = {
PathModified = [caddyCertDir];
Unit = "tlsa-update.service";
};
};
# Fallback in case a renewal event is missed (e.g. inotify overflow).
systemd.timers.tlsa-update = {
wantedBy = ["timers.target"];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
};
};
}
@@ -0,0 +1,22 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.vaultwarden = {
image = "vaultwarden/server:latest";
autoStart = true;
ports = ["1001:80"];
volumes = [
"/home/admin/vw-data:/data:Z"
];
environment = {
DOMAIN = "https://vault.severijnse.eu";
LOG_FILE = "/data/vaultwarden.log";
};
extraOptions = [
"--label=com.centurylinklabs.watchtower.enable=true"
];
};
}
@@ -0,0 +1,22 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.watchtower = {
image = "ghcr.io/nicholas-fedor/watchtower:latest";
autoStart = true;
volumes = [
"/var/run/podman/podman.sock:/var/run/docker.sock:ro"
];
environment = {
WATCHTOWER_CLEANUP = "true";
WATCHTOWER_POLL_INTERVAL = "86400";
WATCHTOWER_INCLUDE_STOPPED = "true";
WATCHTOWER_REVIVE_STOPPED = "true";
TZ = "Europe/Amsterdam";
};
cmd = ["--label-enable"];
};
}
@@ -0,0 +1,27 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.wg-easy = {
image = "ghcr.io/wg-easy/wg-easy:latest";
autoStart = true;
volumes = [
"/home/admin/config:/etc/wireguard:Z"
];
environment = {
WG_HOST = "severijnse.eu";
PASSWORD_HASH = "$2a$12$b3n4drXgS3B6ubMZxxjPUOQ1XktZ1EuDwm4AIdVulhtoD7b1.WQGC";
WG_DEFAULT_ADDRESS = "10.8.0.x";
WG_DEFAULT_DNS = "1.1.1.1";
WG_PORT = "51820";
};
extraOptions = [
"--cap-add=NET_ADMIN"
"--cap-add=SYS_MODULE"
"--cap-add=NET_RAW"
"--network=host"
];
};
}
@@ -0,0 +1,18 @@
{
config,
pkgs,
lib,
...
}: {
virtualisation.oci-containers.containers.wrxproxy = {
image = "localhost/wrxproxy:latest";
autoStart = false;
ports = ["81:81"];
volumes = [
"/home/admin/WRXproxy/config.json:/app/config.json:ro"
];
extraOptions = [
"--label=com.centurylinklabs.watchtower.enable=true"
];
};
}