Wildcard-Zertifikate ueber Plesk-DNS und Let's Encrypt

Legt den A/AAAA-Record fuer einen Namen in Plesk an bzw. aktualisiert ihn und
holt anschliessend per DNS-01-Challenge ein Wildcard-Zertifikat von Let's
Encrypt. Alle Bestandteile werden einzeln abgelegt, zusaetzlich als
kombinierte bundle.pem und als passwortgeschuetzte cert.pfx.

- Plesk-Anbindung ueber die XML-API (Zone finden, Records lesen/anlegen/loeschen)
- ACME-Order mit dns-01, Account-Key wird wiederverwendet
- Propagations-Check gegen die autoritativen Nameserver der Zone, bricht vor
  der Validierung ab statt einen Fehlversuch bei Let's Encrypt zu verbrennen
- Renewal-Check: laeuft idempotent, taugt so direkt fuer den Cron
- Docker-Container, Konfiguration ueber .env, Ausgabe im Projektverzeichnis
- Tests ohne echten Plesk-Server bzw. ohne Zertifikatsausstellung

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-08-13 09:41:26 +02:00
co-authored by Claude Opus 5
commit fd76c18eb0
17 changed files with 2080 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
# 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 Domain muss auf dem Plesk-Server als DNS-Zone liegen und Plesk muss der
**autoritative Nameserver** dafür sein (sonst kann die DNS-Challenge nicht validiert werden).
* 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) |
| `--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-Zone zum Namen suchen (längste passende Zone, z. B. `example.com` für `vpn.example.com`).
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 …` | Domain liegt nicht auf diesem Plesk oder der API-User sieht sie nicht |
| `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)
```