====== NGINX Proxy Manager – IPv64-IP-Blacklist ======
**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.
===== 1. Ergebnis / Status =====
^ 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'').
===== 2. Voraussetzungen / Docker-Umgebung =====
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.
===== 3. CA-Zertifikatsfehler beim Download (einmalige Reparatur) =====
**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:
* Mit einem einzelnen vertrauenswürdigen CA-Zertifikat funktionierte HTTPS:
curl -q -v -I --cacert /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt https://ipv64.net/
* Die Prüfung des gesamten Bundles scheiterte mit ASN.1-/PEM-Fehlern:
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /usr/share/ca-certificates/mozilla/ISRG_Root_X1.crt
* Unter ''/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.
===== 4. IP-Quelle und erste Aufbereitung =====
**Quelle:** [[https://ipv64.net/blocklists/ipv64_blocklist_blocklistde_all.txt|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;
===== 5. NGINX Proxy Manager – globale Konfiguration =====
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.
===== 6. Automatisches Update mit Prüfung und Rollback =====
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.
===== 7. Cron: Aktualisierung alle vier Stunden =====
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.
===== 8. Logrotate =====
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.
===== 9. Funktionstest: HTTP 403 =====
**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.
===== 10. Test der Real-IP-Verarbeitung von außen =====
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.
===== 11. Wartung und Kontrolle =====
# 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.
===== 12. Sperre vorübergehend deaktivieren =====
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.
===== 13. Wichtige Pfade =====
^ 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 |
===== 14. Hinweise / offene Ergänzungen =====
* **Sperrstatistik:** Eine Übersicht über die Anzahl tatsächlicher 403-Sperren durch diese Liste ist noch nicht eingerichtet. Ein HTTP 403 kann auch aus anderen Gründen entstehen.
* **Eigene Allow-/Denylist:** Nicht eingerichtet; bei Bedarf getrennt planen und mit klarer Priorität zu den IPv64-Einträgen einbauen.
* **Cron-Erstlauf:** Der Updater wurde manuell erfolgreich getestet. Die erste selbstständige Cron-Ausführung zum nächsten geplanten Zeitpunkt ist anhand der Logdatei zu kontrollieren.
* **Fehlerfall des Skripts:** Rollback-Logik vorhanden, aber nicht absichtlich unter Störung getestet.
* **NPM-Updates:** Dateien unter ''/root/data/nginx/custom/'' bleiben durch das persistente Docker-Volume erhalten; nach NPM-Upgrades trotzdem ''nginx -t'' und den 403-Test wiederholen.
* **HTTPS-/Strato-Zertifikate:** Die Reparatur betraf den CA-Truststore des LXC-Systems, nicht die NPM-Let's-Encrypt-Konfiguration unter ''/root/letsencrypt''.
===== 15. Kurzfazit =====
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.