Write down the Hetzner DNS migration before starting it

The root cause of the zone_not_found hunt: the old DNS API is gone. It answers
301 to the web console, and the owner's token already returns 200 against
api.hetzner.cloud — so this is code work, not a credential problem.

The note holds what the next session needs and would otherwise have to rediscover:
the endpoints, taken from cloud.spec.json rather than guessed, the fact that
records are now RRSets addressed by name and type, that a zone can be addressed
by NAME directly, and two traps in the name field — it must not end with the zone
name, which is exactly what the current client sends, and PUT on an rrset sets
labels rather than records.

Recorded rather than started, deliberately. HttpHetznerDnsClient writes the A
record for every customer instance; rewriting it is not something to begin
without room left to test it properly, and a half-migrated DNS client is worse
than a documented broken one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feature/host-bootstrap
nexxo 2026-07-31 15:52:55 +02:00
parent d321719180
commit a91e8525d2
1 changed files with 84 additions and 0 deletions

View File

@ -0,0 +1,84 @@
# Hetzner-DNS: die alte API ist abgeschaltet
**Gemessen am 31. Juli 2026, am Live-Server und von außen.**
```
https://dns.hetzner.com/api/v1/zones → 301 → https://console.hetzner.com/ (HTML)
https://api.hetzner.cloud/v1/zones → 401 ohne Token, 200 mit dem Token des Betreibers
```
Hetzner hat die DNS-Verwaltung am 7. Oktober 2025 in die Cloud-Konsole verlegt.
Die alte DNS-Konsole ging am **20. Mai 2026** in den Nur-Lese-Betrieb; seither
leitet der alte Endpunkt auf die Weboberfläche um. **Alte Token und Zonen-IDs
sind mit der neuen Konsole nicht kompatibel** und wurden nicht übernommen.
## Was dadurch kaputt ist
- `App\Services\Dns\DnsTokenCheck` — meldet `zone_not_found`, obwohl Token und
Zone in Ordnung sind. Seit v1.3.77 meldet es immerhin ehrlich
`zone_list_unreadable` mit dem Anfang der HTML-Antwort.
- `App\Services\Dns\HttpHetznerDnsClient`**das ist das Teure.** Er legt den
A-Eintrag jeder Kundeninstanz an. Solange er auf den alten Endpunkt zeigt,
stirbt jede Bereitstellung in `ConfigureDnsAndTls` — nachdem der Kunde bezahlt
hat. Genau der Satz steht als Begründung auf der Bereitschaftsseite.
- Der Token des Betreibers ist bereits ein Cloud-Token und antwortet mit 200.
Es fehlt **nur** der Code.
## Die neue Schnittstelle
Aus `https://docs.hetzner.cloud/cloud.spec.json` gezogen, nicht geraten.
**Anmeldung:** `Authorization: Bearer <token>` — nicht mehr `Auth-API-Token`.
**Basis:** `https://api.hetzner.cloud/v1`
| Zweck | Aufruf |
|---|---|
| Zonen auflisten | `GET /zones``{"zones":[{id,name,…}], "meta":{…}}` |
| Eine Zone direkt | `GET /zones/{id_or_name}`**der Name geht direkt**, kein Suchen mehr |
| Eintragssatz anlegen | `POST /zones/{zone}/rrsets` |
| Eintragssatz ersetzen | `POST /zones/{zone}/rrsets/{name}/{type}/actions/set_records` |
| Eintragssatz löschen | `DELETE /zones/{zone}/rrsets/{name}/{type}` |
**Das Modell hat sich geändert:** keine einzelnen Records mehr, sondern
**RRSets**, adressiert über `{name}/{typ}`. Ein RRSet trägt mehrere Werte.
`set_records` ersetzt den ganzen Satz — damit wird das Schreiben idempotent, was
der alte Client mit Suchen-und-Aktualisieren nachbauen musste.
Rumpf beim Anlegen, Pflichtfelder `name`, `type`, `records`:
```json
{ "name": "kunde", "type": "A", "ttl": 60,
"records": [{"value": "203.0.113.10"}] }
```
**Zwei Fallen im `name`:**
1. Er darf **nicht** auf den Zonennamen enden. `kunde`, nicht
`kunde.clupilot.cloud` — der alte Client hat den vollen Namen geschickt.
2. Für die Zonenwurzel ist es `@`, und er muss klein geschrieben sein.
`PUT /zones/{zone}/rrsets/{name}/{type}` ist **nicht** das Schreiben der Werte —
es setzt nur `labels`. Wer dort die Records erwartet, schreibt ins Leere.
## Was zu tun ist
1. `HttpHetznerDnsClient` auf Basis, Kopfzeile und RRSets umstellen. Der
Zonen-Lookup entfällt: der Name geht direkt in den Pfad.
2. `DnsTokenCheck` dito. Die Schreibprobe bleibt der Kern der Prüfung — ein
lesender Token sieht sonst genauso aus wie ein schreibender, und der
Unterschied fällt erst auf, wenn ein Kunde bezahlt hat.
3. `FakeHetznerDnsClient` und die Tests nachziehen.
4. Den Namen ohne Zonensuffix bilden — Falle 1 oben. Ein Test, der genau das
festhält, ist billiger als der erste Kunde, dessen Adresse
`kunde.clupilot.cloud.clupilot.cloud` heißt.
## Wieso das so lange gesucht wurde
Die Prüfung stufte nur 401 und 403 als abgelehnt ein und las danach den Rumpf.
Jede andere Antwort — hier eine 301 auf eine HTML-Seite — hatte kein `zones`,
und `?? []` machte daraus „keine Zone in diesem Konto". Eine Aussage über ein
fremdes Hetzner-Konto, hergeleitet aus einer Antwort, die niemand angesehen hat.
v1.3.76 und v1.3.77 haben das behoben; die Meldung zeigt jetzt Status und die
ersten Zeichen der Antwort. Ohne diese zwei Schritte wäre die Ursache weiter im
Dunkeln.