NGINX WAF
Architektur-Uebersicht¶
Internet
-> [MikroTik CHR - NAT/Firewall]
-> [Nginx - GeoIP Filter (pro Dienst konfigurierbar)]
-> [CrowdSec - WAF + Community Blocklist]
-> [Authentik - SSO]
-> Dienst
Interne Dienste werden ueber internes DNS direkt auf den Nginx geroutet - ohne GeoIP und CrowdSec.
Dienste¶
| Dienst | Domain | Typ | GeoIP |
|---|---|---|---|
| Authentik | auth.domain.tld | SSO / Identity Provider | Kein GeoIP |
| Freescout | helpdesk.domain.tld | Helpdesk | DACH |
| Guacamole | remote.domain.tld | Remote Desktop | CH only |
| Snipe-IT | assets.domain.tld | Asset Management | DACH |
| OnlyOffice | office.domain.tld | Office Suite | DACH |
| Opnform | forms.domain.tld | Formulare | DACH |
Wichtig: Authentik als OAuth/SSO Provider darf keinen GeoIP Filter haben. Interne OAuth Callbacks haben keinen Country Code und werden sonst geblockt - was zu Endlosschleifen beim Login fuehrt.
Schicht 1 - GeoIP Filter¶
Installation¶
# GeoIP2 Nginx Modul
apt install libnginx-mod-http-geoip2
# geoipupdate (manuell, da nicht im Trixie Repo)
wget https://github.com/maxmind/geoipupdate/releases/download/v7.1.1/geoipupdate_7.1.1_linux_amd64.deb
dpkg -i geoipupdate_7.1.1_linux_amd64.deb
MaxMind Konfiguration¶
# /etc/GeoIP.conf
AccountID DEINE_ACCOUNT_ID
LicenseKey DEIN_LICENSE_KEY
EditionIDs GeoLite2-Country
DatabaseDirectory /usr/share/GeoIP
# DB herunterladen
mkdir -p /usr/share/GeoIP
geoipupdate -v
# Woechentliches Auto-Update mit Logging
echo "0 3 * * 1 root geoipupdate -v >> /var/log/geoipupdate.log 2>&1" > /etc/cron.d/geoipupdate
Nginx Konfiguration¶
In /etc/nginx/nginx.conf im http{} Block nach default_type:
##
# GeoIP2 Settings
##
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1w;
$geoip2_country_code country iso_code;
}
map $geoip2_country_code $geo_dach {
default 0;
CH 1;
DE 1;
AT 1;
}
map $geoip2_country_code $geo_ch {
default 0;
CH 1;
}
##
# SSL Settings
##
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
GeoIP Snippets¶
# DACH
cat > /etc/nginx/snippets/geoip-dach.conf << 'EOF'
if ($geo_dach = 0) { return 444; }
EOF
# Nur Schweiz
cat > /etc/nginx/snippets/geoip-ch.conf << 'EOF'
if ($geo_ch = 0) { return 444; }
EOF
444 = Nginx schliesst Verbindung ohne Antwort.
Snippet pro Dienst einbinden¶
# DACH fuer die meisten Dienste (OHNE Authentik!)
for conf in freescout snipeit onlyoffice opnform; do
sed -i 's| ssl_prefer_server_ciphers on;| ssl_prefer_server_ciphers on;\n\n include snippets/geoip-dach.conf;|' /etc/nginx/sites-available/${conf}.conf
sed -i 's| return 301 https://$server_name$request_uri;| include snippets/geoip-dach.conf;\n return 301 https://$server_name$request_uri;|' /etc/nginx/sites-available/${conf}.conf
done
# Guacamole nur CH
sed -i 's| ssl_prefer_server_ciphers on;| ssl_prefer_server_ciphers on;\n\n include snippets/geoip-ch.conf;|' /etc/nginx/sites-available/guacamole.conf
sed -i 's| return 301 https://$server_name$request_uri;| include snippets/geoip-ch.conf;\n return 301 https://$server_name$request_uri;|' /etc/nginx/sites-available/guacamole.conf
# Authentik - KEIN GeoIP!
nginx -t && systemctl reload nginx
Schicht 2 - CrowdSec WAF¶
Installation¶
# Offizielles Repository
curl -s https://install.crowdsec.net | sh
apt install crowdsec
# Nginx Bouncer via apt
apt install crowdsec-nginx-bouncer
Collections installieren¶
cscli collections install crowdsecurity/appsec-generic-rules
cscli collections install crowdsecurity/appsec-virtual-patching
cscli appsec-rules install crowdsecurity/crs
systemctl reload crowdsec
AppSec Engine¶
cat > /etc/crowdsec/acquis.d/appsec.yaml << 'EOF'
listen_addr: 127.0.0.1:7422
appsec_config: crowdsecurity/appsec-default
name: myAppsecEngine
source: appsec
labels:
type: appsec
EOF
AppSec Config mit OWASP CRS¶
/etc/crowdsec/appsec-configs/appsec-default.yaml:
name: crowdsecurity/appsec-default
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-*
- crowdsecurity/generic-*
- crowdsecurity/crs
outofband_rules:
- crowdsecurity/experimental-*
- crowdsecurity/appsec-generic-test
AppSec im Bouncer aktivieren¶
sed -i 's|APPSEC_URL=|APPSEC_URL=http://127.0.0.1:7422|' /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
sed -i 's|ALWAYS_SEND_TO_APPSEC=false|ALWAYS_SEND_TO_APPSEC=true|' /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
systemctl restart nginx
Freescout False Positive beheben¶
Freescout's /polycast/receive Endpoint wird von CRS als verdaechtig erkannt. Loesung in /etc/nginx/conf.d/crowdsec_nginx.conf:
sed -i 's|map \$server_addr \$unix {|map $request_uri $crowdsec_disable_appsec {\n default 0;\n ~^/polycast/receive 1;\n}\nmap $server_addr $unix {|' /etc/nginx/conf.d/crowdsec_nginx.conf
nginx -t && systemctl reload nginx
Authentik/Guacamole OAuth Fix¶
Das Scenario LePresidente/http-generic-403-bf blockt den OAuth Flow. In /etc/crowdsec/scenarios/http-generic-bf.yaml die Filter anpassen:
# Generic 401 Authorization Errors
name: LePresidente/http-generic-401-bf
filter: "evt.Meta.log_type == 'http_access-log' && evt.Parsed.verb == 'POST' && evt.Meta.http_status == '401' && evt.Meta.http_host != 'auth.domain.tld' && evt.Meta.http_host != 'remote.domain.tld'"
# Generic 403 Forbidden Errors
name: LePresidente/http-generic-403-bf
filter: "evt.Meta.log_type == 'http_access-log' && evt.Parsed.verb == 'POST' && evt.Meta.http_status == '403' && evt.Meta.http_host != 'auth.domain.tld' && evt.Meta.http_host != 'remote.domain.tld'"
Console einrichten¶
cscli console enroll DEIN_ENROLL_KEY
cscli console enable context
cscli console enable console_management
systemctl restart crowdsec
Auto-Update¶
echo "0 4 * * 1 root cscli hub update && cscli hub upgrade && systemctl restart crowdsec" > /etc/cron.d/crowdsec-update
Interne Dienste¶
server {
listen 80;
server_name n8n.int.domain.tld;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name n8n.int.domain.tld;
ssl_certificate /etc/letsencrypt/live/n8n.int.domain.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/n8n.int.domain.tld/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
# kein geoip snippet
location / {
proxy_pass http://10.x.x.x:PORT;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Bekannte Fallstricke¶
Authentik darf kein GeoIP haben¶
OAuth Callbacks haben keinen Country Code - fuehrt zu Endlosschleife beim Login.
if mit Regex im server{} Block¶
# FALSCH - verursacht Probleme mit OAuth Flows
if ($geoip2_country_code !~ ^(CH)$) { return 444; }
# RICHTIG - map im http{} Block, einfaches if im server{} Block
if ($geo_ch = 0) { return 444; }
map nur im http{} Block¶
map Direktiven funktionieren nicht im server{} oder location{} Block.
Doppelter Block in Bouncer Config¶
Bei Neuinstallation pruefen ob Config dupliziert wurde:
Nuetzliche Befehle¶
# Status
systemctl status crowdsec nginx
cscli bouncers list
cscli metrics | grep -i appsec
# Alerts und Decisions
cscli alerts list
cscli alerts inspect <ID>
cscli decisions list
cscli decisions delete --ip 1.2.3.4
# Hub
cscli hub update && cscli hub upgrade
cscli scenarios list -a
cscli appsec-rules list
cscli appsec-configs list
# AppSec direkt testen
curl -s -w "%{http_code}" http://127.0.0.1:7422/ -H "x-crowdsec-appsec-api-key: DEIN_API_KEY" -H "x-crowdsec-appsec-ip: 1.2.3.4" -H "x-crowdsec-appsec-host: helpdesk.domain.tld" -H "x-crowdsec-appsec-verb: GET" -H "x-crowdsec-appsec-uri: /?id=1' OR '1'='1" -H "x-crowdsec-appsec-user-agent: curl"
# GeoIP testen
mmdblookup --file /usr/share/GeoIP/GeoLite2-Country.mmdb --ip 8.8.8.8 country iso_code
Wichtige Dateipfade¶
| Datei | Zweck |
|---|---|
| /etc/nginx/nginx.conf | Hauptkonfiguration (GeoIP Maps, SSL) |
| /etc/nginx/snippets/geoip-dach.conf | GeoIP Snippet fuer DACH |
| /etc/nginx/snippets/geoip-ch.conf | GeoIP Snippet fuer CH only |
| /etc/nginx/sites-available/*.conf | Site-spezifische Configs |
| /etc/nginx/conf.d/crowdsec_nginx.conf | CrowdSec Lua Integration |
| /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf | Bouncer Konfiguration |
| /etc/crowdsec/acquis.yaml | Log-Quellen |
| /etc/crowdsec/acquis.d/appsec.yaml | AppSec Engine |
| /etc/crowdsec/appsec-configs/appsec-default.yaml | WAF Config (inkl. CRS) |
| /etc/crowdsec/scenarios/http-generic-bf.yaml | BF Scenarios (angepasst) |
| /usr/share/GeoIP/GeoLite2-Country.mmdb | GeoIP Datenbank |
| /etc/GeoIP.conf | MaxMind Credentials |
| /var/log/geoipupdate.log | GeoIP Update Logs |
Sicherheits-Schichten im Ueberblick¶
| Schicht | Technologie | Was wird geblockt | Status |
|---|---|---|---|
| 1 | GeoIP (Nginx) | Alle Laender ausser konfigurierter Gruppe | Aktiv |
| 2 | CrowdSec WAF (AppSec + CRS) | SQLi, XSS, RCE, Path Traversal, CVEs | Aktiv |
| 2 | CrowdSec Community Blocklist | Bekannte boese IPs weltweit | Aktiv |
| 2 | CrowdSec Behaviour Detection | Brute-Force, Scanner, Crawler | Aktiv |
| 3 | Authentik SSO | Unautorisierte Benutzer | Aktiv |