Ein Name wie endian.fon-aria.de ist oft nur ein Record in der Zone fon-aria.de und existiert weder als Domain noch als Abo. Ausserdem darf nicht jeder API-User alle Domains des Servers auflisten - dann brach das Tool mit "No domains found on the Plesk server" ab, obwohl die Zone da war. Die Zone wird jetzt Label fuer Label von unten nach oben gezielt abgefragt (endian.fon-aria.de -> fon-aria.de), jeweils per site.get und webspace.get mit Namensfilter. Das Auflisten aller Domains dient nur noch der Fehlermeldung, die jetzt auch zeigt, welche Namen probiert wurden und was Plesk dazu gesagt hat. - neue Option --zone, um die Zone bei Bedarf fest vorzugeben - get_rec meldet jetzt als Warnung, wenn Plesk keine Records liefert - run.sh baut das Image immer (Cache), damit kein alter Stand haengen bleibt - Tests bilden den Fall nach: Zone nur als Abo auffindbar, Auflisten verboten Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
183 lines
7.3 KiB
Markdown
183 lines
7.3 KiB
Markdown
# Wildcard Let's Encrypt Cert Creator für Plesk
|
||
|
||
Legt in Plesk den DNS-Eintrag für einen Namen an (bzw. aktualisiert ihn) und holt
|
||
anschließend per **DNS-01-Challenge** ein **Wildcard-Zertifikat** von Let's Encrypt.
|
||
Alle Bestandteile des Zertifikats landen als einzelne Dateien im Projektverzeichnis –
|
||
zusätzlich als kombinierte `bundle.pem` und als passwortgeschützte `cert.pfx`.
|
||
|
||
```
|
||
./run.sh vpn.example.com 192.0.2.10
|
||
```
|
||
|
||
erzeugt ein Zertifikat für `vpn.example.com` **und** `*.vpn.example.com`.
|
||
|
||
---
|
||
|
||
## 1. Voraussetzungen
|
||
|
||
* Docker (oder lokal Python 3.10+)
|
||
* Die **Zone** muss auf dem Plesk-Server liegen und Plesk muss der
|
||
**autoritative Nameserver** dafür sein (sonst kann die DNS-Challenge nicht validiert werden).
|
||
Der Name selbst braucht *kein* eigenes Abo und keine eigene Domain zu sein: für
|
||
`endian.fon-aria.de` reicht die Zone `fon-aria.de` – das Tool sucht sich die
|
||
passende Zone von unten nach oben zusammen und legt dort einfach die Records an.
|
||
* Ein Plesk-API-Zugang (API-Key empfohlen).
|
||
|
||
### Plesk API-Key erzeugen
|
||
|
||
Auf dem Plesk-Server (SSH, als root):
|
||
|
||
```bash
|
||
plesk bin secret_key --create -ip-address <IP-des-Rechners-der-das-Skript-ausfuehrt> \
|
||
-description "wildcard cert creator"
|
||
```
|
||
|
||
Der ausgegebene Key kommt in die `.env` als `PLESK_API_KEY`.
|
||
Alternativ geht auch `PLESK_USER=admin` + `PLESK_PASSWORD=...`.
|
||
|
||
> Der Port 8443 des Plesk-Panels muss von dem Rechner aus erreichbar sein.
|
||
|
||
## 2. Einrichten
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
$EDITOR .env # Plesk-Daten, ACME_EMAIL, CERT_PASSWORD eintragen
|
||
chmod +x run.sh
|
||
```
|
||
|
||
Wichtig in der `.env`:
|
||
|
||
| Variable | Bedeutung |
|
||
|---|---|
|
||
| `PLESK_HOST` / `PLESK_PORT` | Plesk-Panel (Standard-Port 8443) |
|
||
| `PLESK_API_KEY` | API-Key (oder `PLESK_USER`/`PLESK_PASSWORD`) |
|
||
| `PLESK_VERIFY_TLS` | bei selbstsigniertem Panel-Zertifikat `false` |
|
||
| `ACME_EMAIL` | Kontaktadresse für Let's Encrypt |
|
||
| `ACME_STAGING` | `true` zum Testen (kein Rate-Limit-Risiko) |
|
||
| `CERT_PASSWORD` | Passwort der `.pfx` und des verschlüsselten Keys |
|
||
| `KEY_TYPE` | `rsa` (Standard, 4096 Bit) oder `ec` |
|
||
| `PFX_LEGACY_COMPAT` | `true` für alte Windows-/Java-Importer (SHA1/3DES) |
|
||
|
||
## 3. Benutzen
|
||
|
||
```bash
|
||
# DNS-Eintrag anlegen/aktualisieren + Wildcard-Zertifikat holen
|
||
./run.sh vpn.example.com 192.0.2.10
|
||
|
||
# erstmal testen (Staging-CA von Let's Encrypt)
|
||
./run.sh vpn.example.com 192.0.2.10 --staging
|
||
|
||
# nur den DNS-Eintrag setzen, kein Zertifikat
|
||
./run.sh vpn.example.com 192.0.2.10 --dns-only
|
||
|
||
# Zertifikat erneuern, obwohl das alte noch gültig ist
|
||
./run.sh vpn.example.com 192.0.2.10 --force
|
||
|
||
# ohne Wildcard, dafür mit weiteren Namen
|
||
./run.sh vpn.example.com 192.0.2.10 --no-wildcard --san www.example.com
|
||
```
|
||
|
||
Ohne Docker:
|
||
|
||
```bash
|
||
python3 -m venv .venv && . .venv/bin/activate
|
||
pip install -r requirements.txt
|
||
python -m app.main vpn.example.com 192.0.2.10
|
||
```
|
||
|
||
### Alle Optionen
|
||
|
||
| Option | Wirkung |
|
||
|---|---|
|
||
| `--ip <adresse>` | Alternative zum zweiten Positionsargument (IPv4 → A, IPv6 → AAAA) |
|
||
| `--san <name>` | zusätzlicher Name im Zertifikat (mehrfach möglich) |
|
||
| `--zone <zone>` | Plesk-Zone explizit angeben statt sie zu ermitteln |
|
||
| `--no-wildcard` | nur der reine Name, ohne `*.<fqdn>` |
|
||
| `--skip-dns` | A/AAAA-Record unangetastet lassen |
|
||
| `--dns-only` | nur DNS, kein Zertifikat |
|
||
| `--staging` | Let's-Encrypt-Staging-Umgebung |
|
||
| `--force` | erneuern, auch wenn noch gültig |
|
||
| `--keep-txt` | `_acme-challenge`-Records stehen lassen (Debugging) |
|
||
| `--ignore-propagation-timeout` | trotz fehlender DNS-Propagation weitermachen |
|
||
| `--env <datei>` | andere `.env` verwenden |
|
||
| `-v` | ausführliche Ausgabe (inkl. XML-Requests) |
|
||
|
||
## 4. Was wird erzeugt
|
||
|
||
Alles unter `certs/<fqdn>/`:
|
||
|
||
| Datei | Inhalt |
|
||
|---|---|
|
||
| `privkey.pem` | Private Key, PKCS#8, unverschlüsselt |
|
||
| `privkey-traditional.pem` | derselbe Key im klassischen OpenSSL-Format |
|
||
| `privkey-encrypted.pem` | Key, verschlüsselt mit `CERT_PASSWORD` |
|
||
| `pubkey.pem` | öffentlicher Schlüssel |
|
||
| `csr.pem` | der verwendete Certificate Signing Request |
|
||
| `cert.pem` / `cert.crt` | das reine Zertifikat (Leaf) |
|
||
| `cert.der` | Leaf im DER-Format (für Windows) |
|
||
| `chain.pem` | nur die Zwischenzertifikate |
|
||
| `chain-01.pem`, … | jedes Zwischenzertifikat einzeln |
|
||
| `fullchain.pem` | Leaf + Kette (das, was Webserver meist wollen) |
|
||
| `bundle.pem` | Key + Leaf + Kette in einer Datei |
|
||
| `cert.pfx` | PKCS#12 mit Key + Kette, Passwort = `CERT_PASSWORD` |
|
||
| `cert-info.txt` / `cert-info.json` | Subject, SANs, Gültigkeit, Fingerprints |
|
||
|
||
Der ACME-Account-Key liegt in `data/` – **nicht löschen**, sonst wird bei jedem Lauf
|
||
ein neuer Let's-Encrypt-Account registriert.
|
||
|
||
Schnellprüfung:
|
||
|
||
```bash
|
||
openssl x509 -in certs/vpn.example.com/cert.pem -noout -text
|
||
openssl pkcs12 -info -in certs/vpn.example.com/cert.pfx -nodes -passin pass:<CERT_PASSWORD>
|
||
```
|
||
|
||
## 5. Wie es abläuft
|
||
|
||
1. Plesk nach der Zone fragen – erst nach dem vollen Namen, dann Label für Label
|
||
nach oben (`endian.fon-aria.de` → `fon-aria.de`), jeweils als Domain und als Abo.
|
||
Der Server muss dafür nicht alle Domains herausrücken; mit `--zone` lässt sich
|
||
die Zone auch fest vorgeben.
|
||
2. A/AAAA-Record anlegen oder auf die neue IP korrigieren.
|
||
3. Prüfen, ob überhaupt erneuert werden muss (`RENEW_DAYS_BEFORE_EXPIRY`, Standard 30 Tage).
|
||
4. Key + CSR für `<fqdn>` und `*.<fqdn>` erzeugen.
|
||
5. ACME-Order bei Let's Encrypt, für jede Autorisierung einen `_acme-challenge`-TXT-Record
|
||
in Plesk setzen (alte Reste werden vorher entfernt).
|
||
6. Warten, bis **alle autoritativen Nameserver** die TXT-Records ausliefern
|
||
(`DNS_PROPAGATION_TIMEOUT`).
|
||
7. Challenges beantworten, Zertifikat abholen, TXT-Records wieder löschen.
|
||
8. Alle Dateien schreiben und die Zertifikatsdaten ausgeben.
|
||
|
||
## 6. Automatische Erneuerung
|
||
|
||
Das Skript ist idempotent: Läuft es, obwohl das Zertifikat noch länger als
|
||
`RENEW_DAYS_BEFORE_EXPIRY` Tage gültig ist, passiert nichts. Also einfach per Cron:
|
||
|
||
```cron
|
||
17 3 * * * cd /home/duffy/Dokumente/programmierung/wildcard-lets-encrypt-cert-plesk-creator && ./run.sh vpn.example.com 192.0.2.10 >> renew.log 2>&1
|
||
```
|
||
|
||
## 7. Wenn etwas klemmt
|
||
|
||
| Symptom | Ursache / Lösung |
|
||
|---|---|
|
||
| `Plesk rejected the credentials (HTTP 401)` | API-Key falsch oder nicht für diese Quell-IP erzeugt |
|
||
| `No Plesk DNS zone found for …` | Zone liegt nicht auf diesem Plesk oder der API-User sieht sie nicht. Die Meldung listet auf, welche Namen probiert wurden und was Plesk dazu gesagt hat. Notfalls die Zone mit `--zone example.com` fest vorgeben |
|
||
| `DNS propagation timed out` | Plesk ist nicht der autoritative NS, oder die Zone ist deaktiviert. Mit `-v` prüfen, welcher Nameserver antwortet |
|
||
| `no dns-01 challenge` | Wildcards gehen ausschließlich über DNS-01 – Domain-Validierung per HTTP ist nicht möglich |
|
||
| `urn:ietf:params:acme:error:rateLimited` | Rate-Limit von Let's Encrypt (5 Zertifikate pro Woche pro Domain). Erst mit `--staging` testen |
|
||
| PFX lässt sich in Windows nicht importieren | `PFX_LEGACY_COMPAT=true` setzen und neu erzeugen |
|
||
|
||
Bei `-v` werden auch die XML-Requests an Plesk geloggt – hilfreich, wenn ein
|
||
Record nicht so landet wie erwartet.
|
||
|
||
## 8. Tests
|
||
|
||
Ohne echten Plesk-Server und ohne Zertifikatsausstellung:
|
||
|
||
```bash
|
||
.venv/bin/python tests/test_offline.py # XML-Parsing, DNS-Logik, Dateiausgabe, PFX
|
||
.venv/bin/python tests/test_cli.py # kompletter Ablauf gegen ein Plesk-Fake
|
||
.venv/bin/python tests/test_acme_api.py # prueft die Let's-Encrypt-Anbindung (nur Directory-Abruf)
|
||
```
|