Die Nameserver von fon-aria.de haben AAAA-Records, der Container hat aber kein IPv6. dns.query.udp() ist deshalb mit OSError [Errno 99] "Cannot assign requested address" durchgeschlagen und hat den ganzen Lauf abgebrochen - nach dem Anlegen der Challenge-Records. Adressen, zu denen es keine Route gibt, werden jetzt vorher aussortiert (UDP-connect, verschickt nichts) und waehrend des Wartens zusaetzlich verworfen, falls doch eine ausfaellt. Bleibt keine uebrig, gibt es eine klare Meldung statt eines Tracebacks. Bei fon-aria.de bleiben so die beiden IPv4-Adressen derselben Nameserver uebrig. - OSError wird in main() sauber abgefangen - tests/test_dns.py deckt die Faelle ab; im Container gegen die echte Zone gegengeprueft Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
185 lines
7.6 KiB
Markdown
185 lines
7.6 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 |
|
||
| `Skipping unreachable nameserver address(es): 2001:…` | Normal: der Container hat kein IPv6, die IPv4-Adressen derselben Nameserver werden benutzt |
|
||
| `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_dns.py # Propagations-Check, unerreichbare Nameserver
|
||
.venv/bin/python tests/test_acme_api.py # prueft die Let's-Encrypt-Anbindung (nur Directory-Abruf)
|
||
```
|