Inhaltsverzeichnis

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:

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

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: 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

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.