Added hetzner server configs
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
# NOTE: Caddy handles all TLS natively via its ACME integration.
|
||||
# This module is kept as a fallback for non-Caddy services.
|
||||
# Currently NOT imported in default.nix — uncomment there to activate.
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = "jory@severijnse.eu";
|
||||
group = "caddy";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
time.timeZone = "Europe/Amsterdam";
|
||||
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
vim
|
||||
git
|
||||
curl
|
||||
wget
|
||||
htop
|
||||
iotop
|
||||
btop
|
||||
bind.dnsutils # provides dig, nslookup, host
|
||||
jq
|
||||
yq
|
||||
fish
|
||||
podman-compose
|
||||
];
|
||||
|
||||
programs.fish.enable = true;
|
||||
programs.bash.enableCompletion = true;
|
||||
}
|
||||
Reference in New Issue
Block a user