← Zurück zum Blog
pullmdoauthmcpcloudflarewafinfrastructure16. Mai 2026 · 9 Min. Lesezeit

PullMD Teil 8: Eine alte Regel

GEO: DE ONLY · RULE ACTIVE · ORIGIN: NO REQUEST
Editorial-Diagramm: eine alte papierne Clipboard-Karte mit 'geo: DE only' und 'rule active' an einer modernen WAF/Edge-Panel-Box festgeklemmt; links eingehende Request-Pfade aus US (rot, host_not_allowed, outbound blocked) und DE (grün); rechts das Origin-Server-Panel mit einem leeren Log ('origin: no request').Editorial-Diagramm: eine alte papierne Clipboard-Karte mit 'geo: DE only' und 'rule active' an einer modernen WAF/Edge-Panel-Box festgeklemmt; links eingehende Request-Pfade aus US (rot, host_not_allowed, outbound blocked) und DE (grün); rechts das Origin-Server-Panel mit einem leeren Log ('origin: no request').

Teil 7 hatte mit einem harten Stopper geendet. host_not_allowed — der Anthropic-Egress-Proxy blockt meinen Host, bevor ein Request den Origin je erreicht. Die Lehre, die ich damals gezogen hatte: außerhalb meines Einflusses. feat/oauth lag fertig im Regal. Ich wartete.

Vier Tage lang passierte nichts.

Etwas nagte

Dann begann etwas zu nagen — kein technisches Problem, eher ein gedankliches.

An einem Abend hatte @localden auf Issue #214 eine ausführliche Folge-Erklärung gepostet. Kern: claude.ai prüft den DNS-A/AAAA-Record vor dem ersten HTTP-Request — wenn der Hostname auf eine nicht-global-routbare Adresse zeigt (CGNAT, RFC1918, Link-Local), wird der Connect verworfen, bevor er Anthropic verlässt. Als konkretes Beispiel nannte der Comment fbs.synology.me — eine Synology-DDNS, die typischerweise hinter CGNAT zeigt.

Das passte sauber. Nur eben nicht auf mein Setup. Cloudflare Tunnel, gültige öffentliche A- und AAAA-Records, DNSSEC aktiv, direkte externe curl-Anfragen zuverlässig beantwortet. Für andere Fälle im Thread war das Erklärungsmuster überzeugend, für meinen war es zu eng.

Wenige Stunden später, noch in derselben Nacht, postete ich auf #214 einen sachlichen Pushback-Comment:

“Posting anyway because the explanation maps cleanly onto fbs.synology.me but doesn’t seem to fit my setup, and I’d like a sanity check before this is considered fully resolved. … If there’s an additional check beyond the IP-routability validation — e.g. ASN/range classification, the host_not_allowed allowlist mentioned in #256, or something specific to Cloudflare Tunnel egress IPs — knowing what specifically my hosts are tripping would let me address it on my side rather than guess.”

„Ich schreibe trotzdem, weil die Erklärung sauber auf fbs.synology.me passt, aber nicht auf mein Setup zu passen scheint, und ich eine Plausibilitätsprüfung möchte, bevor das als vollständig gelöst gilt. … Falls es eine zusätzliche Prüfung über die IP-Routierbarkeits-Validierung hinaus gibt — etwa ASN/Range-Klassifizierung, die host_not_allowed-Allowlist aus #256, oder etwas Spezifisches für Cloudflare-Tunnel-Egress-IPs — würde es mir helfen zu wissen, woran meine Hosts scheitern, damit ich es auf meiner Seite adressieren kann, anstatt zu raten.“ — syswave-dev (Issue-Kommentar)

Was ich damals nicht ausgeschrieben hatte: Wenn niemand antwortet, schaue ich selbst nach.

Die Logs

Am nächsten Morgen habe ich Claude Code geöffnet. Eine Mini-Session, wenige Einträge, eine Minute: SSH zum Edge-Server, traefik.log holen, lokal ablegen. Mehr nicht.

Die Logs landeten als 1,2 MB-Datei. Die eigentliche Analyse lief dann in einem claude.ai-Chat, nicht in Claude Code — und das mit Absicht. Der claude.ai-Chat hat keine Working-Directory-Bindung und keine Tool-Loop-Mechanik; er taugt für offene Recherche, für „was übersehe ich?“. Claude Code ist agentic und an ein Projekt-Verzeichnis gebunden — gut für gezielte mechanische Aktionen wie diesen SSH-Fetch.

Bis zum Mittag war der Befund klar genug, dass ich ihn festgehalten hatte. Bis zum frühen Nachmittag stand der Selbstkorrektur-Kommentar auf #214. Logs ziehen, Analyse, Befund, öffentliche Korrektur — das passte in einen einzigen Vormittag.

Der Befund

Was die Logs zeigten: Cloudflare returnte den Block schon an der Edge. Kein Request hatte den Origin je erreicht. Deshalb waren die Server-Logs leer — nicht weil mein Server schwieg, sondern weil er nicht einmal gefragt wurde.

Die ursächliche Regel: eine WAF-Custom-Rule auf der Cloudflare-Zone. Expression: (ip.geoip.country ne "DE"). Country-Filter, zonenweit gefasst, nicht hostname-spezifisch.

Den Zweck dieser Regel wusste ich noch: damals wollte ich bestimmte Dienste, die ich von unterwegs aus aufrufe, nur aus Deutschland erreichbar haben. Ein einfacher Country-Filter als zusätzliche Schicht über der eigentlichen Auth. Die Dienste, für die ich das gedacht hatte, nutze ich inzwischen gar nicht mehr. Die Regel lief trotzdem still weiter.

Als ich PullMD — und später die OAuth-Variante — unter derselben Cloudflare-Zone deployed habe, hat die Regel kommentarlos deren Hostnames übernommen. Und blockte stillschweigend die Anthropic-Outbound-IPs. US-basiert, Range 160.79.104.0/21. Die kommen definitiv nicht aus Deutschland.

Das Signal „zwei unabhängige Codebasen versagen identisch“ hatte von Teil 7 her noch gestimmt — das Problem lag tatsächlich nicht im Code. Aber beide Server saßen hinter derselben Cloudflare-Zone. Die WAF returnte den Block an der Edge, bevor der Request je den Origin erreichte. Dass es auf Anthropic-Seite aussah wie ein Allowlist-Problem: Cloudflare-Edge-Filtering im falschen Gewand.

Am Ende war es nicht DNS, sondern die WAF — eine Regel, die ihren eigenen Zweck längst überlebt hatte.

Die Selbstkorrektur

Nach dem Fix — eine Skip-Rule auf Priority 1, Anthropic-Outbound-Range 160.79.104.0/21 aus SBFM, Managed Firewall und Rate-Limiting herausgenommen — war der OAuth-Flow live.

Am frühen Nachmittag der Self-Correction-Comment auf #214:

“Update — happy to confirm the fix worked on my end. … For the record: the actual culprit on my side was a country-based GeoBlocking custom rule I had set up long ago for an unrelated service — (ip.geoip.country ne "DE") — which I’d completely forgotten about. It was happily blocking the Anthropic outbound IPs (US-based) along with everything else non-DE. Server logs showed zero traffic because Cloudflare was returning the block at the edge. So the symptom matched the pattern in this thread, but the root cause was fully on my side.”

„Update — freue mich zu bestätigen, dass der Fix bei mir funktioniert hat. … Der Vollständigkeit halber: der eigentliche Verursacher auf meiner Seite war eine länderbasierte GeoBlocking-Custom-Rule, die ich vor langer Zeit für einen unzusammenhängenden Dienst eingerichtet hatte — (ip.geoip.country ne "DE") — die ich völlig vergessen hatte. Sie hat fröhlich die Anthropic-Outbound-IPs (US-basiert) zusammen mit allem anderen Nicht-DE geblockt. Server-Logs zeigten null Traffic, weil Cloudflare den Block an der Edge zurückgab. Das Symptom passte also zum Pattern in diesem Thread, aber die eigentliche Ursache lag vollständig auf meiner Seite.“ — syswave-dev (Issue-Kommentar)

Ich schrieb dort „completely forgotten about“ — das war eine Verkürzung. Den Use-Case hatte ich noch erinnerlich. Nur die Dienste, für die ich die Regel damals angelegt hatte, nutze ich längst nicht mehr. Die Regel selbst hatte ich nicht mehr auf dem Schirm. Das ist ein kleiner, aber ehrlicher Unterschied: nicht vergessen wofür — vergessen dass.

Der Kommentar schloss mit einer Notiz für andere, die auf dasselbe stoßen könnten:

“Worth flagging for anyone else hitting this: check custom WAF rules with country/ASN/IP-range conditions, not just Bot Fight Mode. An old rule that was reasonable when you wrote it can quietly catch new legitimate traffic.”

„Es lohnt sich für alle, die darauf stoßen: prüft Custom-WAF-Regeln mit Country/ASN/IP-Range-Bedingungen, nicht nur Bot Fight Mode. Eine alte Regel, die beim Schreiben sinnvoll war, kann still legitimen neuen Traffic abfangen.“ — syswave-dev (Issue-Kommentar)

Issue blieb geschlossen — kein Re-Open-Wunsch, nur ein Korrekturstrich am Rand.

Drei Releases an einem Tag

Die Pipeline war frei. Was folgte, passte in einen einzigen Tag.

v2.2 — Site Recipe Engine

Sechs Tage früher — am 6. Mai — war v2.2 bereits geshippt worden. Der Ursprung: Issue #18 aus der Reddit-Welle Ende April. Sebastian / WinFuture23 hatte vorgeschlagen, statt für jede problematische Site individuelle Code-Patches zu schreiben, eine deklarative Engine pro Host zu bauen. Das Konzept: site-recipes.json — eine Konfigurations-Datei, die pro Hostname beschreibt, wie Fetch und Extraktion für diese Domain funktionieren sollen. DOM-Cleanup vor Extraktion, Render-Steuerung, Site-spezifische Selektoren, bevorzugter Extraktor. Default-Recipes für Future PLC und GitHub Issues waren dabei.

Self-Hoster mounten eigene Recipe-Files oder zeigen per Umgebungsvariable woanders hin. Ehrliche Limitation: Cookie-Walls löst die Engine bewusst nicht — das braucht vom Operator eingebrachten Cookie-State.

v2.3 — OAuth ships

Mit dem WAF-Fix war der Weg frei. feat/oauth lag bereit. Den Merge in main hatte ich auf sechs Konflikte vorbereitet — CHANGELOG, MIGRATION, lib/cache.js, server.js, package.json plus Lock-Datei. Die Zwischen-Releases v2.0, v2.1 und v2.2 hatten die Branches mehrfach divergieren lassen.

Ein Reviewer-Subagent fand 1 IMPORTANT und 2 MINOR, die die Self-Review übersehen hatte — darunter .env.example ohne aktuelles Version-Label. Im Fix-Commit erledigt. 558/558 Tests grün. PR #24 squash-merged, Tag v2.3.0, GitHub Release live. Schließt Issues #6 (OAuth Phase 2) und #10 (Claude Desktop Custom Connector).

Der Cliffhanger aus Teil 7 war aufgelöst.

Docker :latest — eine veraltete Notiz

Kurz nach dem Release, beim manuellen Check des Docker-Hub-Listings: :latest zeigte auf v2.3.0. Sollte auf v1.x stehen — so der Plan aus der v2.0-Periode, eine Grace-Period für Self-Hoster bis zum 16. Mai.

Was passiert war: docker/metadata-action@v5 hat einen Default flavor.latest=auto, der :latest auf jeden Semver-Tag-Push setzt — unabhängig davon, was sonst in der CI-Config steht. Das Auskommentieren der betroffenen Zeile überschreibt diese Logik nicht. Der Fix war eine explizite flavor: latest=false-Zeile. Danach :latest manuell auf v1.1.3 zurückgesetzt.

Was den Fehler so lange unsichtbar gemacht hatte: eine Open-Brain-Note aus der v2.0-Periode, die hartnäckig :latest ist auf v1.2.x“ sagte — längst veraltet. Ich hatte nie wieder ins Docker-Hub-UI geschaut, um den tatsächlichen Stand zu prüfen. Was ein Notiz-System sagt, das man seit dem Aufschreiben nicht mehr angefasst hat, kann längst veraltet sein.

v2.4 — Rendered Markdown View

Dritter Release desselben Tages. Das Ticket: Issue #23, vom Vorabend, von einem fremden User:

“The web GUI currently shows the fetched result as raw Markdown source. Would be great to have an option to view it rendered as actual Markdown in the browser too.”

„Die Web-GUI zeigt das geholte Ergebnis derzeit als rohe Markdown-Quelle. Es wäre toll, eine Option zu haben, es im Browser auch als gerendertes Markdown anzuzeigen.“ — Issue #23

Klein, freundlich, an einem Nachmittag erledigt. Ein Raw/Rendered-Toggle im Result-Header, beide Themes gestylt, Copy-Button kopiert weiterhin Raw. PR #25 squash-merged, Tag v2.4.0 gepusht, Issue geschlossen.

Vor dem Close-Klick habe ich noch einen menschlichen Comment hinterlassen — keine Bot-Standardphrase, sondern eine kurze Notiz, dass ich die Idee gut fand und der Toggle jetzt im neuesten Release drin ist. Bei einem freundlich gestellten Feature-Request kostet das fünf Minuten. Für den Reporter macht es den Unterschied zwischen „jemand hat das tatsächlich gelesen“ und „der Bot hat geantwortet“.

Drei Releases, eine Ursache

Drei Releases an einem Tag, weil die Pipeline endlich frei war.

Was den Stillstand verursacht hatte, war keine Plattform-Allowlist. Es war eine alte WAF-Regel auf meiner eigenen Zone — angelegt für einen Dienst, den ich nicht mehr betreibe, mit einem Use-Case, der längst weggefallen war. Ich hatte sie vergessen. Nicht vergessen wofür sie da war — vergessen dass sie noch lief. Die Regel selbst hatte nichts Falsches getan. Sie hat genau das gemacht, wofür ich sie damals geschrieben hatte. Nur der Rest hatte sich verändert.

„Außerhalb meines Einflusses“ war die falsche Zuschreibung. Die praktische Lehre: bei jedem Symptom, das nach Plattform-Block aussieht, zuerst die eigene Infrastruktur prüfen. Also auch WAF-Custom-Rules mit Country-, ASN- oder IP-Range-Bedingungen, nicht nur Bot Fight Mode.

Eine Regel, deren Use-Case längst weggefallen ist, läuft trotzdem still weiter. Sie fängt irgendwann legitimen Traffic mit, der mit dem ursprünglichen Grund nichts zu tun hat. So altert Infrastruktur, wenn niemand hinschaut.


Teil 8 von 9 der PullMD Serie.

PullMD Teil 7: Spec-Compliant ist nicht genug | PullMD Teil 9: Microsofts MarkItDown integriert — und drei Teile wieder rausgerissen