Stand: 07.11.2026
System: Proxmox-LXC nginx, darin NGINX Proxy Manager über Docker Compose
Ziel: Bekannte Angreifer-IP-Adressen und -Netzwerke aus der IPv64-/blocklist.de-Liste für alle NPM-Proxy-Hosts mit HTTP 403 abweisen. Automatische Aktualisierung alle vier Stunden einschließlich Prüfung, Sicherung und Protokollrotation.
| Komponente | Stand nach Einrichtung |
|---|---|
| NGINX Proxy Manager | Docker-Container nginx-app-1 (OpenResty/NGINX) |
| Blacklist | Aktiv; 26.381 Einträge beim erfolgreichen Test am 06.11.2026 (Anzahl ändert sich) |
| Sperrmechanismus | NGINX geo + if ($ipv64_blocked) { return 403; } |
| Gültigkeitsbereich | Alle regulären NPM-Proxy-Hosts; nicht automatisch NPM-Adminoberfläche, Redirection Hosts oder TCP/UDP-Streams |
| Update | Cron täglich um Minute 17 alle vier Stunden (Zeitzone des LXC beachten) |
| Updateschutz | HTTPS, IP-Validierung, Mindestanzahl, Vergleich, Konfigurationstest, Sicherung und Rollback |
| Logging | /var/log/ipv64-nginx-update.log, wöchentlich rotiert |
| Funktionstest | Erlaubte Test-IP → HTTP 302, gesperrte Test-IP → HTTP 403 |
Hinweis: Die Uhrzeit des Update-Skripts erschien in UTC. Die genauen lokalen Cron-Ausführungszeiten richten sich nach der Zeitzone des LXC (prüfen mit timedatectl).
Auf dem LXC-Host als root ausführen, nicht auf dem Proxmox-Host:
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' docker inspect nginx-app-1 --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' docker exec nginx-app-1 nginx -t
Ermittelte Container und Mounts:
nginx-app-1 jc21/nginx-proxy-manager:latest nginx-db-1 jc21/mariadb-aria:latest duckdns lscr.io/linuxserver/duckdns:latest /root/data -> /data /root/letsencrypt -> /etc/letsencrypt
Wichtig: Die IP-Blacklist wird in den persistenten NPM-Daten unter /root/data gespeichert. Die Docker-Compose-Datei und die automatisch erzeugten Proxy-Host-Konfigurationen werden nicht verändert.
Symptom: Der ursprüngliche Download scheiterte mit:
curl: (77) error setting certificate file: /etc/ssl/certs/ca-certificates.crt
Die CA-Datei war vorhanden; das Paket ca-certificates war installiert. Ein erster Neuaufbau mit update-ca-certificates –fresh beseitigte den Fehler noch nicht.
Die Diagnose zeigte:
curl -q -v -I --cacert /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt https://ipv64.net/
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt
/usr/local/share/ca-certificates/ lag eine fehlerhafte Datei digicert_global_root_g2.crt (Kleinschreibung). Eine ähnlich benannte Datei mit Großschreibung war lesbar.Durchgeführte Reparatur: Nur die konkret fehlerhafte Datei wurde gesichert und aus der lokalen CA-Importstruktur entfernt; es wurde nichts gelöscht.
mkdir -p /root/ca-quarantaene mv /usr/local/share/ca-certificates/digicert_global_root_g2.crt \ /root/ca-quarantaene/ update-ca-certificates --fresh openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \ /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt curl -q -I --max-time 20 https://ipv64.net/
Ergebnis: ISRG_Root_X1.crt: OK und HTTP/1.1 200 OK.
Die weiteren Meldungen zu GeoTrust_TLS_RSA_CA_G1, strato_bundle und doppelten Zertifikaten waren Warnungen, nicht die Ursache des hier behobenen Fehlers. Diese Dateien nicht pauschal löschen. curl -k wurde ausdrücklich nicht verwendet, damit die TLS-Prüfung erhalten bleibt.
Quelle: IPv64 blocklist.de – all
Die Quelle enthält IP-Adressen und CIDR-Netze (IPv4/IPv6), keine DNS-Domain-Blockliste. Deshalb ist sie für NGINX geeignet, nicht als gewöhnliche DNS-Blockliste in AdGuard Home. Einen entsprechenden AdGuard-Eintrag wieder entfernen.
Erster Download und Umwandlung in NGINX-geo-Syntax:
mkdir -p /root/data/nginx/custom curl -fLsS --retry 3 --max-time 90 \ https://ipv64.net/blocklists/ipv64_blocklist_blocklistde_all.txt \ -o /root/data/nginx/ipv64-source.tmp awk 'NF == 1 && /^[0-9A-Fa-f:.]+(\/[0-9]+)?$/ {print $1 " 1;"}' \ /root/data/nginx/ipv64-source.tmp \ > /root/data/nginx/ipv64-geo.conf.tmp wc -l /root/data/nginx/ipv64-source.tmp wc -l /root/data/nginx/ipv64-geo.conf.tmp head -n 5 /root/data/nginx/ipv64-geo.conf.tmp install -m 644 /root/data/nginx/ipv64-geo.conf.tmp \ /root/data/nginx/ipv64-geo.conf
Beim ersten Import: 26.375 Einträge; beim folgenden Updater-Lauf: 26.381. Die späteren automatischen Updates validieren die IP-/Netzsyntax mit Pythons ipaddress-Modul (deutlich zuverlässiger als nur der erste AWK-Filter).
Beispiel eines geo-Eintrags:
1.14.192.95/32 1;
NPM erlaubt die persistenten Custom-Includes /data/nginx/custom/http_top.conf (im http-Kontext) und /data/nginx/custom/server_proxy.conf (in den Proxy-Host-Serverblöcken).
5.1. GEO-Variable im HTTP-Kontext definieren
Host-Datei: /root/data/nginx/custom/http_top.conf
geo $ipv64_blocked { default 0; include /data/nginx/ipv64-geo.conf; }
5.2. Sperrregel für alle regulären Proxy Hosts
Host-Datei: /root/data/nginx/custom/server_proxy.conf
# IPv64 Blacklist – gesperrte Clients bekommen HTTP 403 if ($ipv64_blocked) { return 403; }
5.3. Prüfen und aktivieren
docker exec nginx-app-1 nginx -t docker exec nginx-app-1 nginx -s reload docker exec nginx-app-1 nginx -T 2>/dev/null | \ grep -F -e 'geo $ipv64_blocked' \ -e 'include /data/nginx/ipv64-geo.conf;' \ -e 'if ($ipv64_blocked)' | sort -u
Ergebnis: Konfiguration gültig und Reload erfolgreich. Die Warnungen listen … http2 directive is deprecated in proxy_host/6.conf sind unabhängig von der Blacklist und verhinderten den Reload nicht.
Installiertes Skript: /usr/local/sbin/update-ipv64-nginx.sh
#!/bin/bash set -Eeuo pipefail DIR="/root/data/nginx" TARGET="$DIR/ipv64-geo.conf" BACKUP="$DIR/ipv64-geo.conf.bak" URL="https://ipv64.net/blocklists/ipv64_blocklist_blocklistde_all.txt" CONTAINER="nginx-app-1" exec 9>/run/lock/ipv64-nginx.lock flock -n 9 || { echo "Update läuft bereits." exit 0 } RAW=$(mktemp "$DIR/.ipv64-raw.XXXXXX") NEW=$(mktemp "$DIR/.ipv64-new.XXXXXX") RESTORE=$(mktemp "$DIR/.ipv64-restore.XXXXXX") trap 'rm -f "$RAW" "$NEW" "$RESTORE"' EXIT echo "=== IPv64 Update: $(date) ===" curl -fLsS --retry 3 --max-time 90 \ "$URL" -o "$RAW" python3 - "$RAW" "$NEW" "$TARGET" <<'PY' import ipaddress import sys source, output, previous = sys.argv[1:] entries = [] seen = set() with open(source, encoding="utf-8-sig") as f: for line in f: line = line.strip() if not line or line.startswith("#"): continue network = ipaddress.ip_network(line, strict=False) if network.prefixlen == 0: raise ValueError("Unsicheres /0-Netz erkannt") entry = str(network) if entry not in seen: seen.add(entry) entries.append(entry) with open(previous, encoding="utf-8") as f: old_count = sum( 1 for line in f if line.strip() ) if len(entries) < 10000: raise ValueError("Zu wenige Einträge") if len(entries) < old_count // 2: raise ValueError("Ungewöhnlich starker Listenrückgang") with open(output, "w", encoding="utf-8") as f: for entry in entries: f.write(f"{entry} 1;\n") print(f"Alte Einträge: {old_count}") print(f"Neue Einträge: {len(entries)}") PY if cmp -s "$TARGET" "$NEW"; then echo "Keine Änderungen erforderlich." exit 0 fi cp -p "$TARGET" "$BACKUP" chmod 644 "$NEW" mv -f "$NEW" "$TARGET" if docker exec "$CONTAINER" nginx -t -q && docker exec "$CONTAINER" nginx -s reload; then echo "ERFOLG: Blacklist aktualisiert." else echo "FEHLER: Vorherige Liste wird wiederhergestellt." cp -p "$BACKUP" "$RESTORE" mv -f "$RESTORE" "$TARGET" docker exec "$CONTAINER" nginx -t -q && docker exec "$CONTAINER" nginx -s reload exit 1 fi
Installation und manueller Test:
chmod 750 /usr/local/sbin/update-ipv64-nginx.sh bash -n /usr/local/sbin/update-ipv64-nginx.sh /usr/local/sbin/update-ipv64-nginx.sh
Erfolgreicher Test: 26.375 → 26.381 Einträge, NGINX-Konfiguration geprüft, Reload erfolgreich. Ein zweiter Lauf mit unveränderten 26.381 Einträgen meldete Keine Änderungen erforderlich.
Hinweis zur Fehlerbehandlung: Das Skript bewahrt eine Sicherung der letzten Version unter /root/data/nginx/ipv64-geo.conf.bak auf und versucht bei Fehlern nach dem Austausch die vorherige Liste wiederherzustellen. Ein provozierter Fehler-/Rollback-Test wurde nicht durchgeführt. Bei einem vollständigen Ausfall des Docker-Dienstes kann auch der Wiederherstellungs-Reload fehlschlagen; dann ist manuelles Eingreifen erforderlich.
Datei /etc/cron.d/ipv64-nginx:
SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 17 */4 * * * root /usr/local/sbin/update-ipv64-nginx.sh >> /var/log/ipv64-nginx-update.log 2>&1
Aktivieren / prüfen:
chmod 644 /etc/cron.d/ipv64-nginx systemctl enable --now cron systemctl is-active cron cat /etc/cron.d/ipv64-nginx timedatectl
cron meldete active. Die Ausführung findet um 00:17, 04:17, 08:17, 12:17, 16:17, 20:17 in der eingestellten Zeitzone des LXC statt. Bei UTC-Zeitzone entsprechen die deutschen Lokalzeiten je nach Sommer-/Winterzeit anderen Uhrzeiten.
Datei /etc/logrotate.d/ipv64-nginx:
/var/log/ipv64-nginx-update.log {
su root root
weekly
rotate 8
compress
missingok
notifempty
create 0640 root adm
}
Der erste Test schlug fehl, weil /var/log die Berechtigung 775 root:syslog besitzt. Das ist nicht automatisch ein Problem; die Rechte von /var/log wurden nicht geändert. Die Ergänzung su root root behob die Logrotate-Fehlermeldung.
Prüfung:
logrotate -d /etc/logrotate.d/ipv64-nginx 2>&1 | tail -n 20 /usr/local/sbin/update-ipv64-nginx.sh >> /var/log/ipv64-nginx-update.log 2>&1 tail -n 15 /var/log/ipv64-nginx-update.log logrotate -d /etc/logrotate.d/ipv64-nginx 2>&1 | tail -n 20
Ergebnis: Logdatei erstellt; die Debug-Prüfung (-d, ohne wirkliche Rotation) war anschließend fehlerfrei.
Test von der NGINX-LXC-Konsole mit einer tatsächlich bei NPM eingerichteten HTTPS-Domain. Die Domain muss korrekt geschrieben sein und ein gültiges Zertifikat besitzen. –resolve verbindet lokal über Port 443, erhält aber den für SNI notwendigen Hostnamen.
DOMAIN="joplin.DEINE-DOMAIN.de" # Platzhalter ersetzen BLOCKED_IP=$(awk ' $1 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\/32$/ { sub(/\/32$/, "", $1) print $1 exit } ' /root/data/nginx/ipv64-geo.conf) echo "Gesperrte Test-IP: $BLOCKED_IP" curl --noproxy '*' -sS --max-time 15 \ --resolve "$DOMAIN:443:127.0.0.1" \ -H 'X-Real-IP: 203.0.113.10' \ -o /dev/null -w 'HTTP: %{http_code}\n' "https://$DOMAIN/" curl --noproxy '*' -sS --max-time 15 \ --resolve "$DOMAIN:443:127.0.0.1" \ -H "X-Real-IP: $BLOCKED_IP" \ -o /dev/null -w 'HTTP: %{http_code}\n' "https://$DOMAIN/"
Beobachtetes Ergebnis: Erlaubte IP → 302 (normale Joplin-Weiterleitung); gesperrte IP 1.14.192.95 → 403. Die Prüfung der echten Sperrfunktion war damit erfolgreich.
Zwischenfall beim ersten Test: Mit einem nicht passenden Hostnamen meldete curl tlsv1 unrecognized name / HTTP 000 (SNI-Problem). Nach Verwendung der tatsächlichen Joplin-Domain funktionierte der Test.
NPM enthält automatisch konfigurierte real_ip_header X-Real-IP- und set_real_ip_from-Einträge für private Netze sowie Cloudflare-/CloudFront-Netze. Diese IP-Bereiche sind vertrauenswürdige Proxys, keine Blacklist.
Die Logs zeigten lokale Clients (z. B. 192.168.10.x) und externe öffentliche Client-IP-Adressen. Das ist ein gutes Indiz für eine funktionierende IP-Erfassung, aber keine vollständige Sicherheitsgarantie.
Test vom MacBook über Mobilfunk-Hotspot, nicht aus dem LAN:
DOMAIN="joplin.DEINE-DOMAIN.de" # Platzhalter ersetzen curl -sS -o /dev/null -w 'HTTP: %{http_code}\n' "https://$DOMAIN/" curl -sS -o /dev/null \ -H 'X-Real-IP: 1.14.192.95' \ -w 'HTTP: %{http_code}\n' "https://$DOMAIN/"
Beobachtet: 302 ohne Header und 302 mit gefälschtem Header. Die Anfrage wurde über diesen externen Weg also nicht fälschlich als die blockierte IP eingestuft.
Grenze des Tests: Gleiche Statuscodes beweisen noch nicht, dass sämtliche Proxy-/Header-Konstellationen gegen Spoofing abgesichert sind. Werden später Cloudflare, ein weiterer Reverse Proxy oder besondere Portweiterleitungen eingesetzt, real_ip erneut prüfen.
# Anzahl gesperrter IPs/Netze wc -l /root/data/nginx/ipv64-geo.conf # Letzte Updater-Meldungen tail -n 50 /var/log/ipv64-nginx-update.log # Manueller Update-Lauf /usr/local/sbin/update-ipv64-nginx.sh # Cron und NPM prüfen systemctl is-active cron docker ps --filter name=nginx-app-1 docker exec nginx-app-1 nginx -t # Letzte Zugriffe (ggf. öffentliche IPs vor Weitergabe anonymisieren) tail -n 30 /root/data/logs/proxy-host-1_access.log
Zur Erinnerung: Die Blockliste schützt vor bekannten Quell-IPs, ersetzt aber keine Authentifizierung, Aktualisierung der Dienste, Zugriffsbeschränkungen für Verwaltungsoberflächen oder eine Firewall. Je nach IP-Listenqualität sind Fehlalarme möglich.
Falls legitime Nutzer unerwartet HTTP 403 erhalten, lässt sich die globale Sperrregel ohne Löschen der heruntergeladenen Liste deaktivieren:
mv /root/data/nginx/custom/server_proxy.conf \ /root/data/nginx/custom/server_proxy.conf.disabled docker exec nginx-app-1 nginx -t && \ docker exec nginx-app-1 nginx -s reload
Wieder aktivieren:
mv /root/data/nginx/custom/server_proxy.conf.disabled \ /root/data/nginx/custom/server_proxy.conf docker exec nginx-app-1 nginx -t && \ docker exec nginx-app-1 nginx -s reload
Achtung: Der Cronjob aktualisiert die IP-Datei auch bei deaktivierter Sperrregel weiter, solange er nicht separat deaktiviert wird.
| Pfad im LXC-Host | Zweck |
|---|---|
/root/data/nginx/ipv64-geo.conf | Aktive IP-Liste in NGINX-geo-Syntax |
/root/data/nginx/ipv64-geo.conf.bak | Sicherung der Liste vor dem letzten Änderungslauf |
/root/data/nginx/custom/http_top.conf | Globale geo-Definition |
/root/data/nginx/custom/server_proxy.conf | HTTP-403-Regel für Proxy Hosts |
/usr/local/sbin/update-ipv64-nginx.sh | Update-, Validierungs- und Rollback-Skript |
/etc/cron.d/ipv64-nginx | Update-Zeitplan |
/etc/logrotate.d/ipv64-nginx | Logrotation |
/var/log/ipv64-nginx-update.log | Update-Protokoll |
/root/ca-quarantaene/digicert_global_root_g2.crt | Gesicherte defekte CA-Datei |
/root/data/logs/proxy-host-*_access.log | Zugriffsprotokolle der Proxy Hosts |
/root/data/nginx/custom/ bleiben durch das persistente Docker-Volume erhalten; nach NPM-Upgrades trotzdem nginx -t und den 403-Test wiederholen./root/letsencrypt.Die IPv64/blocklist.de-IP-Liste ist im Docker-basierten NGINX Proxy Manager erfolgreich eingebunden. Ein gesperrter Testclient erhielt HTTP 403, ein erlaubter Client die normale HTTP-302-Antwort. Der Updater läuft manuell korrekt, Cron und Logrotate sind eingerichtet. Die Konfiguration gilt global für NPM-Proxy-Hosts; andere Dienste und die NPM-Adminoberfläche sind dadurch nicht automatisch abgedeckt.