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
parent
d321719180
commit
a91e8525d2
|
|
@ -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.
|
||||
Loading…
Reference in New Issue