Webhost-Konfiguration
Der Webhost ist die Schicht, auf der die Applikation am Server ausgeführt wird — Webserver, FPM-Pool und alles, was die Request-Verarbeitung dranhängt. Aktuell ist das ein nackter Apache2, der Anfragen ohne Filterung an die App durchreicht. Geoblocking und Per-Route-Rate-Limiting fehlen, und die Konfiguration ist für ein automatisiertes Multi-Mandanten-Setup nicht ausgelegt.
Anforderungen
Aus dem Beispiel-Szenario ergeben sich die folgenden Anforderungen:
- Apps müssen nativ oder im Docker-Container laufen können.
- Pro Webhost existiert ein dedizierter Linux-System-User (Konvention:
f01_<app>). - Der Webhost wird vollständig von Puppet aufgesetzt — kein Admin-Handgriff am Server.
- Mehrere Webhosts laufen am selben System nebeneinander.
- Geoblocking und Rate-Limiting können entweder im Webhost oder davor angesiedelt werden — beide Varianten kommen in Frage. Rate-Limiting muss pro Route greifen (z.B.
auth/loginstrenger) und reaktiv auf HTTP-Statuscodes (401-Bruteforce,5xx-Floods).
Damit stellt sich die Frage, ob Apache2 dafür weiter das richtige Werkzeug ist. Unten ein Vergleich mit Nginx — erstellt mit Claude Opus 4.7 und anschließend redaktionell überarbeitet.
Apache2 vs. Nginx
Apache2
Stärken:
puppetlabs/apache::vhostist ein ausgereifter Puppet-Type mit guter Doku.- Großes Modul-Ökosystem (
mod_security,mod_maxminddbetc.).
Schwächen:
- Kein Per-Route-Rate-Limiting an Bord.
mod_ratelimitist ein Bandbreiten-Limiter;mod_qosundmod_securitysind für den Zweck überdimensioniert. - Reverse-Proxy-Konfiguration verboser als bei Nginx.
- Höherer Speicher- und Prozess-Overhead, auch mit
mpm_eventbei vielen idle Connections. - Teils schlecht gepflegte Module (
mod_qos, ältere GeoIP-Module).
Nginx
Stärken:
limit_req_zoneundlimit_conn_zonefür Per-Route-Limits direkt im HTTP-Core.- Reverse-Proxy über
proxy_pass/fastcgi_passals Kernfunktion. - Event-basierte Architektur, geringerer Speicher-Footprint als prefork-Apache.
- Kein
.htaccess-Mechanismus — die VHost-Konfiguration ist die einzige Quelle. - Ein File pro VHost passt zum Multi-Mandanten-Workflow.
Schwächen:
- Das Puppet-Modul (
puppet/nginxvon Vox Pupuli) ist weniger feingranular als das Apache-Pendant. Für VHosts kommen daher Custom-Templates zum Einsatz. - Bestehende
.htaccess-Regeln (Yii2-URL-Rewrites) müssen einmalig intry_filesübersetzt werden.
Verteilung der Security-Anforderungen
Wo welche Anforderung am sinnvollsten landet, hängt davon ab, was der Webserver bordeigen mitbringt:
| Anforderung | Apache2 | Nginx |
|---|---|---|
| IP-Bans bei 401/403/500-Flut | webserver-unabhängig | webserver-unabhängig |
| Geoblocking (DACH) | mod_maxminddb + Require expr | ngx_http_geoip2_module + map |
| Per-Route-Rate-Limiting | kein sauberes Bordmittel | limit_req_zone nativ |
Bewertung im Kontext der Anforderungen
| Kriterium | Apache2 | Nginx |
|---|---|---|
| Per-User-Isolation (FPM-Pool) | OK | OK |
| Reverse-Proxy zu Container | OK | besser |
| Multi-VHost-Templating per Puppet | besser | OK |
| Per-Route-Rate-Limiting bordeigen | unzureichend | sehr gut |
| Geoblocking | OK | OK |
| Footprint bei vielen Mandanten | mittel | gering |
Entscheidung
Nginx. Ausschlaggebend ist das fehlende Per-Route-Rate-Limiting auf Apache-Seite. Geoblocking und Rate-Limiting laufen damit direkt im Webhost; die statuscode-basierte IP-Sperre übernimmt Fail2Ban darüber.
Konfiguration
Die folgenden Abschnitte beschreiben, was Puppet auf einem Webhost ausrollt. Alle Werte sind über Smart Class Parameters im Foreman steuerbar — siehe Foreman.
Verteidigung in Schichten
Die Sicherheits-Anforderungen sind auf drei Ebenen aufgeteilt:
| Schicht | Komponente | Aufgabe |
|---|---|---|
| Host | Fail2Ban (nftables) | Reaktive IP-Bans bei statuscode-basiertem Missbrauch (401/403/500-Floods) |
| Webhost | Nginx | Präventives Geoblocking (DACH) und Per-Route-Rate-Limiting |
| Applikation | App-Code | Fachliche Authentifizierung und Autorisierung |
limit_req in Nginx antwortet bei Überschreitung mit 429. Fail2Ban liest die Access-Logs und bant Clients, die solche 429er (oder andere 4xx) gehäuft produzieren.
Layer-Modell
Ein eingehender HTTPS-Request durchläuft vier hintereinandergeschaltete Schichten. Jede kann den Request frühzeitig verwerfen:
eingehender HTTPS-Request
│
▼
┌───────────────────────────────────────────┐
│ Layer 1 Host / Netzwerk │
│ ─────────────────────── │
│ Fail2Ban (nftables) │
│ IP-Bans bei 401- und 4xx-Floods │
└─────────────────────┬─────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ Layer 2 Webserver — Nginx │
│ ─────────────────────── │
│ TLS-Termination (Certbot / ACME) │
│ Geoblocking (GeoIP2, DACH-Whitelist) │
│ Rate-Limiting (general / api / login) │
└─────────────────────┬─────────────────────┘
│ fastcgi_pass / proxy_pass
▼
┌───────────────────────────────────────────┐
│ Layer 3 Applikation — PHP-FPM │
│ ─────────────────────── │
│ Ein FPM-Pool pro App-Instanz │
│ System-User: f01_<app> │
│ Yii2-App-Code (Auth, Business-Logic) │
└─────────────────────┬─────────────────────┘
│ localhost
▼
┌───────────────────────────────────────────┐
│ Layer 4 Daten — MariaDB │
│ ─────────────────────── │
│ App-Login (localhost, App-DB) │
│ m01_edbs (Remote, REQUIRE SSL) │
└───────────────────────────────────────────┘
── Querschnitt ─────────────────────────────
Provisioning: Puppet (Environment: iteasapps)
Secrets: Vault (Hiera-Backend)Die drei Sicherheits-Schichten aus der Tabelle entsprechen Layer 1–3. Layer 4 ist die Daten-Ebene; bei der EDBS-Anbindung kommt dort zusätzlich REQUIRE SSL ins Spiel — siehe Applikation → Eingehender Remote-Login.
Betriebsmodi
Pro Applikation wird einer von zwei Modi gewählt:
| Modus | Beschreibung |
|---|---|
native | App läuft als PHP-FPM-Pool unter einem System-User. Nginx → fastcgi_pass → Unix-Socket. |
container | App läuft in einem Docker-Container mit HTTP-Port. Nginx → proxy_pass → localhost:<port>. |
Der Defined Type iteasapps::webhost kapselt beide Modi hinter einem mode-Parameter. Die restliche Konfiguration (System-User, VHost, Security-Regeln) ist identisch.
System-User
Pro Webhost gibt es einen eigenen Linux-System-User. Naming-Konvention:
f01_<application-name>Beispiel: f01_iscan. Die Kundenkennung steckt im Hostnamen und nicht im User-Namen. Der konkrete User-Name ist über den Smart-Class-Parameter system_user überschreibbar.
PHP-FPM
Jeder System-User bekommt einen eigenen PHP-FPM-Pool, der unter genau diesem User läuft. FastCGI-Verbindungen kommen über einen Unix-Socket unter /run/php/<system_user>.sock rein.
Daraus folgt: Pools können nicht auf die Dateien anderer Pools zugreifen, und Pool-Parameter (pm.max_children, memory_limit etc.) sind pro Applikation einstellbar.
Nginx VHost
Pro App existiert ein eigener VHost. Inhalt:
- Referenzen auf das Certbot-TLS-Material unter
/etc/letsencrypt/live/<domain>/(siehe TLS-Material). - 301-Redirect von HTTP auf HTTPS, ausgenommen die ACME-Challenge unter
/.well-known/acme-challenge/. - Auswertung der globalen Geoblock-Map vor dem
root-Block; bei Mismatch antwortet Nginx mit HTTP 403. - Pro Route eine zugewiesene Rate-Limit-Zone (abhängig von der App-Klasse).
- Eine
.php-Location, die abhängig vom Modus an den FPM-Socket oder den Container-Port weiterreicht.
TLS-Material (Certbot)
Für TLS kommt Certbot mit der Let's-Encrypt-CA zum Einsatz. Ausstellung, Erneuerung und Verteilung laufen über ACME; Cert und Key bleiben außerhalb von Vault.
Ablauf:
- Puppet installiert Certbot über die Basis-Klasse und legt pro VHost eine ACME-Anfrage an (HTTP-01 oder DNS-01, abhängig von der Erreichbarkeit der Domain).
- Certbot legt das Ergebnis unter
/etc/letsencrypt/live/<domain>/{fullchain,privkey}.pemab. - Der vom Certbot-Paket mitgelieferte systemd-Timer prüft die Restlaufzeit und erneuert vor Ablauf.
- Nach einer erfolgreichen Erneuerung läuft ein Deploy-Hook, der Nginx neu lädt und das Material an die MariaDB weiterverteilt (siehe nächster Abschnitt).
Wiederverwendung für MariaDB
Da der Webhost zu genau einer Kundendomain gehört (z.B. goessl.iscan.at), nutzt MariaDB dasselbe Certbot-Zertifikat wie Nginx. Auf Port 3306 präsentiert MariaDB damit ein Zertifikat aus der Let's-Encrypt-Kette; gegenüber dem EDBS-Client entfällt damit der Aufwand für eine eigene CA.
MariaDB läuft als User mysql, Let's-Encrypt-Dateien sind defaultmäßig root-only. Der Renewal-Deploy-Hook kopiert das Material nach /etc/mysql/ssl/ und stößt FLUSH SSL an, damit MariaDB das neue Zertifikat ohne Service-Restart einliest.
Der Hook wird durch iteasapps::base::mariadb ausgerollt:
# /etc/letsencrypt/renewal-hooks/deploy/iteasapps-renewal.sh
#!/bin/bash
set -e
DOMAIN="$(basename "$(dirname "$RENEWED_LINEAGE")")"
SRC=/etc/letsencrypt/live/"$DOMAIN"
DST=/etc/mysql/ssl
install -m 0640 -o root -g mysql "$SRC/fullchain.pem" "$DST/server-cert.pem"
install -m 0640 -o root -g mysql "$SRC/privkey.pem" "$DST/server-key.pem"
systemctl reload nginx
mysql -e "FLUSH SSL"Initialer Lauf
Beim ersten Provisioning läuft der Hook nicht — Certbot triggert ihn nur bei Renewals. Puppet führt deshalb die initiale Kopie nach /etc/mysql/ssl/ selbst aus und stößt einmalig FLUSH SSL an, sobald das Zertifikat ausgestellt ist.
Geoblocking
Geoblocking läuft über ngx_http_geoip2_module und die MaxMind-GeoLite2-Country-Datenbank. geoipupdate hält die Datenbank aktuell.
Im HTTP-Kontext definiert Nginx eine globale map, die den ISO-Country-Code der Quell-IP auf eine Boolean abbildet. Der Server-Block wertet sie aus; bei 0 antwortet Nginx mit HTTP 403, der Request kommt nicht bis PHP-FPM.
Die erlaubten Länder pro App stehen im Smart-Class-Parameter allowed_countries. Default: ['AT', 'DE', 'CH'].
Vorgelagerter Reverse-Proxy
Sitzt ein Reverse-Proxy (z.B. Cloudflare) vor dem Webhost, sieht Nginx nur dessen IP. Die Country-Auswertung muss dann den entsprechenden Forwarded-Header (CF-IPCountry, X-Forwarded-For) heranziehen.
Per-Route Rate-Limiting
Drei Rate-Limit-Zonen, global im HTTP-Kontext definiert:
| Zone | Schlüssel | Verwendung |
|---|---|---|
general | $binary_remote_addr | Standard-Limit für alle Routen |
api | $binary_remote_addr | Strengeres Limit für REST-Endpoints |
login | $binary_remote_addr | Striktestes Limit für Auth-Endpoints |
Die location-Blöcke referenzieren die passende Zone per limit_req zone=<name> burst=<n> [nodelay];. Bei Überschreitung antwortet Nginx mit HTTP 429.
Burst- und Rate-Werte sowie die Zonen-Definition liegen in iteasapps::base::nginx und sind über Smart-Class-Parameter überschreibbar.
Fail2Ban
Fail2Ban läuft auf Host-Ebene und ist vom Webserver unabhängig. Es liest die Nginx-Access-Logs und sperrt IPs per nftables, sobald sich bestimmte Muster häufen.
Pro App-VHost werden zwei Jails ausgerollt:
| Jail | Auslöser | Wirkung |
|---|---|---|
<app>-auth | Häufung von 401-Antworten | IP wird für die definierte Bantime auf HTTP/HTTPS-Ports gesperrt |
<app>-4xx-flood | Häufung beliebiger 4xx-Antworten | wie oben |
Die nftables-Regel gilt hostweit, nicht pro App. Bruteforce- und 4xx-Flood-Muster sind selten App-spezifisch, daher ist der hostweite Ban gewollt.
limit_req vs. Fail2Ban
Beide Mechanismen sehen ähnlich aus, machen aber Unterschiedliches:
limit_reqdrosselt — kurzes 429, keine persistente Sperre.- Fail2Ban bant — persistente IP-Sperre per nftables.
Sie laufen parallel, nicht alternativ.
