From 4f7283ca897cde47c7138a8a70941c8d155a5f8b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Feb 2026 23:20:49 +0100 Subject: [PATCH] Initial: Caddy, Stalwart, fail2ban, crontab, allowlist scripts --- caddy_Caddyfile | 71 ++++++++++++++++++++++++++++++ crontab_root.txt | 2 + fail2ban/caddy-kuma.conf | 4 ++ fail2ban/caddy-scanner.conf | 4 ++ fail2ban/defaults-debian.conf | 7 +++ fail2ban/jail.local | 8 ++++ fail2ban/stalwart.conf | 6 +++ fail2ban/vaultwarden.conf | 4 ++ stalwart-allowlist-sync.sh | 12 +++++ stalwart-update-allowip.py | 16 +++++++ stalwart_config.toml | 83 +++++++++++++++++++++++++++++++++++ vaultwarden_backup.sh | 28 ++++++++++++ 12 files changed, 245 insertions(+) create mode 100644 caddy_Caddyfile create mode 100644 crontab_root.txt create mode 100644 fail2ban/caddy-kuma.conf create mode 100644 fail2ban/caddy-scanner.conf create mode 100644 fail2ban/defaults-debian.conf create mode 100644 fail2ban/jail.local create mode 100644 fail2ban/stalwart.conf create mode 100644 fail2ban/vaultwarden.conf create mode 100755 stalwart-allowlist-sync.sh create mode 100755 stalwart-update-allowip.py create mode 100644 stalwart_config.toml create mode 100755 vaultwarden_backup.sh diff --git a/caddy_Caddyfile b/caddy_Caddyfile new file mode 100644 index 0000000..a53bdd7 --- /dev/null +++ b/caddy_Caddyfile @@ -0,0 +1,71 @@ +# Global config +{ + log { + output file /var/log/caddy/access.log { + roll_size 100mb + roll_keep 5 + } + format json + } +} + +# Zurich infrastructure +zurich.inou.com { + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "SAMEORIGIN" + Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "geolocation=(), microphone=(), camera=()" + -Server + } + respond "inou security infrastructure" 200 +} + +ntfy.inou.com { + reverse_proxy 127.0.0.1:2586 +} + +kuma.inou.com { + log { + output file /var/log/caddy/kuma.log { + roll_size 50mb + roll_keep 5 + } + format json + } + reverse_proxy 127.0.0.1:3001 +} + +vault.inou.com { + log { + output file /var/log/caddy/vault.log { + roll_size 50mb + roll_keep 5 + } + format json + } + reverse_proxy 127.0.0.1:8080 +} + +mail.inou.com { + reverse_proxy 127.0.0.1:8880 +} + +mail.jongsma.me { + reverse_proxy 127.0.0.1:8880 +} + +harryhaasjes.nl, www.harryhaasjes.nl { + root * /var/www/harryhaasjes/public + file_server + encode gzip +} + +stpetersburgaquatics.com, www.stpetersburgaquatics.com { + root * /var/www/stpetersburgaquatics/public + file_server + encode gzip +} + + diff --git a/crontab_root.txt b/crontab_root.txt new file mode 100644 index 0000000..8526166 --- /dev/null +++ b/crontab_root.txt @@ -0,0 +1,2 @@ +0 3 * * * /opt/vaultwarden/backup.sh >> /var/log/vaultwarden-backup.log 2>&1 +*/15 * * * * /usr/local/bin/stalwart-allowlist-sync.sh >> /var/log/stalwart-allowsync.log 2>&1 diff --git a/fail2ban/caddy-kuma.conf b/fail2ban/caddy-kuma.conf new file mode 100644 index 0000000..5c91fc1 --- /dev/null +++ b/fail2ban/caddy-kuma.conf @@ -0,0 +1,4 @@ +[Definition] +failregex = .*"remote_ip":"".*"uri":"/api/login.*"status":40[13] + .*"client_ip":"".*"uri":"/api/login.*"status":40[13] +ignoreregex = diff --git a/fail2ban/caddy-scanner.conf b/fail2ban/caddy-scanner.conf new file mode 100644 index 0000000..90cf835 --- /dev/null +++ b/fail2ban/caddy-scanner.conf @@ -0,0 +1,4 @@ +[Definition] +failregex = .*"remote_ip":"".*"uri":".*(?:wp-admin|wp-login|\.env|\.git|phpmyadmin|phpinfo|xmlrpc|actuator|setup\.php|config\.php|admin\.php|shell|eval\(|\.aws|\.ssh).*"status":(?:400|403|404|405) + .*"client_ip":"".*"uri":".*(?:wp-admin|wp-login|\.env|\.git|phpmyadmin|phpinfo|xmlrpc|actuator|setup\.php|config\.php|admin\.php|shell|eval\(|\.aws|\.ssh).*"status":(?:400|403|404|405) +ignoreregex = diff --git a/fail2ban/defaults-debian.conf b/fail2ban/defaults-debian.conf new file mode 100644 index 0000000..d0d52ae --- /dev/null +++ b/fail2ban/defaults-debian.conf @@ -0,0 +1,7 @@ +[DEFAULT] +banaction = nftables +banaction_allports = nftables[type=allports] +backend = systemd + +[sshd] +enabled = true diff --git a/fail2ban/jail.local b/fail2ban/jail.local new file mode 100644 index 0000000..52d084d --- /dev/null +++ b/fail2ban/jail.local @@ -0,0 +1,8 @@ +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log +maxretry = 3 +bantime = 3600 +findtime = 600 diff --git a/fail2ban/stalwart.conf b/fail2ban/stalwart.conf new file mode 100644 index 0000000..627e8e4 --- /dev/null +++ b/fail2ban/stalwart.conf @@ -0,0 +1,6 @@ +[Definition] +# Match auth failures and too-many-attempts from Stalwart logs +failregex = ^\S+ \S+ .*\(auth\.failed\).*remoteIp = + ^\S+ \S+ .*\(auth\.too-many-attempts\).*remoteIp = +ignoreregex = +datepattern = %%Y-%%m-%%dT%%H:%%M:%%SZ diff --git a/fail2ban/vaultwarden.conf b/fail2ban/vaultwarden.conf new file mode 100644 index 0000000..c0c49d1 --- /dev/null +++ b/fail2ban/vaultwarden.conf @@ -0,0 +1,4 @@ +[Definition] +failregex = .*"remote_ip":"".*"uri":"/api/accounts/login.*"status":40[13] + .*"client_ip":"".*"uri":"/api/accounts/login.*"status":40[13] +ignoreregex = diff --git a/stalwart-allowlist-sync.sh b/stalwart-allowlist-sync.sh new file mode 100755 index 0000000..51bf316 --- /dev/null +++ b/stalwart-allowlist-sync.sh @@ -0,0 +1,12 @@ +#!/bin/bash +STATE_FILE=/var/lib/stalwart-allowsync/home-ip +CURRENT_IP=47.197.93.62 +[ -z "$CURRENT_IP" ] && exit 1 +mkdir -p /var/lib/stalwart-allowsync +LAST_IP=$(cat "$STATE_FILE" 2>/dev/null) +[ "$CURRENT_IP" = "$LAST_IP" ] && exit 0 +echo "$(date -u): Home IP changed: $LAST_IP -> $CURRENT_IP" +python3 /usr/local/bin/stalwart-update-allowip.py "$CURRENT_IP" +/opt/stalwart/bin/stalwart-cli -u http://127.0.0.1:8880 -c admin:JamesAdmin2026x server reload-config > /dev/null +echo "$CURRENT_IP" > "$STATE_FILE" +echo Done. diff --git a/stalwart-update-allowip.py b/stalwart-update-allowip.py new file mode 100755 index 0000000..112c3bb --- /dev/null +++ b/stalwart-update-allowip.py @@ -0,0 +1,16 @@ +import sys, re +new_ip = sys.argv[1] +config_path = '/opt/stalwart/etc/config.toml' +content = open(config_path).read() +new_section = '[server.allowed-ip]\n"' + new_ip + '" = true\n' +if '[server.allowed-ip]' in content: + content = re.sub(r'\[server\.allowed-ip\].*?(?=\n\[|\Z)', new_section, content, flags=re.DOTALL) +else: + content = content.rstrip() + '\n\n' + new_section +open(config_path, 'w').write(content) +print(f'Updated allowed-ip to {new_ip}') + +# After writing config, commit to git +import subprocess +subprocess.run(['git', '-C', '/opt/stalwart/etc', 'add', 'config.toml'], capture_output=True) +subprocess.run(['git', '-C', '/opt/stalwart/etc', 'commit', '-m', f'auto: allowed-ip updated to {new_ip}'], capture_output=True) diff --git a/stalwart_config.toml b/stalwart_config.toml new file mode 100644 index 0000000..f9d8dae --- /dev/null +++ b/stalwart_config.toml @@ -0,0 +1,83 @@ +[server.listener.smtp] +bind = "[::]:25" +protocol = "smtp" + +[server.listener.submission] +bind = "[::]:587" +protocol = "smtp" + +[server.listener.submissions] +bind = "[::]:465" +protocol = "smtp" +tls.implicit = true + +[server.listener.imap] +bind = "[::]:143" +protocol = "imap" + +[server.listener.imaptls] +bind = "[::]:993" +protocol = "imap" +tls.implicit = true + +[server.listener.pop3] +bind = "[::]:110" +protocol = "pop3" + +[server.listener.pop3s] +bind = "[::]:995" +protocol = "pop3" +tls.implicit = true + +[server.listener.sieve] +bind = "[::]:4190" +protocol = "managesieve" + +[server.listener.https] +protocol = "http" +bind = "127.0.0.1:8443" +tls.implicit = false + +[server.listener.http] +protocol = "http" +bind = "127.0.0.1:8880" + +[storage] +data = "rocksdb" +fts = "rocksdb" +blob = "rocksdb" +lookup = "rocksdb" +directory = "internal" + +[store.rocksdb] +type = "rocksdb" +path = "/opt/stalwart/data" +compression = "lz4" + +[directory.internal] +type = "internal" +store = "rocksdb" + +[tracer.log] +type = "log" +level = "info" +path = "/opt/stalwart/logs" +prefix = "stalwart.log" +rotate = "daily" +ansi = false +enable = true + +[authentication.fallback-admin] +user = "admin" +secret = "$6$stalwartjames$OlCxhWXHNuO3Szh.HHPmjuh3oI/B0iCYjeERKqXSlpGHw40oHxVOd0IW9pJZn54QjA2Dbdlrin.SQRfZBG8pw1" + +[lookup.default] +hostname = "mail.jongsma.me" + +[certificate.default] +cert = "%{file:/etc/letsencrypt/live/mail.jongsma.me/fullchain.pem}%" +private-key = "%{file:/etc/letsencrypt/live/mail.jongsma.me/privkey.pem}%" +default = true + +[server.allowed-ip] +"47.197.93.62" = true diff --git a/vaultwarden_backup.sh b/vaultwarden_backup.sh new file mode 100755 index 0000000..4b17fa2 --- /dev/null +++ b/vaultwarden_backup.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Vaultwarden daily backup +BACKUP_DIR="/opt/vaultwarden/backups" +DATA_DIR="/opt/vaultwarden/data" +DATE=$(date +%Y-%m-%d) +BACKUP_FILE="$BACKUP_DIR/vaultwarden-$DATE.tar.gz.gpg" + +mkdir -p "$BACKUP_DIR" + +# Stop container briefly for consistent backup +docker stop vaultwarden +sleep 2 + +# Create encrypted backup (symmetric, passphrase from file) +tar czf - -C /opt/vaultwarden data | gpg --batch --yes --symmetric --cipher-algo AES256 --passphrase-file /opt/vaultwarden/.backup_passphrase -o "$BACKUP_FILE" + +# Restart +docker start vaultwarden + +# Upload to gdrive if rclone is configured +if rclone listremotes 2>/dev/null | grep -q gdrive; then + rclone copy "$BACKUP_FILE" gdrive:backups/vaultwarden/ +fi + +# Retain only last 30 days +find "$BACKUP_DIR" -name "vaultwarden-*.tar.gz.gpg" -mtime +30 -delete + +echo "Backup complete: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"