Dreizehn Prozent unseres Webverkehrs waren Einbruchsversuche
Am Anfang stand keine Idee, sondern eine Protokolldatei. Sie war 496 Megabyte groß, umfasste siebzehn Tage, und beim Durchsehen fiel auf, dass die überwiegende Mehrheit der Fehlermeldungen darin nichts mit unserem Betrieb zu tun hatte. Es waren fremde Rechner, die unsere fünf Webseiten nach bekannten Schwachstellen absuchten.
Ausgezählt ergab das ein Bild, das wir so nicht erwartet hatten:
| Gesuchtes Ziel | Anfragen |
|---|---|
.env in 24 Schreibweisen | 60.817 |
Verzeichniswechsel (../) | 15.186 |
| PHP- und ASP-Sonden | 13.372 |
| WordPress-Pfade | 4.795 |
.git-Verzeichnisse | 4.467 |
| Summe | 98.411 |
98.411 von insgesamt 715.799 Anfragen – dreizehn Prozent des gesamten Webverkehrs, verteilt auf 755 verschiedene Absender. Eine einzelne Adresse schickte an einem Tag 9.413 Sonden.
Das Teure war nicht der Angriff, sondern die Antwort
Kein einziger dieser Versuche hatte Aussicht auf Erfolg: Odoo hat
keine .env, kein wp-admin und kein
phpMyAdmin. Trotzdem kosteten sie uns erheblich mehr als
ein Achselzucken.
Denn Odoo behandelt eine Anfrage auf einen unbekannten Pfad wie jede
andere: Es sucht eine passende Route, findet keine, ruft die
Webseiten-Fehlerseite auf und rendert sie über die volle
Vorlagenmaschinerie. Enthält der Pfad Punkt-Segmente wie
/@fs/../../.env, stolpert das Rendern zusätzlich beim
Zusammensetzen der og:url-Metaangabe – und die Fehlerseite
der Fehlerseite scheitert ebenfalls.
Aus einer einzigen Sonde wurden so zwei vollständige Stapelprotokolle. Zusammengerechnet machten diese Fehler 96 Prozent unseres gesamten Protokollaufkommens aus. Echte Fehler – die, die man sehen will – gingen darin unter.
Web RBL: erkennen, verbuchen, veröffentlichen
Aus dieser Auswertung ist ein Odoo-19-Modul entstanden, das wir unter der AGPL-3 veröffentlicht haben. Es setzt an drei Stellen an:
- Erkennen und abweisen, bevor Odoo mit der Wegfindung beginnt. Eine Sonde kostet damit ein knappes 403 statt einer gerenderten Fehlerseite – ohne Vorlage, ohne Protokollzeile.
- Buch führen: Welche Adresse hat wann wonach gesucht, und an wie vielen verschiedenen Tagen.
- Die Liste veröffentlichen, damit eine Firewall oder ein Reverse Proxy den Angreifer abweisen kann, bevor er Odoo überhaupt erreicht.
Zwei Listen mit sehr unterschiedlichem Gewicht
Der Kern des Konzepts sind zwei getrennte Listen. Die Trennung ist keine Spielerei, sondern folgt daraus, dass die beiden Listen unterschiedlich schwer wiegen.
Die einfache Liste: 24 Stunden, dann wieder frei
Wer nach /.env oder /.git/config sucht,
landet beim ersten Versuch für 24 Stunden auf der Liste. Ab dann
erreicht er die Webseiten überhaupt nicht mehr – auch nicht die
Startseite.
Das klingt streng, ist aber der eigentliche Gewinn. Rechnet man es gegen die gemessenen Daten durch, kämen von 87.823 Sonden mit erkennbarer Absenderadresse nur noch 819 durch – eine je Adresse und Tag. Die Adresse mit den 9.413 Sonden käme mit genau einer davon ans Ziel; die übrigen 9.412 prallen ab.
Nach 24 Stunden läuft die Sperre aus, und das ist Absicht. 43 Prozent der Angreifer – 328 von 754 – schicken in unseren Daten genau eine Sonde und kommen nie wieder. Ihre Adresse gehört morgen womöglich einem anderen Mieter desselben Rechenzentrums. Eine dauerhafte Sperre würde dann einen Unbeteiligten treffen, und zwar in einem Umfang, der leicht übersehen wird: Wenn die Liste in einer Firewall landet, blockiert sie nicht nur Web, sondern auch Mail.
Wer dagegen an drei verschiedenen Tagen wiederkommt, wird dauerhaft gesperrt. Bewusst an drei Tagen und nicht nach drei Treffern: Dreißig Sonden in zwei Sekunden sind ein Vorfall. Wer am Montag, am Mittwoch und am Freitag wiederkommt, sucht nicht mehr – der hat uns auf einer Liste.
Die Hochrisiko-Liste: bewiesen, nicht vermutet
Die zweite Liste entsteht auf einem anderen Weg. Optional – und in
unserem eigenen Betrieb bewusst abgeschaltet – kann das Modul auf die
ersten Sondierungsversuche einer Adresse eine erfundene Antwort
schicken: eine .env, eine .git/config oder das
Wurzelverzeichnis eines Commodore 500.
Ein Gerät, das nie gebaut wurde. Das ist kein Scherz am falschen Platz, sondern Teil der Überlegung: Der Inhalt ist erkennbar Unsinn, enthält keine plausiblen Zugangsdaten und hat damit keinen Handelswert. Niemand kann behaupten, wir hätten ihn in etwas hineingelockt, was wie ein echtes System aussah.
Der Zweck ist auch nicht Täuschung, sondern Beweis. In jeder Fälschung steckt ein Wert, den es nirgendwo sonst gibt:
APP_NAME=Commodore
ADMIN_PATH=/cp-8f3a2b9e
DB_CONNECTION=cbm
DB_HOST=8050.local
Wird der Pfad /cp-8f3a2b9e je abgerufen, dann hat jemand
unsere Fälschung gelesen und danach gehandelt. Das ist kein
Verdacht und keine Heuristik – der Wert kann aus keiner anderen Quelle
stammen. Ein Fehlalarm ist ausgeschlossen.
Solche Adressen wandern ohne Frist auf die Hochrisiko-Liste. Und diese Liste hat eine Eigenschaft, die die einfache nicht hat: Kommt der Abruf von einer anderen Adresse als derjenigen, die den Köder erhalten hat, dann wurden die Daten weitergegeben. Wir erfahren also nicht nur, dass der Zweitverwerter böswillig ist, sondern auch, von wem er die Daten hat.
Die Sicherheitsaspekte im Einzelnen
Ein Modul, das Besucher aussperren kann, ist selbst ein Risiko. Die folgenden Punkte sind deshalb keine Zutaten, sondern der eigentliche Entwurf.
1. Die Proxy-Erkennung – die gefährlichste Stelle überhaupt
Steht ein Reverse Proxy vor Odoo, ist die Absenderadresse jeder Anfrage die Adresse des Proxy. Wer auf dieser Grundlage sperrt, sperrt den Proxy – und damit auf einen Schlag jeden Besucher aller Webseiten. Aus dem Schutzmodul würde ein Totalausfall.
Das Modul fragt deshalb nicht zuerst „welche Adresse nehme ich?", sondern „darf ich überhaupt sperren?". Im Zweifel lautet die Antwort nein:
proxy_mode = Truein der Odoo-Konfiguration und eineX-Forwarded-Host-Kopfzeile: Odoo hat die Adresse korrigiert, sie ist belastbar.- Es kommen
X-Forwarded-For-Kopfzeilen an, aberproxy_modeist nicht gesetzt: Es wird nicht gesperrt, und im Protokoll steht, warum. - Mehr als ein Eintrag in
X-Forwarded-For: Odoo wertet nur einen Proxy-Sprung aus, die ermittelte Adresse wäre die eines Zwischenproxys. Es wird nicht gesperrt.
2. Eigene Netze niemals
Private Adressbereiche nach RFC 1918, Loopback, Link-Local und Carrier-Grade-NAT stehen auf einer festen Ausnahmeliste, die sich nicht abschalten lässt. Eine Sperre dort träfe den Proxy, einen Kollegen im Haus oder den Server selbst – nie einen Angreifer aus dem Netz.
3. Ein eigener Fehler darf die Webseite nicht mitnehmen
Die gesamte Prüfung steckt in einer Ausnahmebehandlung. Was darin schiefgeht – ein Datenbankfehler, ein fehlerhaftes Muster, eine fehlende Tabelle – wird protokolliert, und die Anfrage läuft unverändert weiter. Ein Schutzmodul, das beim eigenen Fehler die Webseite mitnimmt, ist schlimmer als gar keines.
4. Beobachten vor Sperren
Nach der Installation ist das Sperren abgeschaltet. Sonden werden abgewiesen und verbucht, gewöhnliche Anfragen gelisteter Adressen laufen aber durch. So lässt sich einige Tage mitlesen, wer tatsächlich auf der Liste landet, bevor das Modul jemanden aussperrt.
Das hat sich bei uns bezahlt gemacht: Die ursprünglich geplante Schwelle „drei Tage Wiederholung" hätte über siebzehn Tage genau fünf Adressen dauerhaft gesperrt – weil Scanner aus Rechenzentren bei jedem Start eine neue Adresse bekommen. Die eigentliche Wirkung liegt bei der 24-Stunden-Sperre, nicht bei der Dauersperre.
5. Enge Muster statt breiter Netze
Die Erkennungsmuster sind bewusst schmal gehalten: Ein falsch erkannter Besucher ist teurer als eine übersehene Sonde. Jedes Muster trägt außerdem, ob es sperrt oder nur zählt, und das lässt sich je Muster umstellen.
Ein Beispiel für diese Vorsicht: Ein fremder Webmaster, der ein Bild
relativ falsch verlinkt (/bilder/../logo.png), soll seine
Besucher nicht bei uns aussperren. In der Praxis normalisiert der
Webserver solche Pfade ohnehin, bevor das Modul sie sieht – die
Unterscheidung greift beim doppelt kodierten Fall, den niemand
versehentlich erzeugt.
6. Eine Freigabe von Hand bleibt bestehen
Wird eine Adresse in der Oberfläche freigegeben, bleibt sie frei – auch wenn sie erneut auffällt. Wer sie freigegeben hat, hatte einen Grund; ihn stillschweigend zu überstimmen wäre die schlechtere Überraschung. Die Treffer werden weiter mitgezählt und sind einsehbar, nur die Sperre bleibt aus.
7. Die Liste ist geschützt, aber kein Geheimnis
Der Endpunkt, der die Liste ausliefert, verlangt einen Token in der Adresse. Ohne gültigen Token antwortet er mit 404 statt mit 403: Wer ihn nicht hat, soll nicht einmal erfahren, dass es den Endpunkt gibt. Der Vergleich läuft in konstanter Zeit, damit sich der Token nicht über die Antwortdauer erraten lässt.
Der Schutz dient dabei weniger der Geheimhaltung – wer die Liste liest, erfährt nur, wer uns angegriffen hat – als der Sorgfalt: Eine unbedacht übernommene fremde Sperrliste trifft im eigenen Netz womöglich einen echten Besucher.
8. Der Köder verrät nichts Verwertbares
Die erfundenen Dateien enthalten keine plausiblen Zugangsdaten, keine existierenden Hostnamen und keine Schlüssel in gültiger Form. Das ist Absicht: Ein glaubwürdig gefälschter Schlüsselbund landet in Scanner-Datenbanken und bringt am Ende mehr Verkehr statt weniger.
Der Kanarienwert ist deshalb immer etwas, das man bei uns
abruft – ein Pfad, ein Token in der Adresszeile. Ein erfundener
Datenbankname sieht zwar echt aus, beweist aber nie etwas, weil wir nie
erfahren, ob ihn jemand benutzt hat. Und weil ein 200 auf
/.env für manche Scanner selbst schon ein Signal ist, wird
der Köder abgeschaltet ausgeliefert. Wer ihn
einschaltet, soll das bewusst tun.
9. Eine Obergrenze für die Köder
Nach fünf erfundenen Antworten je Adresse ist Schluss, danach gibt es wieder 403. Wer bis dahin nicht angebissen hat, beißt nicht mehr an – und eine Adresse mit 9.413 Sonden soll uns nicht 9.413 erfundene Dateien kosten.
Einbindung in Firewall und Reverse Proxy
Die Liste wird im ärmsten denkbaren Format ausgeliefert: eine Adresse je Zeile, sonst nichts. Kein JSON, keine Kopfzeile. Genau das lesen HAProxy, nftables und ipset ohne Umweg.
curl -s -o /etc/haproxy/rbl.lst \
"https://www.example.com/web_rbl/liste?token=..."
frontend web
acl gesperrt src -f /etc/haproxy/rbl.lst
http-request silent-drop if gesperrt
Damit erreicht ein gelisteter Angreifer Odoo gar nicht mehr. Eine zweite Adresse liefert nur die Hochrisiko-Einträge – die kann man ohne schlechtes Gewissen dauerhaft in eine Firewall hängen, weil dort nur steht, wer nachweislich gehandelt hat.
Zusätzlich gibt es eine JSON-Fassung mit Zustand, Trefferzahl und Frist für eigene Auswertungen, sowie eine Verwaltung im Odoo-Backend: Sperrliste, einzelne Treffer nach Muster gruppiert, und je Eintrag die Möglichkeit, freizugeben oder dauerhaft zu sperren.
Was das Modul nicht kann
Es ersetzt keine Firewall und keine Web Application Firewall. Es erkennt bekannte Sondierungsmuster in Pfaden – nicht Angriffe auf Formulare, nicht Einschleusungsversuche in Parameter, nicht Anmeldeversuche mit geratenen Kennwörtern.
Es sieht außerdem nur, was über HTTP zu uns zurückkommt. Was ein Angreifer mit erbeuteten Datenbank- oder Postfachzugängen anstellt, bleibt unsichtbar – und genau deshalb sind die Kanarienwerte Pfade und keine Zugangsdaten.
Verfügbarkeit
Das Modul steht unter der AGPL-3 öffentlich zur Verfügung:
github.com/Alex0176/odoo-web-rbl
Es läuft bei uns produktiv auf fünf Webseiten. Den Köder haben wir dabei abgeschaltet gelassen – die Sperre allein nimmt uns den Großteil des Lärms ab, und die erfundenen Antworten wollten wir uns für den Fall aufheben, dass wir genauer wissen müssen, wer da eigentlich sucht.