Ein Reverse Proxy hat wesentliche Vorteile gegenüber direkt von außen erreichbaren Webseiten:
- Webserver sind nicht über IP Adresse erreichbar.
- Durch die Weiterleitung per Namensauflösung benötigt man nur eine externe IP und kann damit mehrere Webserver nach außen hin anbieten.
- Auf dem Reverse-Proxy können zerntral Sicherheitsfeatures implementiert werden.
Vorraussetzungen
Die Anleitung ist für Debian 11. Andere Distributionen können bei Pfaden und Programmpaketen oder Programmnamen leicht abweichen. Apache2 wurde vom Paketmanager vorinstalliert.
Debian hat kein sudo vorinstalliert. Die unten gezeigten Kommandos müssen aber mit root-Rechten ausgeführt werden. Also entweder sudo installieren oder als root anmelden und bei den Befehlen das vorangestellte sudo weg lassen.
Referenzen
https://samhobbs.co.uk/2015/09/example-whitelisting-rules-apache-modsecurity-and-owasp-core-rule-set
https://www.oreilly.com/content/how-to-tune-your-waf-installation-to-reduce-false-positives/
https://www.netnea.com/cms/apache-tutorials/
Überlegungen
Webzugriff der Server hinter dem Reverseproxy
Ein Reverseproxy behandelt alle Anfragen von außen an eine Webseite. Nun betten Webseiten teilweise auch externe Informationen von anderen URL’s ein, prüfen auf Updates oder kommunizieren mit einem Marketplace. Solche Anfragen, die dann von der Webseite selbst aus richtung Internet gehen, müssen für das Funktionieren einer Webseite erlaubt werden. Hier wären also zusätzliche Freigaben auf der Firewall nötig. Ich gehe davon aus, dass bei einem Reversproxy Konstrukt eine Gateway Firewall im Einsatz ist.
HTTP oder SSL Termination
Soll zwischen dem Reverseproxy und den dahinter liegenden Servern über SSL kommuniziert werden oder reicht HTTP aus, da wir uns bereits im internen Netzwerk befinden?
Oft erscheint die Konfiguration von SSL als zusätzliche Last und viele belassen es bei HTTP. Das ist aber grundlegend falsch. Ganz klar sollte auch hinter dem Reverseproxy SSL gesprochen werden. Eine unverschlüsselte Kommunikation ist in jedem Fall überall zu vermeiden. Da ist sich die Sicherheitcommunity zumindest einig.
Logging
Auf dem Reverseproxy selbst werden die eingehenden Verbindungen mit der richtigen RemoteIP geloggt. Auf den Servern hinter dem Remoteproxy wird in der Standardkonfiguration die IP das Reverseproxy geloggt. Wer also auch auf den Servern hinter dem Reverseproxy die RemoteIP des Clients sehen will, muss das Loggen von RemoteIP’s konfigurieren. Das geht bei Apache mit mod_remote. Der Reversoproxy bringt die dafür nötigen über mod_proxy mit ein:
https://httpd.apache.org/docs/current/mod/mod_proxy.html#x-headers
Debian vorbereiten
Zusatzpakete installieren (optional)
Vim ist das bessere vi und ich persönlich bevorzuger den gegenüber nano
Die resolvconf benötigt man, um unter /etc/network/interfaces auch DNS-Server angeben zu können. Anstonsten sind die fest unter /etc/resolv.conf hinterlegt.
sudo apt install vim resolvconf
Apache installieren (falls noch nicht installiert)
sudo apt install apache2
Apache konfigurieren
Module aktivieren
sudo a2enmod ssl sudo a2enmod mod_proxy sudo a2enmod mod_proxy_http
Konfiguration anpassen
Zuerst deaktivieren wir die Standardkonfiguration von Apache:
sudo a2dissite *
Dann erstellen wir unsere eigene Seitenkonfiguration.
vi /etc/apache2/sites-available/revproxy.conf
Hier eine Beispielkonfiguration. Das Wissen um SSL Zertifikate und wie man die nutzt setze ich hier mal vorraus. Dazu gibt es genug Tutorials im Netz.
Mit der ProxyPass und ProxyPassReverse Direktive wird der Webserver angegeben, der unter subdomain.domain.de erreichbar sein soll. Wichtig zu wissen ist hierbei, dass man mehrere dieser VirtualHost Konfigurationen gleichzeitig einsetzen kann. An dem Domainnamen wird festgemacht, welche Konfiguration für eine Weiterleitung genutzt wird. Wenn ich nun also subdomain2.domain.de im Webbrowser aufrufen würde, wurde der Reverseproxy in ein Timeout laufen, da es hierzu noch keine Konfiguration gibt. Und genau das wollen wir. Denn nur deshalb kann ein Webserver (in diesem Fall ReverseProxy) mehrere Seiten über eine IP bedienen.
<VirtualHost *:80> ServerName subdomain.domain.de ProxyPreserveHost On ProxyPass / http://172.31.10.195/ ProxyPassReverse / http://172.31.10.195/ </VirtualHost> <VirtualHost *:443> SSLEngine On SSLProxyEngine On SSLProxyVerify none SSLProxyCheckPeerCN off SSLProxyCheckPeerName off SSLProxyCheckPeerExpire off ServerName subdomain.domain.de ProxyPreserveHost On #SSLCACertificateFile /etc/apache2/ssl/Intermediate_CA_Bundle.crt SSLCertificateFile /etc/apache2/ssl/cert.crt SSLCertificateKeyFile /etc/apache2/ssl/cert.key ProxyPass / https://172.31.10.195/ ProxyPassReverse / https://172.31.10.195/ </VirtualHost>
a2enconf revproxy systemctl reload apache2
Über das auskommentierte SSLCACertificateFile kann man Intermediate Keys einbinden. Sofern das Intermediate Key also nicht bereits im SSLCertificateFile enthalten ist, sollte man das Zertifikat separat angeben.
Damit ist der Reverseproxy für den ersten Webserver konfiguriert. Weitere Konfigurationen für weitere Webserver können nun hinzugefügt werden.
Websockets
Soll der Reverseproxy auch Websockets unterstützen, muss die extra konfiguriert werden.
https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html
Erstmal aktivieren wir die nötigen Module
sudo a2enmod rewrite sudo a2enmod proxy_wstunnel
Dann die VirtualHost-Konfiguration anpasssen. Aus der Verlinkung oben:
Proxying both HTTP and websockets at the same time, where the websockets URL’s are not websocket-only or not known in advance can be done by using the
https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.htmlRewriteRule
directive to configure the websockets proxying:
ProxyPass / http://example.com:9080/ RewriteEngine on RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule ^/?(.*) "ws://example.com:9080/$1" [P,L]
Loggen von Remote IP’s auf den Hosts hinter dem ReverseProxy
Apache
Step 1 – Konfiguration von mod_remoteip
Auf dem Reverseproxy selbst werden die eingehenden Verbindungen mit der richtigen RemoteIP geloggt. Auf den Servern hinter dem Remoteproxy wird in der Standardkonfiguration die IP das Reverseproxy geloggt. Wer also auch auf den Servern hinter dem Reverseproxy die RemoteIP des Clients sehen will, muss das Loggen von RemoteIP’s konfigurieren. Das geht bei Apache mit mod_remote. Der Reversoproxy bringt die dafür nötigen über mod_proxy mit ein:
https://httpd.apache.org/docs/current/mod/mod_proxy.html#x-headers
Wir erstellen hierzu auf den Hosts hinter dem ReverseProxy eine weitere Konfig, die einen X-Forwarded-For Header für alle VirtualHosts implementiert. Man kann die Konfiguration auch pro VirtualHost konfigurieren. Die globale Konfiguration hält die VirtualHosts aber kleiner und gilt auch für neue VirtualHosts.
Hierfür wird das Modul remoteip beötigt:
sudo a2enmod remoteip sudo vi /etc/apache2/conf-available/remoteip.conf
Folgenden Inhalt einfügen:
RemoteIPHeader X-Forwarded-For RemoteIPTrustedProxy 127.0.0.1
Konfig aktivieren und Apache neu laden:
a2enconf remoteip
Step 2 – Apache Logformat anpassen
Nun muss noch das Logformat in der apache2.conf angepasst werden. Wir ersetzen %h mit %a, behalten sonst aber die Originalkonfiguration.
https://httpd.apache.org/docs/2.4/mod/mod_log_config.html#formats
sudo vi /etc/apache2/apache2.conf LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%a %l %u %t \"%r\" %>s %O" common LogFormat "%{Referer}i -> %U" referer LogFormat "%{User-agent}i" agent
Dann den Apache neu starten und fertig:
systemctl reload apache2
Nginx
Nginx unterstützt den Kram nativ. Man muss nur das Logformat unter /etc/nginx/nginx.conf
anpassen.
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main;
Wenn man die RemoteIP vorne im Log haben will:
log_format main '$http_x_forwarded_for - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent"'; access_log /var/log/nginx/access.log main;
Achtung: wenn eine VirtualHost-Konfiguration auf dem Nginx einen Eintrag für ein eigenes access log enthält, muss diesem auch das main
hintenangestellt werden.
Mod_security
mod_security befindet sich gerade in einer Transitionsphase. Es war früher nur als Apache Modul erhältlich, wurde aber in einen plattformunabhängigen Kern umgewandelt und kann nun auch für Nginx oder den ISS eingesetzt werden. Für jede Plattform gibt es dann einen zusätzlichen Connector, der die Plattforminterna abbildet. Das mod_security Team empfiehlt für den Einsatz im Apache aber noch immer das Paket libapache2-mod-security2 (Stand 07.12.2021), siehe Link zu ModSecurity-apache Connector.
https://github.com/SpiderLabs/ModSecurity
https://github.com/SpiderLabs/ModSecurity-nginx
https://github.com/SpiderLabs/ModSecurity-apache
https://github.com/coreruleset/coreruleset
https://wiki.ubuntuusers.de/Archiv/Apache/mod_security/
https://github.com/SpiderLabs/ModSecurity/wiki
Installation
Wir konfigurieren den Apache und installieren daher libapache2-mod-security2, was zusätzlich auch das Core Ruleset installiert (modsecurity-crs):
sudo apt install libapache2-mod-security2 sudo systemctl restart apache2
Nun aktivieren wir noch die empfohlene Konfiguration von ModSecurity:
cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
ModSecurity läuft nun im Detection Mode. Es wird also noch keine Inhalte auf Basis der Regeln implementieren. Um das zu ändern Setzen wir in /etc/modsecurity/modsecurity.conf
die Direktive:
SecRuleEngine On #DetectionOnly
Das wars auch schon. Ob ModSecurity ordentlich gestartet ist kann man über das error.log
einsehen. Da steht dann sowas drin wie:
ModSecurity for Apache/2.9.3 (http://www.modsecurity.org/) configured.
Pfade und Zusammenhänge
Die Installation unter Debian teilt sich in ein paar Teilbereiche auf.
Installationspfade und Hauptkonfigurationsdateien
Apche ModSecurity Modulkonfiguration
/etc/apache2/mods-available/security2.conf
Über die Apache ModSecurity Modulkonfiguration wird die modsecurity.conf und die owasp-crs.load eingebunden
ModSecurity: /etc/modsecurity/modsecurity.conf
Konfigurationsdatei von Modsecurity
Core Ruleset: /usr/share/modsecurity-crs/owasp-crs.load
Die Datei lädt die Core Ruleset Regeln. Diese teilen sich in unterschiedliche Dateien auf. Konkret steht in dieser Datei folgendes:
Include /etc/modsecurity/crs/crs-setup.conf IncludeOptional /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf Include /usr/share/modsecurity-crs/rules/*.conf IncludeOptional /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
Die crs-setup.conf sollte man sich durchlesen und ggf. Anpasungen vornehmen.
Test
Kuerzer Test, ob die Regeln greifen:
https://www.domain.com/aphpfilethatdonotexist.php?something=../../etc
Der Zugriff wird mit HTTP 403 Forbidden verweigert und unter /var/log/apache2/error.log
sollte nun der Grund für die Verweigerung stehen.
Beispiele
Für die angegebene Webseite die Regel 930120 einschränken, so dass kein XML geparst wird:
SecRule SERVER_NAME "sub.domain.com" "phase:2,id:12005,nolog,allow,ctl:ruleRemoveTargetById=930120;XML:/*"
Für die angegebene Webseite die Regel 930120 einschränken, so dass das Argument (ARG) xr nicht geparst wird. Dieses Argument könnte auch ein Argument auf der ersten Hierachrieebene von einem JSON-Anteil sein. Die zweite Hierarchieebene erreicht man über eine Punktnoation (z.B. xr.windows.width).
SecRule SERVER_NAME "wikitest.fps-law.de" "phase:2,id:12005,nolog,allow,ctl:ruleRemoveTargetById=930120;ARGS:xr"
Engine für einen bestimmten URI-Anteil ausschalten
SecRule REQUEST_URI "@beginsWith /rest/api/user/watch" \ "phase:1,\ id:12003,\ nolog,\ allow,\ ctl:ruleEngine=off"
Regeln für einen bestimmten Webseitenanteil ausschalten
SecRule REQUEST_URI "@beginsWith /pages/doeditpage.action" \ "phase:1,\ id:12002,\ nolog,\ allow,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160"
Verschachteln von Regeln
SecRule REQUEST_METHOD "@streq PUT" \ "id:9003105,\ phase:2,\ pass,\ t:none,\ nolog,\ ver:'OWASP_CRS/3.3.0',\ chain" SecRule REQUEST_FILENAME "@contains /remote.php/webdav" \ "t:none,\ ctl:ruleRemoveById=920000-920999,\ ctl:ruleRemoveById=932000-932999,\ ctl:ruleRemoveById=921150,\ ctl:ruleRemoveById=930110,\ ctl:ruleRemoveById=930120"
Wissenswertes
ARG und ARG_NAMES werden auf den GET und POST Requests gebildet.
http://server.invalid/test.php?pretty_arg=test123&ugly_arg=345test
ARGS_NAMES = "pretty_arg","ugly_arg"
ARGS = "pretty_arg:test123","ugly_arg:345test"
Man kann diese Werte in Regeln verwenden, z.B. um Anteile eines Requests von der Prüfung in einer Regel auszuschließen.
SecRule REQUEST_FILENAME "@streq /path/to/file.php" "phase:1,id:2001,t:none,nolog,pass,ctl:ruleRemoveTargetById=959072;ARGS:ugly_arg"
Die ModSecurity Direktiven sind gleichzeitig Apache Direktiven. Man kann diese also in einem VirtualHost-Eintrag verwenden. Nach meinen Erkenntnissen aber nur die Regeln für Phase 1 Checks.
https://malware.expert/modsecurity/processing-phases-modsecurity/
Die Sicherheitregeln kann man in eigene Konfigs packen oder in die dafür vorgesehenen Dateien
/etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
Eine Sicherheitsregel muss immer eine ID erhalten, die man aus einem Pool mehr oder weniger frei wählen kann.
Apache Config als Referenz
Hier eine Beispielkonfiguration mit allerhand modsecurity-Regeln als kleine Referenz. Die Konfiguration stammt von einer Confluence onPremise Installation. Der Domänname ist fiktiv und hat nichts mit evtl. real existierenden Domänen zu tun. Wenn Teilbereiche der Website durch modsecurity geblockt werden, stehen die Details dazu im Error-Log des Servers (tail -f /var/log/apache2/error.log -n200).
<VirtualHost *:80> ServerName wiki.domain.de ProxyPreserveHost On Redirect permanent / https://wiki.domain.de/ # RewriteEngine on # RewriteCond %{SERVER_NAME} =wiki.domain.de [OR] # RewriteCond %{SERVER_NAME} =wiki.domain.de # RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent] </VirtualHost> <VirtualHost *:443> ServerName wiki.domain.de ProxyRequests Off ProxyVia Off ProxyPreserveHost On SSLEngine On SSLProxyEngine On SSLProxyVerify none SSLProxyCheckPeerCN off SSLProxyCheckPeerName off SSLProxyCheckPeerExpire off #SSLCACertificateFile /etc/apache2/ssl/Intermediate_CA_Bundle.crt SSLCertificateFile /etc/apache2/ssl/domain.de.crt SSLCertificateKeyFile /etc/apache2/ssl/domain.de.key ProxyPass / https://172.32.0.95/ # Das ist die IP des eigentlichen Webservers ProxyPassReverse / https://172.32.0.95/ RewriteEngine on RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule ^/?(.*) "wss://172.32.0.95/$1" [P,L] <IfModule mod_security2.c> SecRuleEngine On #### Editor laden 1 #### SecRule REQUEST_URI "@beginsWith /rest/tinymce/1/drafts" \ "phase:1,\ id:13001,\ nolog,\ allow,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160" #### Editor laden 2 #### SecRule REQUEST_URI "@beginsWith /pages/doeditpage.action" \ "phase:1,\ id:13002,\ nolog,\ allow,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160" #### Editor speichern 1 #### SecRule REQUEST_URI "@beginsWith /rest/api/content" \ "phase:1,\ id:13003,\ nolog,\ allow,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160" #### Beobachten-Feature #### SecRule REQUEST_URI "@beginsWith /rest/api/user/watch" \ "phase:1,\ id:13004,\ nolog,\ allow,\ ctl:ruleEngine=off" # ctl:ruleRemoveById=200002" #### Favorit-Feature, disable Engine so Logs don't generate JSON Error Messages #### SecRule REQUEST_URI "@beginsWith /rest/experimental/relation/user/current/favourite" \ "phase:1,\ id:13011,\ nolog,\ allow,\ ctl:ruleEngine=off" #### Der JSON-Baum xr beinhaltet zu ladende Module. ModSec erkennt dort einen Anteil .profile (definiert in der Regeldatei lfi-os-files.data) als Dateisystemattacke #### SecRule REQUEST_URI "@beginsWith /rest/webResources" "phase:1,id:13021,nolog,allow,ctl:ruleRemoveTargetById=930120;ARGS:xr" SecRule REQUEST_FILENAME "@contains .profile" "phase:1,id:13022,nolog,allow,ctl:ruleRemoveTargetById=930120;ARGS:xr" #### Makros im Editor einfügen - [id "941160"] [msg "NoScript XSS InjectionChecker: HTML Injection"] #### SecRule REQUEST_URI "@beginsWith /rest/tinymce/1/macro" "phase:1,id:13023,nolog,allow,ctl:ruleRemoveTargetById=941160;ARGS:macroHTML" #### Backend Konfiguration #### SecRule REQUEST_URI "@beginsWith /admin" \ "phase:1,\ id:13024,\ nolog,\ allow,\ ctl:ruleRemoveById=932130,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160 #### Analytics #### SecRule REQUEST_URI "@beginsWith /rest/analytics" \ "phase:1,\ id:13025,\ nolog,\ allow,\ ctl:ruleRemoveById=921130 #### User Suche / individuelle Benutzerliste #### SecRule REQUEST_URI "@beginsWith /rest/cup/1.0/search/users/by/cql" \ "phase:1,\ id:13026,\ nolog,\ allow,\ ctl:ruleRemoveById=942100 #### App Updates #### SecRule REQUEST_URI "@beginsWith /rest/plugins/1.0/" \ "phase:1,\ id:13031,\ nolog,\ allow,\ ctl:ruleRemoveById=920420 #### Suche #### SecRule REQUEST_URI "@beginsWith /rest/api/search" \ "phase:1,\ id:13032,\ nolog,\ allow,\ ctl:ruleRemoveById=942100 SecRule REQUEST_URI "@beginsWith /dosearchsite.action" \ "phase:1,\ id:13033,\ nolog,\ allow,\ ctl:ruleRemoveById=942100 #### Misc #### SecRule REQUEST_URI "@beginsWith /rest/masterdetail/1.0/detailssummary/lines" \ "phase:1,\ id:13034,\ nolog,\ allow,\ ctl:ruleRemoveById=980130 SecRule REQUEST_URI "@contains /comment" \ "phase:1,\ id:13035,\ nolog,\ allow,\ ctl:ruleRemoveById=941160 #### Uploads #### SecRule REQUEST_URI "@beginsWith /rest/synchrony/1.0/content" \ "phase:1,\ id:13036,\ nolog,\ allow,\ ctl:ruleRemoveById=200002 SecRule REQUEST_URI "@beginsWith /plugins/drag-and-drop/upload.action" \ "phase:1,\ id:13037,\ nolog,\ allow,\ ctl:ruleRemoveById=921110 SecRule REQUEST_URI "@beginsWith /pages/doattachfile.action" \ "phase:1,\ id:13038,\ nolog,\ allow,\ ctl:ruleRemoveById=200004 #### Misc 2 #### SecRule REQUEST_URI "@beginsWith /rest/cql/expressions" \ "phase:1,\ id:13039,\ nolog,\ allow,\ ctl:ruleRemoveById=942100 SecRule REQUEST_URI "@beginsWith /rest/searchv3/1.0/cqlSearch" \ "phase:1,\ id:13040,\ nolog,\ allow,\ ctl:ruleRemoveById=942100 SecRule REQUEST_URI "@beginsWith /pages/rendercontent.action" \ "phase:1,\ id:13041,\ nolog,\ allow,\ ctl:ruleRemoveById=932115,\ ctl:ruleRemoveById=941100,\ ctl:ruleRemoveById=941160 SecRule REQUEST_URI "@beginsWith /rest/create-dialog" \ "phase:1,\ id:13042,\ nolog,\ allow,\ ctl:ruleRemoveById=920420 SecRule REQUEST_URI "@beginsWith /pages/templates2/doeditpagetemplate.action" \ "phase:1,\ id:13043,\ nolog,\ allow,\ ctl:ruleRemoveById=941160,\ ctl:ruleRemoveById=941100 SecRule REQUEST_URI "@beginsWith /pages/templates2/docreatepagetemplate.action" \ "phase:1,\ id:13044,\ nolog,\ allow,\ ctl:ruleRemoveById=941160,\ ctl:ruleRemoveById=941100 # Profil-Fotos hochladen SecRule REQUEST_URI "@beginsWith /rest/cup/1.0/profile" \ "phase:1,\ id:13045,\ nolog,\ allow,\ ctl:ruleRemoveById=941130,\ ctl:ruleRemoveById=941170 # Zugriff von extern sperren SecRule REMOTE_ADDR "!@ipMatchF /etc/apache2/ipaccesslist.txt" \ "phase:1,\ id:13046,\ nolog,\ deny </IfModule> </VirtualHost>