Skip to content

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:

grep -c "ENABLED=true" /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
# Muss 1 ergeben, nicht 2


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