Skip to content

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/login strenger) 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::vhost ist ein ausgereifter Puppet-Type mit guter Doku.
  • Großes Modul-Ökosystem (mod_security, mod_maxminddb etc.).

Schwächen:

  • Kein Per-Route-Rate-Limiting an Bord. mod_ratelimit ist ein Bandbreiten-Limiter; mod_qos und mod_security sind für den Zweck überdimensioniert.
  • Reverse-Proxy-Konfiguration verboser als bei Nginx.
  • Höherer Speicher- und Prozess-Overhead, auch mit mpm_event bei vielen idle Connections.
  • Teils schlecht gepflegte Module (mod_qos, ältere GeoIP-Module).

Nginx

Stärken:

  • limit_req_zone und limit_conn_zone für Per-Route-Limits direkt im HTTP-Core.
  • Reverse-Proxy über proxy_pass/fastcgi_pass als 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/nginx von 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 in try_files übersetzt werden.

Verteilung der Security-Anforderungen

Wo welche Anforderung am sinnvollsten landet, hängt davon ab, was der Webserver bordeigen mitbringt:

AnforderungApache2Nginx
IP-Bans bei 401/403/500-Flutwebserver-unabhängigwebserver-unabhängig
Geoblocking (DACH)mod_maxminddb + Require exprngx_http_geoip2_module + map
Per-Route-Rate-Limitingkein sauberes Bordmittellimit_req_zone nativ

Bewertung im Kontext der Anforderungen

KriteriumApache2Nginx
Per-User-Isolation (FPM-Pool)OKOK
Reverse-Proxy zu ContainerOKbesser
Multi-VHost-Templating per PuppetbesserOK
Per-Route-Rate-Limiting bordeigenunzureichendsehr gut
GeoblockingOKOK
Footprint bei vielen Mandantenmittelgering

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:

SchichtKomponenteAufgabe
HostFail2Ban (nftables)Reaktive IP-Bans bei statuscode-basiertem Missbrauch (401/403/500-Floods)
WebhostNginxPräventives Geoblocking (DACH) und Per-Route-Rate-Limiting
ApplikationApp-CodeFachliche 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:

ModusBeschreibung
nativeApp läuft als PHP-FPM-Pool unter einem System-User. Nginx → fastcgi_pass → Unix-Socket.
containerApp läuft in einem Docker-Container mit HTTP-Port. Nginx → proxy_passlocalhost:<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:

  1. 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).
  2. Certbot legt das Ergebnis unter /etc/letsencrypt/live/<domain>/{fullchain,privkey}.pem ab.
  3. Der vom Certbot-Paket mitgelieferte systemd-Timer prüft die Restlaufzeit und erneuert vor Ablauf.
  4. 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:

bash
# /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:

ZoneSchlüsselVerwendung
general$binary_remote_addrStandard-Limit für alle Routen
api$binary_remote_addrStrengeres Limit für REST-Endpoints
login$binary_remote_addrStriktestes 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:

JailAuslöserWirkung
<app>-authHäufung von 401-AntwortenIP wird für die definierte Bantime auf HTTP/HTTPS-Ports gesperrt
<app>-4xx-floodHäufung beliebiger 4xx-Antwortenwie 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_req drosselt — kurzes 429, keine persistente Sperre.
  • Fail2Ban bant — persistente IP-Sperre per nftables.

Sie laufen parallel, nicht alternativ.

Iteas Tools Integration Platform Version v1.0.21

Version: v1.0.21 Version: v1.0.21
Commit: 7a0e1c11
Deployed at: 2026-09-24T13:56:52Z