Files
wildcard-lets-encrypt-cert-…/README.md
T
duffyduckandClaude Opus 5 fd76c18eb0 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>
2026-08-13 09:41:26 +02:00

176 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```