Merge the Hetzner DNS migration note
tests / pest (push) Failing after 8m26s Details
tests / assets (push) Successful in 26s Details
tests / release (push) Has been skipped Details

main
nexxo 2026-07-31 15:52:56 +02:00
commit acc0ac0082
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.