j4n
j4n.e7h.eu
e7h

2025-01-19 19:00:00

Massenhafte Anfragen an Gitea von OpenAIs AI-Crawler »GPTbot« mit Caddy »abwehren«

Seit wenigen Tagen ist mein Gitea in den Fokus von OpenAIs ChatGPT geraten. Die Folge: Massenhaft Anfragen, alle 1-2 Sekunden, nahezu ohne Unterbrechung. Es wird wiederholt praktisch jede Seite aufgerufen, die irgendwie zu finden ist:

14.###.###.### - - [19/Jan/2025:19:16:23 +0100] "GET /..pfad.. HTTP/2.0" 200 16821 "-" "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)"

Versteht mich nicht falsch: Ich habe nichts dagegen, wenn meine Seiten von Bots und Crawlern besucht werden, aber dieser AI-Crawler sprengt alle höflichen und angemessenen Dimensionen.

Erste Hilfe: Einen harten Riegel vorschieben

Leider habe ich auf dem Host, auf dem meine Gitea-Instanz läuft, keinen Zugriff auf die Firewall-Regeln. Sonst wäre eine »erste Hilfe« gewesen, den Netzbereich des auffälligen Bots einfach per Firewall-Regel auszusperren.

Doch hat eine solche Lösung auch Nachteile: Hat man den richtigen Bereich erwischt? Ist er zu groß oder zu klein? Und was ist mit anderen Bereichen? Wie soll der Bot dann noch an die robots.txt kommen, um Regeln ggf. geänderte Regeln von dort zu erlernen?

Besser: Anfragen filtern - mittels Reverse Proxy »Caddy«

Der in Gitea integrierte Server ermöglicht keine feingranularen Regeln. Ein Reverse Proxy wie Caddy1 jedoch schon. Außerdem ist Caddy schnell installiert (ein einzelnes Binary) und sehr leicht zu konfigurieren.

Mein Provider bindet meine Gitea-Instanz über seinen eigenen Reverse Proxy an und sorgt somit auch gleich für die passenden SSL-Zertifikate. Ich muss nur das Port benennen, auf dem Gitea horcht. Standardmäßig ist das Port 3000, nun will ich aber Caddy »dazwischenschieben«. Caddy wird selbst also auf Port 3001 hören, und die Verbindung zu Gitea über das Port 3000 herstellen. Das Web Backend des Providers konfiguriere ich dann statt auf Port 3000 auf Port 3001.

So wird das aussehen:

1┌────────────────┐      ┌────────────────┐      ┌────────────────┐
2│ Provider       │      │ Caddy          │      │ Gitea          │
3│ Reverse Proxy  |----->| Reverse Proxy  |----->| Int. Webserver │
4│                │      │                │      │                │
5│ :443/HTTPS     |<-----| :3001/HTTP     │<-----| :3000/HTTP     │
6└────────────────┘      └────────────────┘      └────────────────┘

Da Caddy nun in der Mitte sitzt, können wir ihn zur Verarbeitung von einer einfachen Regel verwenden:

  • Ein »guter« Request ist ein solcher, dessen »User-Agent«-Header keinen AI-Bot enthält: Der Caddy-Matcher header_regexp ermöglicht eine einfache Auswertung.
  • Logging brauchen wir auf Caddy-Seite nur für solche, die eben keine »guten« Requests sind, damit wir erkennen können, wenn das Blocken zu weit geht. Damit wird die Caddy-Logging-Konfiguration von einer log_skip-Direktive für den Matcher flankiert.
  • Caddys route-Direktive hilft uns nun dabei, »gute« Requests direkt an den Reverse Proxy zu übergeben, alle anderen noch weiter zu verarbeiten.
  • Zunächst schauen wir bei »nicht guten« Requests, ob sie an die robots.txt gerichtet waren und senden eine allgemeine robots.txt zurück. So weiß der Bot, dass er nicht erwünscht ist.
  • Jeglichen weiteren Request beantworten wir mit einem HTTP-Status 403.

Damit sieht der Caddyfile so aus:

 1# Defaults
 2{
 3  # Ist zwar ein cooles Caddy-Feature, brauchen wir nicht:
 4  # HTTPS mit Let's Encrypt-Zertifikaten macht der Reverse Proxy des Providers für uns.
 5  auto_https off
 6  default_bind 0.0.0.0
 7}
 8
 9# Caddy soll auf Port 3001 hören
10:3001 {
11  # Ein guter Request...
12  @good {
13    # ... hat keinen "verdächtigen" User-Agent
14    not header_regexp User-Agent (?i)(gptbot|amazon|bytespider|openai|chatgpt|perplexity|facebook|ccbot|google-extended|omgili|anthropic|claude|cohere|meta-extern)
15  }
16  # Grundsätzlich wollen wir loggen...
17  log badbot {
18    output file /var/log/caddy/badbots-access_log.jsonl {
19      # Zu viele Logs müssen wir nicht aufheben
20      roll_size 10MiB
21      roll_keep 5
22      mode 0600
23    }
24    format json
25    level INFO
26  }
27  # ... aber gute Requests brauchen wir nicht im Log
28  log_skip @good
29
30  route {
31    # Gute Requests gehen direkt an Gitea, welches auf Port 3000 hört
32    reverse_proxy @good localhost:3000 {
33      # Gitea braucht noch die "Durchreichung" von ein paar Headern
34      header_up X-Forwarded-Proto {header.X-Forwarded-Proto}
35      header_up X-Forwarded-For {header.X-Forwarded-For}
36    }
37    # Nicht gute Requests, die die robots.txt anfragen, bekommen auch eine
38    respond /robots.txt 200 {
39      # Standard robots.txt
40      body <<ROBOTS_TXT
41User-agent: *
42Disallow: /
43
44ROBOTS_TXT
45      close
46    }
47    # Alles andere wird mit Status 403 und ohne weitere Ausgabe beantwortet
48    respond 403 {
49      body ""
50      close
51    }
52  }
53}

Test der Konfiguration

Da wir »nur« auf den User-Agent-Header filtern, lässt sich die Regel sehr einfach testen:

  1. Aufruf im normalen Webbrowser: Es kann wie gewohnt mit Gitea gearbeitet werden.
  2. Curl-Aufruf mit curl -H "User-Agent: GPTBot/1.2" -v https://########: Es kommt eine leere Antwort mit dem HTTP-Status-Code 403:
    1 * Request completely sent off
    2 < HTTP/2 403 
    3 < date: Sun, 19 Jan 2025 17:37:15 GMT
    4 < content-length: 0
    5 < server: Caddy
    
  3. Curl-Aufruf mit curl -H "User-Agent: GPTBot/1.2" -v https://########/robots.txt: Es wird eine allgemeine robots.txt ausgeliefert, die auf jeden Bot passen sollte:
    1 * Request completely sent off
    2 < HTTP/2 200 
    3 < date: Sun, 19 Jan 2025 17:41:05 GMT
    4 < content-type: text/plain; charset=utf-8
    5 < content-length: 26
    6 < server: Caddy
    7 < 
    8 User-agent: *
    9 Disallow: /
    

Das Ergebnis

Damit ist jetzt für Gitea erst einmal Ruhe. Caddy kümmert sich effizient um die Abarbeitung der Anfragen von AI-Crawlern, ohne dass diese Gitea überhaupt erreichen. Im Log sieht man direkt den Erfolg (gekürzt und formatiert):

 1{
 2  "level": "info",
 3  "ts": 1737313670.0997527,
 4  "logger": "http.log.access.badbot",
 5  "msg": "handled request",
 6  "request": {
 7    "proto": "HTTP/1.1",
 8    "method": "GET",
 9    "headers": {
10      "From": [
11        "gptbot(at)openai.com"
12      ],
13      "User-Agent": [
14        "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.2; +https://openai.com/gptbot)"
15      ]
16    }
17  },
18  "duration": 0.000152906,
19  "size": 0,
20  "status": 403,
21  "resp_headers": {
22    "Connection": [
23      "close"
24    ]
25  }
26}

Damit ist jetzt erst einmal Ruhe. Übrigens sind im Log innerhalb von 30 Minuten, während ich diesen Beitrag schrieb, fast 2.000 Requests aufgezeichnet worden.

1$ wc -l /var/log/caddy/badbots-access_log.jsonl 
21982 badbots-access_log.jsonl

Requests, um die sich Gitea nicht mehr kümmern musste und die sehr Datentransfer-sparsam beantwortet wurden.


  1. Alle Informationen zu Caddy: https://caddyserver.com/ ↩︎




Zuletzt geändert: 2025-01-20 19:50:12