Nginx-Hardening auf HestiaCP: Layer-7-DDoS-Angriffe stoppen und Websites mit Caching beschleunigen

Praktische Anleitung zum Erstellen eines benutzerdefinierten Nginx-Templates auf HestiaCP, das Anwendungs-DDoS-Angriffe (Layer 7) abwehrt und gleichzeitig das Caching dynamischer Inhalte aktiviert, um Websites zu beschleunigen.

Nginx-Hardening auf HestiaCP: Layer-7-DDoS-Angriffe stoppen und Websites mit Caching beschleunigen

Ein „klassischer" DDoS-Angriff zielt darauf ab, die Netzwerkbandbreite oder die Verbindungen des Servers zu sättigen (sogenannte Angriffe auf Layer 3/4, also auf Netzwerk- und Transportebene). Es gibt jedoch eine weitere Kategorie, die oft heimtückischer ist, weil sie viel weniger Bandbreite benötigt, um Schaden anzurichten: Angriffe auf Layer 7, der Anwendungsebene. Hier ist nicht die Netzwerkverbindung selbst das Ziel, sondern der Webserver und die darauf laufenden Anwendungen: Tausende HTTP-Anfragen, die der Form nach „legitim" wirken, aber so schnell gesendet werden, dass sie CPU, Arbeitsspeicher und Datenbankverbindungen erschöpfen und so selbst einen Server in die Knie zwingen, dessen Internetanbindung alles andere als ausgelastet ist.

Wer seine eigenen Websites mit HestiaCP betreibt, dem Open-Source-Server-Kontrollpanel auf Nginx-Basis (allein oder als Reverse-Proxy vor Apache), verfügt über ein mächtiges, aber wenig bekanntes Werkzeug, um sich gegen diese Art von Angriff zu verteidigen: benutzerdefinierte Nginx-Templates. In dieser Anleitung zeigen wir, wie man ein maßgeschneidertes Template erstellt, das Regeln zur Abwehr von Anwendungs-DDoS hinzufügt, und gleichzeitig, wie man den Nginx-Cache nutzt, um dynamische Inhalte (PHP, Node.js-Anwendungen, Docker-Container) zu beschleunigen und die Serverlast weiter zu reduzieren – auch wenn gerade kein Angriff läuft.

Wie Nginx-Templates in HestiaCP funktionieren

HestiaCP schreibt die Konfiguration für jede Website nicht „von Hand": Für jede Domain wendet es ein Template an, also eine Vorlagendatei mit Platzhaltern, die das Panel bei jeder Erstellung oder jedem „Rebuild" der Domain durch die tatsächlichen Werte (Domainname, Dateipfad, Systembenutzer usw.) ersetzt. Die Standard-Templates befinden sich unter:

/usr/local/hestia/data/templates/web/nginx/          # Nginx als Reverse-Proxy vor Apache
/usr/local/hestia/data/templates/web/nginx/php-fpm/  # Nginx mit PHP-FPM (ohne Apache)

Jedes Template besteht eigentlich aus drei Dateien mit demselben Namen, aber unterschiedlichen Endungen: .tpl (HTTP-Konfiguration), .stpl (HTTPS-Konfiguration) und .sh (ein optionales Skript, das bei der Erstellung ausgeführt wird). Das Template „default" besteht beispielsweise aus default.tpl, default.stpl und default.sh.

Goldene Regel: Die Original-Templates niemals bearbeiten. Bei jedem HestiaCP-Update werden die Standarddateien überschrieben, und jede direkt an ihnen vorgenommene Anpassung ginge verloren. Die richtige Vorgehensweise ist, sie unter einem neuen Namen zu kopieren und nur an der Kopie zu arbeiten:

cd /usr/local/hestia/data/templates/web/nginx/php-fpm/
cp default.tpl hardened.tpl
cp default.stpl hardened.stpl
cp default.sh hardened.sh 2>/dev/null || true

(das Beispiel verwendet die Templates für Nginx + PHP-FPM; läuft die Website hinter Nginx + Apache, gilt dasselbe Verfahren im Ordner nginx/ statt nginx/php-fpm/).

In den kopierten Dateien finden sich Platzhalter wie %domain%, %docroot%, %user%, %home% und %backend_lsnr%: HestiaCP ersetzt sie automatisch durch die korrekten Werte für jede Domain, die das Template verwendet, daher müssen sie unverändert bleiben, während man neue Direktiven um sie herum hinzufügt.

Erste Hardening-Regel: Anfragerate begrenzen (Rate Limiting)

Das Modul ngx_http_limit_req_module, standardmäßig in Nginx enthalten, erlaubt es, Anfragen pro IP-Adresse (oder jeder anderen Variable) zu zählen und diejenigen abzulehnen, die einen Schwellenwert überschreiten, wobei ein HTTP-Fehler 429 Too Many Requests zurückgegeben wird. Es ist die einfachste und wirksamste Verteidigung gegen Anwendungs-Floods: Ein Bot, der Hunderte von Anfragen pro Sekunde auf die Login-Seite oder die interne Suchmaschine der Website abschießt, wird blockiert, bevor er Ressourcen auf dem PHP-Backend verbrauchen kann.

Die Zähl-Zone muss nur einmal, global, im http-Block der Haupt-Nginx-Konfiguration deklariert werden (/etc/nginx/nginx.conf oder einer separaten Datei, die von dort eingebunden wird, zum Beispiel /etc/nginx/conf.d/ddos-mitigation.conf):

# 10 MB gemeinsamer Speicher reichen aus, um etwa 160.000 IPs zu verfolgen
limit_req_zone $binary_remote_addr zone=req_zone_general:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=req_zone_login:10m rate=5r/m;

limit_conn_zone $binary_remote_addr zone=conn_zone:10m;

Hier werden zwei getrennte Rate-Limiting-Zonen angelegt: eine allgemeine (req_zone_general, 20 Anfragen pro Sekunde pro IP) für die gesamte Website und eine deutlich strengere (req_zone_login, 5 Anfragen pro Minute), die nur auf die Login-Seite angewendet wird – ein typisches Ziel sowohl für Brute-Force- als auch für DDoS-Angriffe. Zusätzlich wurde eine Zone für limit_conn hinzugefügt, die nicht die Frequenz, sondern die Anzahl der gleichzeitigen Verbindungen pro IP begrenzt – nützlich gegen Versuche, den Verbindungspool des Backends zu erschöpfen.

Im benutzerdefinierten Template (hardened.tpl und hardened.stpl) werden innerhalb des server { ... }-Blocks die eben definierten Zonen angewendet:

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;
    root        %docroot%;

    # Begrenzt gleichzeitige Verbindungen pro einzelner IP auf der gesamten Website
    limit_conn conn_zone 20;

    location / {
        # Allgemeines Rate Limit: erlaubt kurze Spitzen (Burst), ohne
        # einen Nutzer sofort zu blockieren, der schnell klickt
        limit_req zone=req_zone_general burst=40 nodelay;
        limit_req_status 429;
        try_files $uri $uri/ /index.php?$args;
    }

    location = /wp-login.php {
        limit_req zone=req_zone_login burst=5 nodelay;
        limit_req_status 429;
        include /home/%user%/conf/web/%domain%/nginx.hosting.conf*;
    }

    # ... der Rest des Templates (PHP-FPM-Handling, Fehlerseiten usw.)
    # bleibt wie im ursprünglichen „default"-Template.
}

Eine kurze Erklärung der verwendeten Parameter, für alle, die nicht täglich mit Nginx arbeiten:

  • rate=20r/s: die erlaubte „Dauergeschwindigkeit", hier 20 Anfragen pro Sekunde je IP.
  • burst=40: eine Art „Puffer" zusätzlicher Anfragen, den eine IP ansammeln und auf einmal aufbrauchen kann, damit ein echter Nutzer, der schnell zwischen mehreren Seiten navigiert, nicht benachteiligt wird (jedes Bild, jede CSS- oder JS-Datei einer Seite erzeugt eine eigene Anfrage).
  • nodelay: ohne diese Option würde Nginx Anfragen, die den Schwellenwert überschreiten, in eine Warteschlange stellen (verzögern); mit nodelay werden Anfragen jenseits des Bursts stattdessen sofort mit einem 429-Fehler abgelehnt, wodurch sich kein Stau wartender Verbindungen bildet, der selbst Ressourcen verbrauchen würde.
  • limit_req_status 429: legt explizit den zurückgegebenen Fehlercode fest (standardmäßig wäre es 503, aber 429 ist semantisch korrekter und wird von CDNs und Monitoring-Tools besser erkannt).

Zweite Hardening-Regel: langsame Verbindungen abschneiden (Anti-Slowloris)

Eine weitere Familie von Layer-7-Angriffen, bekannt als Slowloris, sendet nicht viele, sondern sehr wenige Anfragen, hält aber jede Verbindung so lange wie möglich offen, indem sie Daten in winzigen Häppchen sendet: So erschöpft sie die maximale Anzahl gleichzeitiger Verbindungen, die der Server verarbeiten kann, ohne jemals hohen Traffic erzeugen zu müssen. Die Abhilfe besteht darin, die maximale Zeit zu verkürzen, die Nginx einem Client für den Abschluss seiner Anfrage einräumt:

client_body_timeout   10s;
client_header_timeout 10s;
keepalive_timeout     15s;
send_timeout          10s;

# Begrenzt auch die Größen, um ungewöhnliche Uploads als Flood-Vektor zu vermeiden
client_max_body_size  10m;
large_client_header_buffers 4 8k;

Diese Werte (deutlich niedriger als die 60 Sekunden standardmäßig) gehören ebenfalls in den server { ... }-Block des benutzerdefinierten Templates, oder in den globalen http { ... }-Block, wenn sie für alle Domains des Servers gelten sollen. Vorsicht beim Übertreiben: zu aggressive Zeiten können legitime große Datei-Uploads über langsame oder mobile Verbindungen unterbrechen; 10 Sekunden sind für gewöhnliche Websites in der Regel ein guter Kompromiss.

Caching dynamischer Inhalte optimieren

Anwendungs-DDoS abzuwehren bedeutet nicht nur, „die Bösen zu blockieren": Es bedeutet auch, die Beantwortung einer Anfrage sehr billig zu machen, damit selbst ein hohes Trafficvolumen (bösartig oder einfach ein plötzlicher Popularitätsschub) den Server nicht in die Knie zwingt. Genau dafür ist der Nginx-Cache für dynamische Inhalte gedacht: Von PHP (oder einer Node.js-App hinter einem Proxy) erzeugte Seiten werden vorübergehend auf der Festplatte gespeichert und bei nachfolgenden Anfragen identisch ausgeliefert, ohne dass der Anwendungscode und die Datenbankabfragen jedes Mal erneut ausgeführt werden müssen.

HestiaCP unterscheidet hier zwei Fälle, abhängig vom für die Domain gewählten Stack:

Nginx + PHP-FPM: FastCGI-Cache

Für Domains, die Nginx mit PHP-FPM nutzen (ohne Apache), verwendet man den FastCGI-Cache. In der globalen Konfiguration (derselben Datei /etc/nginx/conf.d/ddos-mitigation.conf wie oben, oder nginx.conf) wird der Cache definiert:

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=FASTCGICACHE:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

Im benutzerdefinierten Template muss im Block, der PHP-FPM behandelt, die Aktivierungs-Include-Zeile nach der bereits im Original-Template vorhandenen Zeile include /etc/nginx/fastcgi_params; hinzugefügt werden:

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass   %backend_lsnr%;
    fastcgi_index  index.php;
    include        /etc/nginx/fastcgi_params;

    # Aktiviert den Cache für diese Domain (die Datei wird von HestiaCP erzeugt,
    # wenn der FastCGI-Cache über das Panel für die Domain aktiviert wird)
    include /home/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*;

    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Allerdings sollten nicht alle Seiten zwischengespeichert werden: Ein Warenkorb, ein Bereich nach dem Login oder ein Endpunkt, der für jeden Nutzer unterschiedliche Inhalte erzeugt, dürfen einem Besucher nicht die für einen anderen gespeicherte Seite ausliefern. Diese Pfade schließt man aus, indem man die Variable $no_cache setzt:

set $no_cache 0;
if ($request_uri ~* "/(warenkorb|checkout|wp-admin|my-account)") {
    set $no_cache 1;
}
if ($http_cookie ~* "wordpress_logged_in") {
    set $no_cache 1;
}

Sobald das Template erstellt ist, wird der FastCGI-Cache für eine bestimmte Domain über die Bearbeitungsseite der Domain im HestiaCP-Panel aktiviert (im Bereich für das Nginx-/Backend-Template), indem man das neu erstellte Template auswählt. Um den Cache jederzeit zu leeren (etwa nach einem Website-Update), reicht die Schaltfläche „Purge Nginx Cache" auf derselben Seite, oder auf der Kommandozeile:

v-purge-nginx-cache benutzername domain.tld

Nginx + Apache: Proxy-Cache

Nutzt eine Domain hingegen Nginx als Reverse-Proxy vor Apache (eine sehr verbreitete HestiaCP-Konfiguration, um .htaccess und mod_php zu unterstützen), steht der FastCGI-Cache nicht zur Verfügung: Stattdessen nutzt man den Proxy-Cache von Nginx, der konzeptionell identisch funktioniert, sich aber auf den weitergeleiteten Traffic anwendet (er funktioniert auch gut für Node.js-Anwendungen oder Docker-Container hinter dem Proxy). Benutzerdefinierte Templates für diesen Zweck folgen einer eigenen Namenskonvention: caching-eigenername.tpl, caching-eigenername.stpl und caching-eigenername.sh, immer erstellt durch Kopieren des passenden Ausgangs-Templates, das HestiaCP im Ordner nginx/ (nicht nginx/php-fpm/) bereitstellt, und anschließend als eigenes „Caching-Template" über die Domain-Seite auswählbar.

Eine Warnung zu WebSockets

Sowohl das Standard-Template als auch die Proxy-Cache-Templates enthalten standardmäßig die Direktive proxy_hide_header Upgrade;, die ursprünglich hinzugefügt wurde, um ein Kompatibilitätsproblem mit Safari zu umgehen. Diese Zeile blockiert jedoch auch WebSocket-Verbindungen (verwendet etwa von Echtzeit-Chats, Browser-Push-Benachrichtigungen oder manchen Admin-Panels): Benötigt die gehostete Anwendung diese, muss sie aus dem benutzerdefinierten Template entfernt werden.

Das Template anwenden und seine Funktion prüfen

Nach dem Speichern der Änderungen an den .tpl/.stpl-Dateien muss das neue Template für die gewünschte Domain ausgewählt werden: Im HestiaCP-Panel öffnet man die Bearbeitungsseite der Domain (Web > Domainname > Bearbeiten), klappt die erweiterten Optionen auf und wählt das neu erstellte Template (hardened, in unserem Beispiel) aus dem Dropdown-Menü für das Nginx-/Proxy-Template. Alternativ kann man über die Kommandozeile den Rebuild der Konfiguration für alle Domains eines Benutzers erzwingen mit:

v-rebuild-user benutzername

Bevor man alles in Produktion schaltet, ist es ratsam, die Syntax zu validieren und Nginx manuell neu zu laden:

nginx -t && systemctl reload nginx

Um zu prüfen, ob das Rate Limiting tatsächlich funktioniert, kann man einen kleinen Strom wiederholter Anfragen von der eigenen Maschine simulieren (nicht zu verwechseln mit einem echten Lasttest, der nur vorsichtig und ausschließlich gegen Systeme durchgeführt werden sollte, deren Eigentümer man ist):

for i in $(seq 1 80); do
  curl -o /dev/null -s -w "%{http_code}\n" https://domain.tld/
done | sort | uniq -c

Ist die Konfiguration korrekt, liefern die ersten Anfragen (innerhalb des konfigurierten burst) 200 zurück, während nachfolgende, nach Überschreiten des Schwellenwerts, mit 429 antworten. Das ist das Zeichen, dass die Rate-Limiting-Zone ihre Arbeit tut.

Ein paar Warnhinweise, bevor alles in Produktion geht

IP-basiertes Rate Limiting hat eine inhärente Einschränkung, die man bedenken sollte: Viele Nutzer, die aus Firmennetzen, Universitäten oder bestimmten Mobilfunknetzen surfen, teilen sich über NAT dieselbe öffentliche IP, und zu niedrige Schwellenwerte riskieren, legitime Nutzer zusammen mit eventuellen Bots zu blockieren. Deshalb empfiehlt es sich, mit vorsichtigen Werten zu starten (wie den 20 r/s im Beispiel), die 429-Fehlerprotokolle einige Tage zu beobachten und die Schwellenwerte erst danach bei Bedarf zu verschärfen. Ebenfalls empfehlenswert ist es, die IP-Adressen der eigenen Monitoring-Tools (Uptime-Checker, CDNs, eventuelle Backup-Dienste) mittels einer geo-Direktive oder eines einfachen map-Blocks vor der limit_req-Zone vom Rate Limiting auszunehmen, um Fehlalarme zu vermeiden.

Schließlich stellt das in dieser Anleitung beschriebene Rate Limiting auf Anwendungsebene eine erste, wirksame Verteidigungslinie gegen Layer-7-DDoS dar, ersetzt aber keinen dedizierten Schutz auf Netzwerkebene (Layer 3/4) gegen sehr große volumetrische Angriffe: Dafür bleiben vorgeschaltete Mitigationsdienste unerlässlich, etwa ein CDN mit Anti-DDoS-Schutz oder die Filterung, die manche Hosting-Anbieter und Konnektivitätsprovider anbieten.

Offizielle Quellen

Teilen

Was ist Ihre Reaktion?

Gefällt mir Gefällt mir 0
Gefällt mir nicht Gefällt mir nicht 0
Liebe Liebe 0
Lustig Lustig 0
Wütend Wütend 0
Traurig Traurig 0
Wow Wow 0