Zertifikats-Spec auf Weg B umgeschrieben, Registereintrag gestrichen
Die Spec fuer die Host-Konsole (docs/superpowers/specs/2026-08-01-host- konsole-zertifikat-design.md) war auf einer eigenen DNS-Zone und Proxmox' Alias-Modus fuer DNS-01 gebaut. Hetzner kennt keine Subzonen (docs.hetzner. com/networking/dns/faq/zones/, Artikel NE-7597D: "Subzones are not supported") -- damit entfallen eigene Zone, CNAME je Host, Alias-Modus und der pro Host verteilte DNS-Token ersatzlos. Der Grund bleibt in der Spec stehen, statt geloescht zu werden, damit niemand denselben Weg in einem halben Jahr erneut vorschlaegt. Weg B, wie im Register vorgegeben: CluPilot stellt zentral aus, DNS-01 ueber den vorhandenen kontoweiten Hetzner-Token, Zertifikat per SSH (RemoteShell::putFile + `pvenode cert set --force --restart`, geprueft gegen die Proxmox-Dokumentation) auf den Host, Erneuerung als geplanter Job auf der Bereitstellungs-Warteschlange (dieselbe Grenze wie SyncVpnPeers -- nur queue-provisioning erreicht einen Host ueber den Tunnel). Dazu ein Vergleich mit der Kundeninstanz (ConfigureDnsAndTls, HTTP-01) und eine genaue Bestandsaufnahme der Bereitschaftsseite: sie kennt heute kein Zertifikat, weder fuer Hosts noch, trotz ersten Anscheins, uebertragbar fuer die Plattform -- CertificateSweep/ProxyHost misst nur oeffentlich erreichbare Namen und laeuft im falschen Container fuer einen Host-FQDN. Im Code bestaetigt und in der Spec vermerkt: RrsetId::zone() ist heute fest auf die Kundenzone verdrahtet, ein Host-FQDN liegt aber in der Plattformzone -- das ist Bauarbeit, keine offene Entscheidung. Offen bleibt nur, welches Werkzeug das ACME-Protokoll auf CluPilot-Seite spricht (keine Bibliothek/kein Tool dafuer im Repo) und ob Plattform- und Kundenzone im selben Hetzner-Projekt liegen -- beides als offene Fragen benannt, keine davon blockiert den Rest des Ablaufs. Registereintrag in OpenWork.php gestrichen: die Spec beschreibt keinen toten Weg mehr, und genau das war der einzige Punkt, den der Eintrag festhielt. Getestet: php artisan test --filter=OpenWork, 8 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
fd216be623
commit
5fe33553ad
|
|
@ -49,13 +49,6 @@ final class OpenWork
|
|||
'missing' => 'Kostet 22,80 € im Monat und verspricht mehr gleichzeitige Bearbeiter und erweiterte Office-Funktionen. Es gibt keine Collabora-Konfiguration, die vom gebuchten Modul abhängt.',
|
||||
'risk' => 'Wie oben: bezahlt und nicht erbracht. Die Paketsperre für Start ist inzwischen gesetzt, das Modul selbst tut weiterhin nichts.',
|
||||
],
|
||||
[
|
||||
'title' => 'Die Zertifikats-Spec beschreibt einen Weg, den es nicht gibt',
|
||||
'state' => self::PLANNED,
|
||||
'where' => 'docs/superpowers/specs/2026-08-01-host-konsole-zertifikat-design.md',
|
||||
'missing' => 'Die Spec ist auf eine eigene Zone und Proxmox\' Alias-Modus gebaut. Hetzner unterstützt keine Subzonen (Artikel NE-7597D), damit entfallen beide ersatzlos. Weg B steht fest: CluPilot stellt zentral aus, DNS-01 über den vorhandenen Token, Zertifikat per SSH auf den Host, Erneuerung als geplanter Lauf mit Restlaufzeit auf der Bereitschaftsseite. Die Spec ist noch nicht umgeschrieben.',
|
||||
'risk' => 'Wer sie heute aufschlägt und loslegt, baut den toten Weg.',
|
||||
],
|
||||
[
|
||||
'title' => 'Der Rettungssystem-Weg liegt still',
|
||||
'state' => self::PARTIAL,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
# Die Host-Oberfläche unter ihrem Namen, mit gültigem Zertifikat, ohne Port
|
||||
|
||||
**Stand:** Entwurf, 1. August 2026 — Punkt 1 beantwortet, Weg entschieden
|
||||
**Stand:** Entwurf, 4. August 2026 — Weg B, ersetzt die Fassung vom 1. August
|
||||
**Auslöser:** gemessen auf `fsn-01`, dem ersten echten Host
|
||||
**Vorherige Fassung:** baute auf einer eigenen DNS-Zone und Proxmox' Alias-Modus
|
||||
für DNS-01. Beides entfällt ersatzlos — siehe „Der Weg, der nicht geht" weiter
|
||||
unten. Diese Fassung beschreibt den Ersatz, nicht eine Variante daneben.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -44,206 +47,416 @@ _acme-challenge.fsn-01.node.clupilot.com TXT "…"
|
|||
|
||||
Wer den setzen kann, besitzt den Namen. Der Host braucht keine öffentliche
|
||||
Adresse, keinen offenen Port, und ein **A-Eintrag ist nicht nötig** — der Name
|
||||
löst weiterhin nur im Tunnel auf `10.66.0.100` auf.
|
||||
löst weiterhin nur im Tunnel auf `10.66.0.100` auf. Diese Begründung stand
|
||||
schon in der ersten Fassung und ändert sich nicht — was sich ändert, ist
|
||||
ausschließlich, **wer** den TXT-Eintrag setzt und **wie** das Zertifikat
|
||||
danach auf den Host kommt.
|
||||
|
||||
Nachzutragen, weil die erste Fassung es stillschweigend voraussetzte:
|
||||
`RegisterHostDns` trägt den Namen eines Hosts **nur** in die tunnelinterne
|
||||
Auflösung ein (den `dns-hosts`-Ordner, den der `vpn-dns`-Container liest — s.
|
||||
`App\Services\Dns\FileHostDnsDirectory`), ausdrücklich **nicht** in die
|
||||
öffentliche Hetzner-Zone. Der Kopfkommentar dieses Schritts nennt den Grund:
|
||||
ein öffentlicher Eintrag verriete jedem Scanner das interne Subnetz und
|
||||
ungefähr, wie viele Hosts dahinterstehen. Für DNS-01 macht das nichts — der
|
||||
einzige öffentliche Spuren, den ein Host hinterlässt, ist der TXT-Eintrag
|
||||
während einer Ausstellung, und der ist danach wieder weg.
|
||||
|
||||
---
|
||||
|
||||
## Der Punkt, der eine Entscheidung brauchte — beantwortet am 1. August 2026
|
||||
## Der Weg, der nicht geht
|
||||
|
||||
Die Frage war: **Lässt sich bei Hetzner ein DNS-Token beschränken?** Denn für
|
||||
DNS-01 muss ein Token auf den Host, und ein kontoweiter Token auf jeder
|
||||
Proxmox-Maschine wäre ein Schlüssel, der weiter reicht als die Maschine.
|
||||
Die erste Fassung baute auf zwei Dingen: einer eigenen DNS-Zone
|
||||
`acme.clupilot.com`, angelegt in einem separaten, sonst leeren
|
||||
Hetzner-Cloud-Projekt, und Proxmox' eingebautem **Alias-Modus** für DNS-01
|
||||
(`pvenode config set -acmedomain0 …,alias=…`). Der Host hätte einen eigenen,
|
||||
auf dieses eine Projekt beschränkten Token bekommen und selbst mit Let's
|
||||
Encrypt gesprochen.
|
||||
|
||||
**Ja — über Projekte, nicht über Zonen.** Und zwar erst seit kurzem.
|
||||
**Hetzner kennt keine Subzonen.** Geprüft an der eigenen Dokumentation:
|
||||
|
||||
Die alte DNS-Konsole (`dns.hetzner.com`) kannte genau einen Tokentyp:
|
||||
kontoweit, alle Zonen, keine Abstufung. Wäre das noch der Stand, hieße die
|
||||
Antwort nein und dieser Entwurf sähe anders aus. Diese Konsole ist am
|
||||
**20. Mai 2026 abgeschaltet** worden; `HttpHetznerDnsClient` fährt seit dem
|
||||
Umbau vom 31. Juli auf `api.hetzner.cloud/v1`.
|
||||
> „Subzones are not supported." — Hetzner Docs, Artikel NE-7597D
|
||||
> (<https://docs.hetzner.com/networking/dns/faq/zones/>)
|
||||
|
||||
Im neuen Modell liegen DNS-Zonen **in Cloud-Projekten**, und Token sind
|
||||
projektweit. Hetzner nennt das ausdrücklich als den vorgesehenen Weg:
|
||||
Eine eigene Zone für `acme.clupilot.com` unterhalb von `clupilot.com` lässt
|
||||
sich bei Hetzner nicht anlegen — der Versuch kommt mit einem Fehler zurück
|
||||
(von Betroffenen berichtet als `422 invalid TLD`), nicht mit einer Zone. Ohne
|
||||
diese Zone gibt es keinen Alias-Ziel-Namen, ohne den wiederum ergibt Proxmox'
|
||||
Alias-Modus keinen Sinn, und ohne den Alias-Modus fällt der ganze Zweck des
|
||||
separaten Projekts weg — er sollte ja genau verhindern, dass ein Host-Token
|
||||
die eigentliche Zone lesen oder schreiben kann. Das Gerüst trägt sich nicht
|
||||
mehr selbst, sobald der unterste Stein fehlt.
|
||||
|
||||
> „As API tokens are scoped per project, you can restrict access to specific
|
||||
> zones by placing them in separate projects."
|
||||
**Das gehört hier stehen, nicht gelöscht:** Der Gedanke — ein Token pro Host,
|
||||
beschränkt auf eine Zone, die nichts Wertvolles enthält — war richtig gedacht
|
||||
für ein Problem, das es so bei Hetzner nicht lösen lässt. Wer in einem halben
|
||||
Jahr denselben Gedanken noch einmal hat, soll hier lesen können, woran er
|
||||
das letzte Mal gescheitert ist, statt ihn noch einmal zu bauen.
|
||||
|
||||
### Der Haken, der bleibt
|
||||
|
||||
**Ein Projekt-Token ist kein DNS-Token.** Es hat genau zwei Achsen: Projekt und
|
||||
Lesen/Schreiben. Ein Schreibtoken darf in seinem Projekt alles, was die
|
||||
Cloud-API kann — auch Server anlegen. Eine Beschränkung auf einen Ressourcentyp
|
||||
gibt es nicht.
|
||||
|
||||
Daraus folgt die Regel für das Projekt: **es enthält nichts außer der einen
|
||||
Zone.** Keine Server, keine Netze, kein Speicher. Dann ist der Schaden eines
|
||||
gestohlenen Tokens auf diese Zone begrenzt, plus Kosten in einem sonst leeren
|
||||
Projekt.
|
||||
|
||||
### Und die Zone, die er erreicht, enthält nichts Wichtiges
|
||||
|
||||
Der Token wird nicht auf `node.clupilot.com` ausgestellt, sondern auf eine
|
||||
eigene Zone `acme.clupilot.com`, die **nichts enthält als Challenge-Einträge**.
|
||||
Proxmox kann das von Haus aus — Alias-Modus:
|
||||
|
||||
```
|
||||
pvenode config set -acmedomain0 \
|
||||
domain=fsn-01.node.clupilot.com,alias=fsn-01.acme.clupilot.com,plugin=hetznercloud
|
||||
```
|
||||
|
||||
Dazu ein fester CNAME im öffentlichen DNS, einmal je Host:
|
||||
|
||||
```
|
||||
_acme-challenge.fsn-01.node.clupilot.com CNAME _acme-challenge.fsn-01.acme.clupilot.com
|
||||
```
|
||||
|
||||
Let's Encrypt folgt dem CNAME bei der Prüfung; der Host schreibt nur in die
|
||||
Alias-Zone. **Ein kompromittierter Host erreicht `node.clupilot.com` damit gar
|
||||
nicht mehr** — auch nicht lesend. Das ist der Unterschied zwischen „der
|
||||
Schlüssel kann nur eine Zone ändern" und „die Zone, die er ändern kann, ist
|
||||
wertlos".
|
||||
|
||||
Der CNAME kostet nichts: `RegisterHostDns` legt ohnehin Einträge für den Host
|
||||
an, mit dem zentralen Token. Ein Eintrag mehr, an der Stelle, die es schon tut.
|
||||
|
||||
### Was damit vom Tisch ist
|
||||
|
||||
Weg 3 aus dem ursprünglichen Entwurf — CluPilot setzt den TXT selbst und
|
||||
übernimmt die Erneuerung — wird **nicht** gebaut. Er wäre eine zweite Stelle,
|
||||
die still ausfallen kann, und Zertifikate laufen nach 90 Tagen ab.
|
||||
Mit der Zone fallen ersatzlos weg: das eigene ACME-Projekt, der CNAME je Host,
|
||||
der Proxmox-Alias-Modus, der pro Host verteilte `hetznercloud`-DNS-Token, und
|
||||
die Versionsprüfung auf `libproxmox-acme-plugins` ≥ 1.7.1 (die existierte nur,
|
||||
weil dieses Plugin den Alias-Modus sprechen musste).
|
||||
|
||||
---
|
||||
|
||||
## Was einmal von Hand eingerichtet wird
|
||||
## Weg B: zentral ausstellen, statt vor Ort
|
||||
|
||||
Nicht pro Host, nicht durch die Pipeline. Einmal, vom Betreiber, bevor der
|
||||
Schritt überhaupt laufen kann:
|
||||
Vom Betreiber entschieden. In einem Satz: **CluPilot stellt das Zertifikat
|
||||
selbst aus** — über DNS-01, mit dem Token, der ohnehin schon zentral liegt —
|
||||
**und liefert es per SSH an den Host aus.** Kein Token verlässt CluPilot,
|
||||
kein Host spricht je mit Let's Encrypt oder mit der Hetzner-API.
|
||||
|
||||
1. Ein eigenes Hetzner-Cloud-Projekt, z. B. `clupilot-acme`. **Leer** — nur die
|
||||
Zone darin.
|
||||
2. Zone `acme.clupilot.com` in diesem Projekt anlegen und beim Registrar auf
|
||||
Hetzners Nameserver delegieren.
|
||||
3. API-Token für dieses Projekt, Lesen **und** Schreiben. Roher Wert, ohne
|
||||
Anführungszeichen.
|
||||
4. Der Token wird als **eigener Schlüssel** hinterlegt, nicht als der aus
|
||||
`provisioning.dns.token` — der ist kontoweit und bleibt zentral. Zwei
|
||||
Schlüssel, zwei Reichweiten; wer sie zusammenlegt, hebt die ganze
|
||||
Entscheidung oben wieder auf.
|
||||
Das ist nicht nur der Ausweg aus der toten Zonen-Idee — es ist der
|
||||
einfachere Weg ohnehin. Was in der ersten Fassung als Sicherheitsgewinn
|
||||
gedacht war (ein Token, der nur eine wertlose Zone erreicht, falls ein Host
|
||||
kompromittiert wird), erreicht Weg B direkter: **gar kein Token auf dem
|
||||
Host.** Ein kompromittierter Host hat nichts, was er gegen die Hetzner-API
|
||||
verwenden könnte — weil dort nie etwas lag.
|
||||
|
||||
Fehlt einer dieser vier Punkte, muss der Schritt mit einer klaren Meldung
|
||||
stehenbleiben und nicht auf den kontoweiten Token zurückfallen.
|
||||
### Wer stellt aus, und womit weist er sich aus
|
||||
|
||||
---
|
||||
CluPilot selbst, mit dem Token, der schon existiert:
|
||||
`provisioning.dns.token` / `HETZNER_DNS_TOKEN`, im Tresor unter dem Schlüssel
|
||||
`dns.token`. Das ist derselbe Token, den `App\Services\Dns\HttpHetznerDnsClient`
|
||||
heute für die A-Einträge der Kundeninstanzen benutzt, und dessen
|
||||
Schreibfähigkeit `App\Services\Dns\DnsTokenCheck` bereits nachweist — indem
|
||||
sie einen echten TXT-Eintrag anlegt und wieder löscht. Das ist praktisch
|
||||
genau die Bewegung, die eine DNS-01-Antwort auch macht: TXT anlegen, Let's
|
||||
Encrypt prüfen lassen, TXT wieder entfernen.
|
||||
|
||||
## Die zwei Teile
|
||||
**Ein Unterschied ist zu bauen, kein Unterschied ist zu entscheiden:**
|
||||
`HttpHetznerDnsClient` und die Adressierung darunter
|
||||
(`App\Services\Dns\RrsetId::zone()`) sind heute fest auf **eine** Zone
|
||||
verdrahtet — `ProvisioningSettings::dnsZone()`, die **Kundenzone**
|
||||
(`clupilot.cloud`). Ein Aufruf mit einem Namen aus einer anderen Zone wirft
|
||||
eine `RuntimeException`, mit Absicht (s. `RrsetId::relativeName()` — genau
|
||||
diese Falle hat schon einmal einen Kundennamen doppelt an die Zone
|
||||
angehängt). Ein Host-FQDN liegt aber in der **Plattformzone**
|
||||
(`provisioning.dns.platform_zone`, Vorgabe `clupilot.com`, abgeleitet aus
|
||||
`APP_URL`) — einer anderen Zone als der, die der Client heute erreichen
|
||||
kann. Bevor CluPilot einen TXT-Eintrag für `_acme-challenge.fsn-01.node`
|
||||
schreiben kann, braucht es entweder eine zonenparametrisierte Fassung dieses
|
||||
Clients oder einen zweiten, auf die Plattformzone gerichteten Weg. Das ist
|
||||
Handwerk, keine offene Frage — der Token selbst ist, wie im Register
|
||||
festgehalten, kontoweit und nicht auf eine Zone beschränkt (bestätigt am
|
||||
31. Juli beim Umzug auf die Cloud-API: „Der Token des Betreibers ist bereits
|
||||
ein Cloud-Token und antwortet mit 200" auf `GET /zones`, das mehr als eine
|
||||
Zone auflistet).
|
||||
|
||||
### 1. Zertifikat — eingerichtet in Proxmox, nicht in CluPilot
|
||||
**Ungeprüft, und das gehört als offene Frage stehen:** ob die Plattformzone
|
||||
`clupilot.com` im selben Hetzner-Cloud-**Projekt** liegt wie die Kundenzone
|
||||
`clupilot.cloud`, gegen die der Token heute schon schreibt. Token sind bei
|
||||
Hetzner projektweit, nicht zonenweit (dieselbe Erkenntnis, an der die erste
|
||||
Fassung ihre Entscheidung für Projekte statt Zonen festmachte). Liegen beide
|
||||
Zonen im selben Projekt, reicht der vorhandene Token ohne Weiteres. Liegen
|
||||
sie in getrennten Projekten, braucht die Plattformzone entweder einen
|
||||
eigenen Token oder muss ins selbe Projekt wie die Kundenzone wandern. Das
|
||||
lässt sich mit einem einzigen Schreibversuch klären — nach demselben Muster
|
||||
wie `DnsTokenCheck`, nur gegen `clupilot.com` statt `clupilot.cloud` — und
|
||||
gehört **vor** der ersten Implementierungsstunde erledigt, nicht danach.
|
||||
|
||||
Proxmox bringt ACME mit und kann DNS-01. Der Schritt richtet über SSH ein:
|
||||
### Wie das Zertifikat entsteht
|
||||
|
||||
- ACME-Konto (Kontaktadresse aus den Einstellungen)
|
||||
- DNS-Modul `hetznercloud` mit dem Token des ACME-Projekts, Variable
|
||||
`HETZNER_TOKEN`
|
||||
- Zertifikatsauftrag für den FQDN des Hosts, **im Alias-Modus** (siehe oben)
|
||||
DNS-01 ist ein Protokoll, kein einzelner Aufruf: ACME-Konto (oder
|
||||
Wiederverwendung eines bestehenden), Order für den FQDN des Hosts,
|
||||
Autorisierung anfordern, den verlangten TXT-Wert unter
|
||||
`_acme-challenge.<fqdn>` in die Plattformzone schreiben, Let's Encrypt zur
|
||||
Prüfung auffordern, auf `valid` pollen, TXT-Eintrag wieder entfernen, CSR
|
||||
einreichen, Zertifikat abholen.
|
||||
|
||||
**Warum in Proxmox und nicht in CluPilot:** Proxmox erneuert danach selbst,
|
||||
über seinen täglichen Zeitgeber. Eine Erneuerung, die in CluPilot hinge, wäre
|
||||
eine zweite Stelle, die still ausfallen kann — und Zertifikate laufen nach 90
|
||||
Tagen ab. Das merkt man dann an einem Dienstag um sieben.
|
||||
**Wer das auf CluPilot-Seite spricht, ist offen — und gehört als offene
|
||||
Frage benannt, nicht stillschweigend entschieden.** Im Repo gibt es dafür
|
||||
heute nichts: kein ACME-Paket in `composer.json`, kein `certbot`/`acme.sh`/
|
||||
`lego` in `docker/` oder `deploy/`. Das einzige ACME-Wissen, das existiert,
|
||||
steckt in Traefik (`deploy/bootstrap/lib/traefik.sh`), und das läuft **auf
|
||||
jedem Host selbst**, spricht nur HTTP-01 für Kundennamen und kommt für
|
||||
diesen Zweck nicht infrage — Weg B verlangt ausdrücklich, dass kein Host je
|
||||
mit Let's Encrypt spricht. Zwei Richtungen liegen offen: eine gepflegte
|
||||
PHP-ACME-Bibliothek mit DNS-01-Unterstützung im App-Container, oder ein
|
||||
Werkzeug wie `certbot`/`acme.sh` dort, gesteuert über einen Hook, der in den
|
||||
oben beschriebenen (noch zu bauenden) plattformzonenfähigen DNS-Schreibweg
|
||||
ruft. Das ist keine Entscheidung, die den Betreiber braucht — es ist eine
|
||||
gewöhnliche technische Abwägung, wie sie in diesem Projekt sonst beim Bauen
|
||||
fällt, nicht beim Entwerfen. Sie blockiert diese Spezifikation nicht, weil
|
||||
der Rest des Ablaufs — wo das Ergebnis herkommt, wo es hingeht, wer es prüft
|
||||
— davon unabhängig steht.
|
||||
|
||||
**Das Modul heißt `hetznercloud`, nicht `hetzner`.** Das alte `hetzner` spricht
|
||||
mit `dns.hetzner.com/api/v1` und ist damit tot. Ein Host, auf dem noch das alte
|
||||
Modul konfiguriert ist, muss es losbekommen, bevor das neue greift.
|
||||
### Wie das Zertifikat auf den Host kommt
|
||||
|
||||
**Vor allem anderen: Version prüfen.** Das Modul `hetznercloud` kam erst mit
|
||||
`libproxmox-acme-plugins` **1.7.1**. Darunter existiert es nicht — und der
|
||||
Schritt scheitert dann nicht beim Einrichten, sondern erst beim Ausstellen,
|
||||
also spät und mit einer Meldung, die nicht danach aussieht. Deshalb steht die
|
||||
Prüfung **an den Anfang** des Schritts, nicht ans Ende:
|
||||
Über dieselbe Verbindung, mit der CluPilot den Host ohnehin schon erreicht.
|
||||
Jeder Host-Schritt in der Übernahme-Pipeline meldet sich über
|
||||
`App\Services\Ssh\RemoteShell`, mit dem Flottenschlüssel aus dem Tresor
|
||||
(`ssh.private_key`) und gepinnt auf den Fingerabdruck, den
|
||||
`EstablishSshTrust` beim ersten Kontakt festgehalten hat
|
||||
(`host.ssh_host_key`) — genau das Muster in `HostStep::keyLogin()`. Für eine
|
||||
Erneuerung nach der Übernahme gilt dieselbe Regel: über die
|
||||
WireGuard-Adresse, sobald der Tunnel einmal bewiesen ist, was nach der
|
||||
Übernahme immer der Fall ist.
|
||||
|
||||
Zertifikat und Schlüssel gehen über `RemoteShell::putFile()` auf den Host,
|
||||
danach installiert ein einziger Proxmox-eigener Befehl beides und lädt
|
||||
`pveproxy` neu:
|
||||
|
||||
```
|
||||
dpkg-query -W -f='${Version}' libproxmox-acme-plugins
|
||||
pvenode cert set <zertifikatsdatei> <schluesseldatei> --force --restart
|
||||
```
|
||||
|
||||
- ≥ 1.7.1 → weiter
|
||||
- darunter → aktualisieren; geht das nicht, **abbrechen mit klarer Meldung**.
|
||||
Kein Nachrüsten der Datei von Hand nach `/usr/share/proxmox-acme/dnsapi/` —
|
||||
das überlebt kein Paket-Update und hinterlässt einen Host, der sich von den
|
||||
anderen unterscheidet, ohne dass es jemand aufgeschrieben hat.
|
||||
Geprüft gegen die Proxmox-eigene Dokumentation (`pvenode(1)`, Stand August
|
||||
2026): `<certificates>` ist die PEM-kodierte Zertifikatskette,
|
||||
`<key>` der PEM-kodierte, **unverschlüsselte** private Schlüssel — Proxmox
|
||||
verlangt das ausdrücklich, ein Passwort auf der Schlüsseldatei lässt sich
|
||||
nicht verwenden. `--force` überschreibt ein vorhandenes Zertifikat (egal ob
|
||||
von Hand oder per ACME gesetzt), `--restart` lädt `pveproxy` im selben
|
||||
Aufruf neu. Kein separates `systemctl reload`, kein ACME-Plugin, keine
|
||||
Versionsprüfung wie bei `libproxmox-acme-plugins` — das ist ein Kernbefehl
|
||||
von `pvenode`, keine Zusatzfunktion. Ziel auf der Platte ist
|
||||
`/etc/pve/local/pveproxy-ssl.pem` und `…-ssl.key`, dieselbe Stelle, die die
|
||||
erste Fassung schon für den ausgelesenen Proxy-Zustand nannte.
|
||||
|
||||
Nach dem Einrichten `pveproxy` und `pvedaemon` neu starten, sonst kennt die
|
||||
Oberfläche das Modul nicht.
|
||||
Das läuft im `queue-provisioning`-Container und nirgendwo sonst — er ist der
|
||||
einzige, der im Namensraum von `wg0` steht; jeder Verbindungsversuch aus dem
|
||||
`app`-Container läuft nachweislich in eine Zeitüberschreitung (so
|
||||
dokumentiert an `vpn-gateway` im `docker-compose.yml`). Dieselbe Grenze gilt
|
||||
für jede Wartung, die einen Host über die Tunneladresse erreichen muss —
|
||||
`SyncVpnPeers` und `SyncMonitoringStatus` sind bereits nach diesem Muster
|
||||
gebaut (`Schedule::job(...)`, ausgeführt auf der Bereitstellungs-Warteschlange,
|
||||
nicht im `scheduler`-Container).
|
||||
|
||||
### 2. Port — ein Proxy, gebunden an den Tunnel
|
||||
### Wer erneuert es, und wie oft
|
||||
|
||||
443 → 127.0.0.1:8006, **nur auf der Tunneladresse lauschend**, nicht auf der
|
||||
öffentlichen. `deploy/bootstrap/lib/traefik.sh` liegt bereits im Repo.
|
||||
Ein geplanter Lauf, kein manueller. Nach demselben Muster wie
|
||||
`SyncVpnPeers`/`SyncMonitoringStatus`: ein Job, eingetragen in
|
||||
`routes/console.php` über `Schedule::job(...)`, ausgeführt auf der
|
||||
Bereitstellungs-Warteschlange, damit er den Tunnel benutzen kann. Anders als
|
||||
`clupilot:check-certificates` (das nur öffentliche Plattformnamen misst und
|
||||
deshalb im `scheduler`-Container laufen darf) braucht dieser Lauf SSH auf
|
||||
jeden Host.
|
||||
|
||||
Das Zertifikat aus Teil 1 wird von Proxmox verwaltet; der Proxy liest es von
|
||||
dort (`/etc/pve/local/pveproxy-ssl.pem`) oder reicht TLS unverändert durch.
|
||||
Zwei Wege, einer davon ist zu wählen — durchreichen ist einfacher und hat
|
||||
keine zweite Kopie des Schlüssels.
|
||||
Idempotent geprüft, nicht blind ausgestellt: bevor irgendetwas passiert,
|
||||
liest der Lauf die Restlaufzeit des Zertifikats, das gerade auf dem Host
|
||||
liegt (per SSH, z. B. aus `pvenode cert info` oder direkt aus der
|
||||
PEM-Datei). Bleibt genug Zeit, passiert nichts. Ein sinnvoller Schwellwert
|
||||
ist derselbe, den `App\Models\ProxyHost::isExpiringSoon()` schon für die
|
||||
Plattformnamen benutzt — dreißig Tage, mit derselben Begründung: bei
|
||||
neunziger Laufzeit hätte eine funktionierende Erneuerung bei dreißig
|
||||
verbleibenden Tagen längst gegriffen.
|
||||
|
||||
## Wo der Schritt hingehört
|
||||
### Woran der Betreiber sieht, dass die Erneuerung läuft
|
||||
|
||||
Nach `RegisterHostDns` — der Name muss registriert sein, bevor ein Zertifikat
|
||||
auf ihn ausgestellt wird, und der CNAME in die Alias-Zone entsteht dort. Vor
|
||||
`SecureHostFirewall` ist nicht nötig (DNS-01 braucht nichts Eingehendes), aber
|
||||
es schadet auch nicht.
|
||||
**Heute steht dazu nichts auf der Bereitschaftsseite — für keinen der beiden
|
||||
Zertifikatswege.** Das war zu prüfen, nicht anzunehmen, und das Ergebnis ist
|
||||
eindeutig: keine der sechs `App\Support\Readiness\*Checks`-Klassen erwähnt
|
||||
ein Zertifikat, `CertificateInspector` oder `ProxyHost`.
|
||||
|
||||
Was es **gibt**, ist ein anderer, bereits fertiger Mechanismus für eine
|
||||
**andere** Frage: `App\Services\Proxy\CertificateSweep` misst täglich
|
||||
(`clupilot:check-certificates`, `Schedule::command(...)->dailyAt('04:17')`)
|
||||
die Zertifikate der **Plattform-eigenen** Namen (Portal, Konsole, Status,
|
||||
Dateien — aus `App\Support\KnownHostnames`) und legt das Ergebnis in
|
||||
`ProxyHost` ab, sichtbar auf einer eigenen Seite, `Admin\ProxyHosts` unter
|
||||
`/admin/proxy-hosts`. Das ist **nicht** die Bereitschaftsseite, und es ist
|
||||
auch technisch nicht auf einen Host übertragbar: `CertificateInspector`
|
||||
misst von außen, mit einem einfachen TLS-Handschlag gegen Port 443 des
|
||||
Namens — und ein Host-FQDN wie `fsn-01.node.clupilot.com` löst außerhalb des
|
||||
Tunnels **gar nicht** auf. Der Lauf, der `clupilot:check-certificates`
|
||||
ausführt, sitzt zudem im `scheduler`-Container, der den Tunnel nicht
|
||||
erreicht. Diesen Mechanismus für Hosts wiederzuverwenden hieße, ihn an genau
|
||||
der Stelle zu brechen, die ihn heute trägt.
|
||||
|
||||
Was für Weg B fehlt, ist deshalb neu und eigenständig, nicht eine Erweiterung
|
||||
von `CertificateSweep`:
|
||||
|
||||
- Der Erneuerungslauf selbst kennt die Restlaufzeit schon — er hat sie gerade
|
||||
per SSH gelesen. Sie muss **je Host** abgelegt werden (nach dem Vorbild von
|
||||
`ProxyHost::certificate_expires_at`, aber an einer Stelle, die zum Host
|
||||
gehört, nicht zu einem einzelnen globalen Zeitstempel wie bei den beiden
|
||||
Heartbeats in `OperationChecks` — es gibt viele Hosts, nicht einen Dienst).
|
||||
- Eine neue `Check`-Klasse, die dieselbe Bauweise wie
|
||||
`App\Support\Readiness\SecurityChecks` und `OperationChecks` benutzt: eine
|
||||
Warnung (nicht blockierend — ein einzelner Host mit knapper Restlaufzeit
|
||||
darf keine ganze Installation als „nicht bereit" markieren), sichtbar,
|
||||
sobald ein Host unter die Dreißig-Tage-Schwelle fällt.
|
||||
- **Getrennt davon** — dieselbe Unterscheidung, die `OperationChecks` schon
|
||||
für Zeitplaner und Warteschlangen-Arbeiter trifft —, ob der Lauf selbst
|
||||
überhaupt noch stattfindet. Ein Job, der seit zwei Wochen nicht mehr lief,
|
||||
sagt „alles grün", weil er die letzte gemessene Restlaufzeit einfach
|
||||
stehen lässt. Das ist der gefährlichere der beiden Fehler, weil er sich
|
||||
nicht von einem echten guten Zustand unterscheidet, ohne dass man das Datum
|
||||
der letzten Messung mit ansieht.
|
||||
|
||||
## Vergleich mit der Kundeninstanz
|
||||
|
||||
`App\Provisioning\Steps\Customer\ConfigureDnsAndTls` löst ein ähnliches
|
||||
Problem für eine Kundeninstanz — Hostname, Zertifikat, ohne dass der Kunde
|
||||
etwas tun muss. Der Vergleich zeigt, was sich überträgt und was nicht:
|
||||
|
||||
| | Kundeninstanz | Host-Konsole (Weg B) |
|
||||
|---|---|---|
|
||||
| Herausforderer | HTTP-01, über Traefik auf dem Host | DNS-01, zentral von CluPilot |
|
||||
| Warum dieser Weg | Kundenname liegt öffentlich auf Port 80/443 — HTTP-01 ist der einfachste Weg, der dort funktioniert | Host-Konsole hat **nie** einen offenen Port 80 — HTTP-01 geht nicht |
|
||||
| Wer stellt aus | Traefik/lego, auf dem Host selbst | CluPilot zentral, nie der Host |
|
||||
| Womit weist sich der Aussteller aus | nichts — der Port ist der Beweis | Hetzner-Token, kontoweit, nur bei CluPilot |
|
||||
| Was bei Fehlschlag passiert | Lauf geht trotzdem weiter (`domain_cert_ok=false`, Ereignis vermerkt) — ein Kunde, der noch keinen A-Eintrag gesetzt hat, darf seine Cloud trotzdem bekommen | muss auffallen — die Konsole ist ein täglich gebrauchtes Werkzeug, kein Kundenversprechen mit Kulanzfrist |
|
||||
| Erneuerung | Traefiks eigener ACME-Zeitgeber, pro Host | ein CluPilot-eigener geplanter Lauf, fleetweit |
|
||||
|
||||
Der auffälligste Unterschied ist der letzte in der Tabelle. Bei einer
|
||||
Kundeninstanz ist ein fehlendes Zertifikat ein Zustand, den man anzeigt und
|
||||
mit dem man wartet — es kostet niemanden etwas, solange der Kunde seinen
|
||||
A-Eintrag noch nicht gesetzt hat. Bei der Host-Konsole gibt es diese
|
||||
Kulanz nicht: ein abgelaufenes Zertifikat dort ist ein Betreiber, der seine
|
||||
eigene Infrastruktur nicht mehr ohne Warnklick erreicht. Das ist der Grund,
|
||||
warum Weg B eine sichtbare, geprüfte Restlaufzeit braucht und die
|
||||
Kundeninstanz-Logik das nicht in gleicher Form tut.
|
||||
|
||||
## Was der Host braucht, was CluPilot braucht
|
||||
|
||||
**Der Host braucht:**
|
||||
|
||||
- Eine funktionierende WireGuard-Tunneladresse und einen bewiesenen
|
||||
Handschlag (`wg_peer`-Breadcrumb) — beides existiert bereits vor diesem
|
||||
Schritt, aus dem Rest der Übernahme.
|
||||
- Den CluPilot-Flottenschlüssel als vertrauenswürdig (`EstablishSshTrust`,
|
||||
bereits gebaut).
|
||||
- Eine Proxmox-Version, deren `pvenode cert set --force --restart` sich wie
|
||||
dokumentiert verhält. Nicht unabhängig gegen den echten Fuhrpark geprüft —
|
||||
`hosts.pve_version` liegt bereits als Feld vor und eignet sich für eine
|
||||
solche Prüfung, falls sich beim Bauen ein Unterschied zwischen den
|
||||
eingesetzten PVE-Ständen zeigt.
|
||||
- **Nichts**, was heute nicht schon da ist. Kein DNS-Token, kein
|
||||
ACME-Plugin, kein CNAME.
|
||||
|
||||
**CluPilot braucht:**
|
||||
|
||||
- Den vorhandenen Hetzner-Token, mit Schreibzugriff auf die Plattformzone —
|
||||
zu prüfen wie oben beschrieben.
|
||||
- Eine plattformzonenfähige Fassung des DNS-Schreibwegs (heute fest auf die
|
||||
Kundenzone verdrahtet, s. oben).
|
||||
- Ein Werkzeug, das ACME/DNS-01 spricht — offen, s. oben, blockiert aber
|
||||
nicht das Schreiben dieser Spezifikation.
|
||||
- Einen geplanten Job auf der Bereitstellungs-Warteschlange, der Restlaufzeit
|
||||
liest, bei Bedarf erneuert, das Ergebnis je Host ablegt.
|
||||
- Eine neue Prüfung auf der Bereitschaftsseite, die dieses Ergebnis zeigt.
|
||||
|
||||
## Der Proxy — Port 443 ohne Port 8006
|
||||
|
||||
Dieser Teil der ersten Fassung bleibt inhaltlich stehen; Weg B vereinfacht
|
||||
ihn sogar. 443 → 127.0.0.1:8006, **nur auf der Tunneladresse lauschend**,
|
||||
über den Traefik-Dienst, den `deploy/bootstrap/lib/traefik.sh` bereits auf
|
||||
jedem Host installiert (dort heute zuständig für die HTTP-01-Zertifikate der
|
||||
Kundeninstanzen auf demselben Host).
|
||||
|
||||
Die erste Fassung ließ offen, ob dieser Traefik das Zertifikat aus
|
||||
`/etc/pve/local/pveproxy-ssl.pem` selbst einliest oder TLS unverändert
|
||||
durchreicht, mit der Empfehlung „durchreichen ist einfacher". Unter Weg B
|
||||
ist die Frage entschieden, nicht mehr offen: `pvenode cert set` installiert
|
||||
das Zertifikat **in Proxmox**, das ist der ganze Witz des Befehls — pveproxy
|
||||
hält und liefert es. Es ein zweites Mal in Traefik einzulesen wäre eine
|
||||
zweite Kopie desselben privaten Schlüssels, ohne Gegenwert. **Durchreichen
|
||||
ist damit nicht mehr die einfachere von zwei Optionen, sondern die einzig
|
||||
sinnvolle.**
|
||||
|
||||
## Wo der Schritt in der Pipeline hingehört
|
||||
|
||||
Unverändert gegenüber der ersten Fassung: nach `RegisterHostDns` — der Name
|
||||
muss feststehen, bevor ein Zertifikat auf ihn ausgestellt wird. Vor
|
||||
`SecureHostFirewall` ist weiterhin nicht nötig (DNS-01 braucht nichts
|
||||
Eingehendes), schadet aber auch nicht.
|
||||
|
||||
```
|
||||
… RegisterHostDns → SecureHostConsole → RegisterCapacity → SecureHostFirewall …
|
||||
… RegisterHostDns → SecureHostConsole (neu, Weg B) → RegisterCapacity → SecureHostFirewall …
|
||||
```
|
||||
|
||||
`RegisterHostDns` bekommt eine Aufgabe dazu: den festen CNAME
|
||||
`_acme-challenge.<host>.node.clupilot.com` → `_acme-challenge.<host>.acme.clupilot.com`,
|
||||
gesetzt mit dem **zentralen** Token. Er ist dauerhaft und wird nie wieder
|
||||
angefasst.
|
||||
Der neue Schritt ruft dieselbe Ausstellungslogik, die später auch der
|
||||
geplante Erneuerungslauf benutzt — ein Weg, zwei Aufrufer, damit Ausstellung
|
||||
bei der Übernahme und Erneuerung danach nicht auseinanderlaufen.
|
||||
|
||||
## Wiederholbarkeit
|
||||
|
||||
- `libproxmox-acme-plugins` < 1.7.1 → abbrechen, bevor irgendetwas geändert wird
|
||||
- Zertifikat vorhanden und gültig → nichts tun, `advance()`
|
||||
- Zertifikat auf dem Host noch gültig und weit genug von der
|
||||
Erneuerungsschwelle entfernt → nichts tun
|
||||
- Proxy schon eingerichtet und läuft → nichts tun
|
||||
- ACME-Modul schon mit demselben Token eingerichtet → nichts tun; bei anderem
|
||||
Token überschreiben, nicht ein zweites danebenstellen
|
||||
- CNAME existiert schon → nichts tun (`RegisterHostDns` ist ohnehin schon so
|
||||
gebaut)
|
||||
- Beides prüfen, nicht annehmen: `pvenode cert info` sagt, was wirklich liegt
|
||||
- TXT-Eintrag für eine laufende Ausstellung existiert schon (z. B. nach
|
||||
einem abgebrochenen Lauf) → ersetzen, nicht daneben anlegen — dieselbe
|
||||
RRSet-Ersetzungslogik, die `HttpHetznerDnsClient` für Kundeneinträge schon
|
||||
benutzt
|
||||
- Beides prüfen, nicht annehmen: `pvenode cert info` sagt, was auf dem Host
|
||||
wirklich liegt, unabhängig davon, was CluPilot zuletzt geschrieben zu
|
||||
haben glaubt
|
||||
|
||||
Dieselbe Regel wie überall in dieser Pipeline — ein Wiederanlauf darf nichts
|
||||
zerstören, was funktioniert.
|
||||
## Wo es schiefgehen kann, und was dann passiert
|
||||
|
||||
Ein Zertifikat, das still abläuft, ist die teuerste Fehlerart hier — es
|
||||
merkt niemand, bis der Betreiber selbst ausgesperrt ist. Deshalb:
|
||||
|
||||
- **Der DNS-Schreibversuch schlägt fehl** (Netzwerk, abgelaufener Token,
|
||||
falsches Projekt). Der Lauf bricht diesen Host ab, ohne das
|
||||
Vorgängerzertifikat anzurühren — ein Host mit einem noch gültigen, nur
|
||||
nicht mehr ganz frischen Zertifikat ist besser als einer ohne. Das
|
||||
Ereignis wird protokolliert, mit dem Hostnamen darin, nicht nur „DNS
|
||||
fehlgeschlagen".
|
||||
- **Let's Encrypt lehnt ab** (Rate-Limit, Kontoproblem). Gleiche Reaktion:
|
||||
bestehendes Zertifikat bleibt, Fehlschlag wird sichtbar vermerkt statt
|
||||
verschluckt.
|
||||
- **SSH auf den Host schlägt fehl** (Host offline, Tunnel gerade nicht
|
||||
bewiesen). Das neue Zertifikat existiert dann bei CluPilot, aber nicht auf
|
||||
dem Host — der nächste Lauf versucht es erneut, mit demselben oder einem
|
||||
neu ausgestellten Zertifikat, je nachdem wie weit die vorherige
|
||||
Restlaufzeit inzwischen gesunken ist.
|
||||
- **Der geplante Lauf selbst bleibt aus** (Warteschlangen-Arbeiter tot,
|
||||
Deploy vergessen). Das ist der Fall, den eine reine
|
||||
Restlaufzeit-Anzeige **nicht** fängt — ein Wert, der einfach stehen
|
||||
bleibt, sieht aus wie ein gesunder. Deshalb gehört ein zweiter,
|
||||
unabhängiger Blick auf „lief der Lauf kürzlich" dazu, nach demselben
|
||||
Muster wie die beiden Heartbeats in `OperationChecks`.
|
||||
- **Ein Zertifikat läuft trotzdem ab**, weil alles oben zusammen zu lange
|
||||
nicht griff. Dann bricht der TLS-Handschlag auf der Konsole, mit einer
|
||||
Warnung, die der Betreiber sieht — schlecht, aber sichtbar, und genau der
|
||||
Zustand von heute, den dieser Entwurf beheben soll. Der Unterschied ist:
|
||||
mit der Bereitschaftsprüfung hätte er es dreißig Tage vorher schon
|
||||
gesehen.
|
||||
|
||||
## Was ausdrücklich nicht gebaut wird
|
||||
|
||||
- **Kein Token auf dem Host.** Weder ein kontoweiter noch ein
|
||||
projektbeschränkter — das war der ganze Punkt der ersten Fassung, und Weg
|
||||
B erreicht es ohne die Zone, die dafür nötig gewesen wäre.
|
||||
- **Kein Proxmox-eigenes ACME.** Kein `-acmedomain0`, kein Alias-Modus, kein
|
||||
`hetznercloud`-Plugin, keine Versionsprüfung darauf.
|
||||
- **Keine eigene Zone, kein CNAME je Host.**
|
||||
- **Kein Wildcard-Zertifikat.** Jeder Host bekommt sein eigenes, genau wie
|
||||
jede Kundeninstanz ihr eigenes bekommt — dieselbe Begründung wie dort.
|
||||
- **Keine Selbstbedienung für den Betreiber.** Eine Erneuerung von Hand
|
||||
anzustoßen ist kein Ziel dieser Spezifikation; falls es später gebraucht
|
||||
wird, ist es ein Knopf über demselben Lauf, kein eigener Weg.
|
||||
|
||||
## Abnahme
|
||||
|
||||
1. Neuen Host anlegen → am Ende ist `https://<name>.node.clupilot.com` im VPN
|
||||
erreichbar, ohne Port, ohne Warnung
|
||||
1. Neuen Host übernehmen → am Ende ist `https://<name>.node.clupilot.com`
|
||||
im VPN erreichbar, ohne Port, ohne Warnung
|
||||
2. Von außen (ohne VPN) ist derselbe Name **nicht** erreichbar
|
||||
3. Im öffentlichen DNS existiert **kein A-Eintrag** für den Namen
|
||||
3. Im öffentlichen DNS existiert **kein** A-Eintrag für den Namen, und nach
|
||||
Abschluss der Ausstellung auch **kein** übrig gebliebener TXT-Eintrag
|
||||
4. Wiederholung des Schritts stellt kein zweites Zertifikat aus
|
||||
5. `pvenode cert info` zeigt ein Let's-Encrypt-Zertifikat mit Laufzeit
|
||||
6. Der Token auf dem Host kann `node.clupilot.com` **nicht** lesen und nicht
|
||||
schreiben — einmal von Hand nachgewiesen, mit genau diesem Token gegen die
|
||||
Zone
|
||||
7. Ein Host mit `libproxmox-acme-plugins` < 1.7.1 bricht den Schritt ab und
|
||||
hinterlässt keine halbe ACME-Konfiguration
|
||||
|
||||
Punkt 2, 3 und 6 sind die wichtigen: Sie beweisen, dass der Weg das Zertifikat
|
||||
gebracht hat, **ohne** die Maschine zu öffnen und **ohne** einen Schlüssel zu
|
||||
verteilen, der weiter reicht als sie.
|
||||
|
||||
---
|
||||
5. `pvenode cert info` auf dem Host zeigt ein Let's-Encrypt-Zertifikat mit
|
||||
Laufzeit
|
||||
6. Kein Prozess auf dem Host besitzt einen Hetzner-Token — einmal von Hand
|
||||
nachgesehen (Dateisystem, Umgebung, Proxmox-Konfiguration)
|
||||
7. Ein Zertifikat, dessen Restlaufzeit unter die Schwelle fällt, erscheint
|
||||
als Warnung auf der Bereitschaftsseite, bevor es abläuft
|
||||
8. Wird der geplante Lauf zwei Zyklen lang nicht ausgeführt, zeigt die
|
||||
Bereitschaftsseite das ebenfalls an — unabhängig davon, wie lange das
|
||||
zuletzt gemessene Zertifikat noch gültig wäre
|
||||
|
||||
## Belege
|
||||
|
||||
- Hetzner, DNS-Migration ins Console (Projekt-Scoping der Token):
|
||||
<https://docs.hetzner.com/networking/dns/migration-to-hetzner-console/features-and-differences/>
|
||||
- Hetzner, API-Token erzeugen (nur Lesen/Schreiben, keine Ressourcentypen):
|
||||
- Hetzner Docs, Zonen-FAQ — Subzonen werden nicht unterstützt (NE-7597D):
|
||||
<https://docs.hetzner.com/networking/dns/faq/zones/>
|
||||
- Hetzner, API-Token erzeugen (projektweit, keine Ressourcentypen-Beschränkung):
|
||||
<https://docs.hetzner.com/cloud/api/getting-started/generating-api-token/>
|
||||
- Hetzner Status, Abschaltung der DNS-Konsole im Mai 2026:
|
||||
- Hetzner Status, Abschaltung der alten DNS-Konsole im Mai 2026:
|
||||
<https://status.hetzner.com/incident/c2146c42-6dd2-4454-916a-19f07e0e5a44>
|
||||
- Proxmox-Forum, `hetznercloud` in `libproxmox-acme-plugins` 1.7.1, bestätigt
|
||||
im April 2026:
|
||||
<https://forum.proxmox.com/threads/acme-plugin-hetzner-dns-console-is-moving-to-the-hetzner-console.174363/page-2>
|
||||
- Proxmox VE Wiki, Alias-Modus für DNS-01:
|
||||
- Proxmox VE, `pvenode(1)` — Syntax von `cert set`:
|
||||
<https://pve.proxmox.com/pve-docs/pvenode.1.html>
|
||||
- Proxmox VE Wiki, Zertifikatsverwaltung — Ablageort externer Zertifikate:
|
||||
<https://pve.proxmox.com/wiki/Certificate_Management>
|
||||
|
|
|
|||
Loading…
Reference in New Issue