# 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.
