Gitea stürzt ab: Zu viele Anfragen

5 Minuten Lesezeit

Hier möchte ich kurz darstellen, wie ein Absturz eines Dienstes meinen Server über Stunden wiederholt lahmgelegt hat. Und dies so gründlich, dass ich nur aus der Konsole des Anbieters überhaupt einen Neustart durchführen konnte. Ich erkläre, wie es dazu kam und wie ich dieses und ähnliche Probleme in Zukunft vermeiden möchte.

Ich wurde angegriffen. Oder?

Ich saß gerade an einem Artikel, den ich später posten wollte. Zur Sicherheit erstellte ich wie üblich einen Commit und wollte ihn auf meine Gitea-Instanz schieben. Doch mein git push Befehl lief einfach nicht durch.

Verwundert versuchte ich, meine Webseite aufzurufen. Zurück kam:

Image: A browser's timeout error message "Could not complete your request"

Merkwürdig. Anschließend wollte ich mich auf meinem Server einloggen, um nach dem Rechten zu sehen: ssh <servername>. Wieder blieb das Terminal ohne Reaktion. Mist!

Timeout. Als letzte Rettung meldete ich mich bei meinem Hoster an und schaute auf die Graphen des Servers:

Image: 200% CPU load on my server for nearly two hours

Oh, was ist denn da los? Nun versuche ich über die Weboberfläche, den Server herunterzufahren. Klappt auch nicht. Erst ein harter Neustart ist erfolgreich. Ich kann mich wieder per SSH einloggen und sehe, dass auch alle Docker-Container wieder ganz normal hochfahren.

Was war passiert?

Gut, dass ich eine Woche lang alle Logs aufbewahre. So und über die Auslastung der Maschine über Zeit kann ich einigermaßen rekonstruieren, was passiert ist.

Welche Logs helfen?

  1. Crash lokalisieren! Auslösende Anwendung finden im kern.log, Zeitstempel notieren.
  2. Gibt es systemweite Auswirkungen oder andere Dienste, die beeinträchtigt werden? Im syslog nachsehen.
  3. Falls der Verdacht aufkommt, das System sei möglicherweise gehackt worden: auth.log hat die Details.
  4. Sollte die betroffene Anwendung im Container laufen, helfen entsprechende Logs dort möglicherweise weiter.
  5. Logs der Anwendung selbst sichten. Besonders die Zeit kurz vor dem Crash ist interessant.

Und hier nochmal im Detail die Logs, die ich mir für meine Verhalten angesehen habe und wo sie zu finden sind.

log name purpose relevant content for this issue
/var/log/syslog System-wide (bare metal) messages (docker) warning: “health check for container <ID> timeout” (containerd) error: “ttrpc: received message on inactive stream”
/var/log/auth.log Contains authentication messages from external hosts, in my case mostly SSH None
/var/log/kern.log Logs all app, service, daemon and system crashes Out of memory: Killed process <ID> (<serviceName>)
[.]/gitea/log/gitea.log Protocol for requests, actions on Gitea’s web interface, repo changes etc. GET requests, crash/restart indications
docker container logs <containerID> Docker’s logs for the container in question Received signal 15; terminating. (SIGTERM)

kern.log/syslog

Hier sehe ich neben dem Stack trace, was genau passiert ist. Die am besten verständlichste Meldung ist folgende:

Out of memory: Killed process `<ID>` (gitea)

Gitea ist also der Stecker gezogen worden, weil es praktisch alle Systemressourcen gezogen hatte. Wenn ich im Log nach oben scrolle, sehe ich ein paar Minuten vorher:

<timestamp> <machineName> dockerd[716]: level=warning msg="Health check for container <ID>"
<timestamp> <machineName> dockerd[716]: level=error msg="stream copy error: reading from a closed fifo"

Bereits hier sehe ich Warnungen, dass der gitea-Container nicht sauber arbeitet. Ein weiterer Vorteil, den das Einbauen von “Healthchecks” in der docker-compose.yml Datei hat. Über Zweck und Einbau von Healthchecks in docker hatte ich aus anderen Gründen bereits einen Artikel verfasst.

gitea.log

Folgender Log-Eintrag zeigt wegen eines Bugs in einem Submodul indirekt an, dass Gitea gerade neu gestartet ist:

cmd/web.go:205:serveInstalled() [W] Table system_setting Column version db default is , struct default is 1

Und weiter oben im Log nun:

Image: Gitea log showing numerous GET request entries, many of which for a specific large file (MiB range), following a typical Gitea startup message

Sehr interessant. Die highlight.css liegt in meinem öffentlichen Repo. Auf ihrer Grundlage erhält die Seite lectures.schallbert.de ihr Aussehen. Diese Datei ist ziemlich groß, fast 1MiB. Und sie wird hier gleich zig male geladen, und das praktisch für jeden Commit.

Nun schaue ich mir weitere Crashes in der Vergangenheit an. Immer sind es bursts von GET-Befehlen auf große Dateien oder die Anforderung von compares zwischen zwei Branches des Repository, die einem Aufhängen meines Servers vorausgehen.

Wer steckt dahinter?

Alle crash-auslösenden Requests kommen aus dem selben IP-Adressraum. Die Abstürze starteten bereits vor ein paar Wochen. Zumeist aber zu Tageszeiten, die ich (und anscheinend auch viele meiner LeserInnen) nicht mitbekamen. Und nach ein paar Minuten war der Server stets wieder im Normalbetrieb.

Image: Whois-request for the IP that had my server crashed, owner: Google LLC (GCP)

Oha, die Spur führt zu Google’s Cloud Platform (GCP).

Aber ich hatte doch Robots ausgesperrt?

In der Tat hatte ich die Suchmaschinenindexierung für Gitea gestoppt. Merkwürdig. Oder habe ich es hier gar nicht mit einer der Google-spiders oder “KI”-Scraper, sondern mit einem fiesen Hacker zu tun, der sich bei “Google Cloud” eine virtuelle Maschine gemietet hat?

Log-Recherche: Wie häufig wird die Datei angefragt?

Ein Spider würde ja höchstens einmal alle paar Wochen alle meine Seiten durchsuchen, richtig? Und sich hoffentlich nicht über meine robots.txt hinwegsetzen. Erst recht nicht würde ein Crawler mehrfach und in kurzen Abständen denselben Request stellen, oder?

Zum dies zu prüfen, suche ich in den *Gitea Logs nach Einträgen von GET-Anfragen auf einer der großen und damit ressourcenintensiv zu übertragenen Dateien:

gunzip gitea.log.<date.rotateID>
grep "<filename>" gitea.log.<date.rotateID>

In der Liste sehe ich, dass dieselbe Anfrage auf dieselbe Zieldatei von derselben IP-Adresse mehrfach und binnen Sekunden gestellt wird. Kurz vor dem Crash benötigt mein Server schon fast 4sec, um die Anfrage zu bedienen.

Auch sehe ich, dass sich die IP-Adresse alle paar Stunden ändert.

Einen DoS-Angriff parieren

Zusammenfassend muss ich zum Schluss kommen, dass ich aus dem Adressraum des GCP per Denial-of-service angegriffen werde.

Um ein bisschen mehr Hintergrundwissen zu sammeln, besuche ich ein paar Websites zum Thema. Dort lerne ich, dass gitea auf meinem Server unter einer Application Layer Attack zusammenbricht. Jetzt da ich weiß, was passiert und wie das Problem heißt, fällt das Finden von Lösungen gleich viel leichter 😅

Ich will mich natürlich nicht kampflos ergeben, indem ich meine Gitea-Instanz dauerhaft vom Netz nehme. Also, welche Optionen habe ich?

Mehr Ressourcen bereitstellen

Zugegeben, meine Maschine hat nur 40GB Speicher und 2GB VRAM sowie eine mickrige 2-Kern CPU aus 2009. Ich könnte einen leistungsfähigeren Server buchen, um Lastspitzen besser abreiten zu können. Doch dies würde den Angriff nicht verhindern, sondern nur seine Auswirkungen abmildern.

Rate limiting direkt im Webserver

Rate limiter begrenzen die Anzahl Anfragen eines Clients innerhalb eines Zeitfensters. Dadurch werden die Ressourcen des Servers geschont. Dies ist eine “weiche” Abwehr von Dos-Attacken, denn auslösende IP-Adressen werden mit einer Fehlermeldung kurzzeitig und sanft abgewehrt. Üblicherweise wird HTTP status code 429 “Too Many Requests” zurückgegeben, wenn der Limiter eingreift.

Gitea verfügt über keinen Rate Limiter. In meinem Setup läuft Gitea hinter einem reverse proxy, der von meinem Caddyserver bereitgestellt wird. Hier also müsste ich ansetzen. Für Caddy gibt es Rate limiter nur als externe Module, welche manuell in xcaddy nachinstalliert und eingerichtet werden müssen.

Blocken mit IP-table Regeln

Hier könnte man sich erneut fail2ban hernehmen und mehrfache Anfragen derselben Ressource von einer IP-Adresse schlicht blocken. Gitea hat in der Dokumentation eine Beschreibung der Einrichtung. In meinem Falle müsste ich für die Umsetzung dort weitermachen, wo ich bereits einmal aufgegeben hatte und damit nicht nur SSH, sondern auch normale Seitenaufrufe überwachen.

Für mich klingt diese Lösung am Sinnvollsten, denn sie trennt klar Funktionen in verschiedene Anwendungen auf. Ich nutze ausschließlich bei mir bereits verfügbare Applikationen: Caddy würde die Zugriffslogs bereitstellen und fail2ban müsste sie lesen und Filter in der Konfoguration jail.local so setzen, dass es sich wie ein Rate Limiter verhält.

Jetzt schaue ich mal, wie ich Caddy entsprechend mit fail2ban verbinden kann. Den weiterführenden Artikel Fail2ban mit Caddy einrichten verlinke ich direkt mal 🙂