Wildcard-SSL mit acme.sh und IONOS-DNS
Wildcard-Zertifikate automatisch beziehen und erneuern: acme.sh, DNS-01 über die IONOS-API, ZeroSSL und ein Apache-Deploy, das sich nicht selbst abschießt.
Ein Wildcard-Zertifikat deckt alle Subdomains einer Domain auf einen Schlag ab.
Statt für jede neue Subdomain ein eigenes Zertifikat auszustellen, gibt es
einmal *.example.de und die Sache ist erledigt. Der Preis dafür: die
Validierung läuft zwingend über DNS, nicht über die Webserver-Datei, und das
braucht einen Client, der mit der DNS-API des Providers sprechen kann.
Dieses Tutorial richtet das komplett ein: Zertifikat ausstellen, nach
/etc/apache2/ssl deployen, automatisch erneuern lassen. Am Ende läuft ein
systemd-Timer, der sich um alles kümmert, und es gibt einen Deploy-Schritt, der
Apache im Fehlerfall lieber gar nicht neu lädt statt kaputt.
Voraussetzungen
- Debian mit Apache 2.4 und root-Zugang
- eine Domain, deren DNS-Zone bei IONOS liegt, nicht nur die Registrierung
curl,tarundopenssl(auf einem normalen Debian alles vorhanden)
Der Unterschied zwischen Registrierung und DNS-Zone ist wichtig: DNS-01 setzt einen TXT-Record über die API des DNS-Anbieters. Zeigen die Nameserver der Domain auf Cloudflare, brauchst Du das Cloudflare-Plugin und nicht das von IONOS, auch wenn die Domain bei IONOS gekauft wurde.
Warum DNS-01 und warum acme.sh
Es gibt zwei gängige Wege, einer Zertifizierungsstelle zu beweisen, dass die Domain Dir gehört:
| Verfahren | Wie | Wildcard möglich |
|---|---|---|
| HTTP-01 | Datei unter /.well-known/acme-challenge/ ablegen | nein |
| DNS-01 | TXT-Record _acme-challenge.example.de setzen | ja |
Für ein Wildcard gibt es keine Wahl, es muss DNS-01 sein. Damit fällt Certbot als bequeme Option weg: es bringt kein offizielles Plugin für IONOS-DNS mit, man landet bei Drittanbieter-Plugins über pip.
acme.sh hat dns_ionos eingebaut,
ist ein reines Shellscript ohne Python-Umgebung und schreibt fullchain.cer
nativ. Das passt zu Apache, der die Zwischenzertifikate seit 2.4 direkt aus der
Fullchain liest.
Welche Zertifizierungsstelle
acme.sh spricht jeden ACME-Server. Drei kommen bei einer IONOS-Domain in Frage:
| CA | EAB nötig | Laufzeit | Wurzel |
|---|---|---|---|
| ZeroSSL | nein, acme.sh holt es selbst | 90 Tage | USERTrust (über Sectigo) |
| Let's Encrypt | nein | 90 Tage | ISRG |
| IONOS ACME | ja, von Hand | wie gekauft | Sectigo |
Dieses Tutorial nimmt deshalb ZeroSSL. Der Grund ist nicht Willkür: wer von einem gekauften IONOS-Zertifikat kommt, behält damit die Sectigo-Vertrauenskette.
example.de
└─ ZeroSSL RSA DV SSL CA 2
└─ Sectigo Public Server Authentication Root R46
└─ USERTrust RSA Certification AuthorityIONOS-Zertifikate laufen über Sectigo Public Server Authentication CA DV R36
und damit über denselben Root R46. Oberhalb des Zwischenzertifikats sind die
Ketten identisch, es ändert sich nur die Ausstellerzeile.
Wer das nicht braucht, nimmt Let's Encrypt. Alles Weitere ist gleich, nur die
--server-URL unterscheidet sich.
Schritt 1: DNS-API-Key bei IONOS erzeugen
Auf developer.hosting.ionos.de einen
API-Key anlegen. Der wird als eine lange Zeichenkette der Form prefix.secret
angezeigt. Der erste Punkt trennt die beiden Teile:
export IONOS_PREFIX="<Teil vor dem ersten Punkt>"
export IONOS_SECRET="<Rest dahinter>"Ein Key gilt für alle Zonen des Kontos. Für weitere Domains im selben Konto brauchst Du also keinen zweiten.
Schritt 2: acme.sh installieren
cd /tmp
curl -fsSL https://github.com/acmesh-official/acme.sh/archive/master.tar.gz -o acme.tar.gz
tar -xzf acme.tar.gz
cd acme.sh-master
sh ./acme.sh --install \
--home /root/.acme.sh \
--nocron \
--noprofile--nocron unterdrückt den eigenen Cronjob, wir nehmen später einen
systemd-Timer. --noprofile spart die Shell-Aliase, wir rufen acme.sh mit
vollem Pfad auf.
/root/.acme.sh/acme.sh --home /root/.acme.sh --versionSchritt 3: ACME-Account registrieren
/root/.acme.sh/acme.sh --home /root/.acme.sh \
--register-account \
--server https://acme.zerossl.com/v2/DV90 \
-m info@example.deacme.sh hält pro CA genau einen Account, abgelegt unter
/root/.acme.sh/ca/acme.zerossl.com/v2/DV90/. Wenn Du später weitere Domains
aufnimmst, nimm dieselbe Adresse, sonst wird der Accountschlüssel ersetzt.
Schritt 4: Zertifikat ausstellen
export IONOS_PREFIX="..."
export IONOS_SECRET="..."
/root/.acme.sh/acme.sh --home /root/.acme.sh \
--issue \
--server https://acme.zerossl.com/v2/DV90 \
--dns dns_ionos \
--keylength 2048 \
-d example.de \
-d '*.example.de'Zwei Details, die leicht untergehen:
Das Wildcard deckt den Apex nicht ab. *.example.de passt auf
www.example.de und shop.example.de, aber nicht auf example.de selbst.
Deshalb stehen beide in der Liste.
Ein Wildcard deckt genau ein Label ab. *.example.de passt auch auf
b.example.de, aber nicht auf a.b.example.de. Für tiefere Namen ergänzt Du
entweder den Namen einzeln oder eine zweite Wildcard-Ebene:
-d example.de -d '*.example.de' -d '*.b.example.de'Schlägt die Validierung mit einem Timeout fehl, ist meist die DNS-Propagation
zu langsam. Dann --dnssleep 180 anhängen und den Aufruf wiederholen.
Schritt 5: Zielverzeichnis prüfen, bevor Du installierst
Der nächste Schritt verknüpft acme.sh dauerhaft mit Pfaden unter
/etc/apache2/ssl. Bevor das passiert, lohnt ein Blick, was dort schon liegt.
Also vorher nachsehen, welche Dateien belegt sind und wer sie referenziert:
ls -la /etc/apache2/ssl/
grep -RnE 'SSLCertificate(File|KeyFile|ChainFile)' \
/etc/apache2/sites-available/ /etc/apache2/conf-available/Am saubersten ist ein eigenes Unterverzeichnis pro Domain. Dann kann sich nichts in die Quere kommen, auch nicht bei der zweiten Domain später:
mkdir -p /etc/apache2/ssl/example.de
chmod 0710 /etc/apache2/ssl/example.deSchritt 6: Deploy-Script anlegen
acme.sh kann nach jeder Erneuerung ein Kommando aufrufen. Genau dort gehört die
Sicherung hin, die apachectl -t nicht leisten kann: der Abgleich, ob
Schlüssel und Zertifikat wirklich zusammengehören.
nano /usr/local/sbin/ssl-deploy.sh#!/usr/bin/env bash
set -euo pipefail
SSL_DIR="/etc/apache2/ssl/example.de"
KEY="$SSL_DIR/example.de.key"
CERT="$SSL_DIR/example.de.cer"
FULLCHAIN="$SSL_DIR/fullchain.cer"
for f in "$KEY" "$CERT" "$FULLCHAIN"; do
[[ -s "$f" ]] || { echo "Datei fehlt oder ist leer: $f" >&2; exit 1; }
done
# Gehoeren Schluessel und Zertifikat zusammen? Vergleich der oeffentlichen
# Schluessel, weil apachectl -t das nicht prueft.
a="$(openssl pkey -in "$KEY" -pubout | openssl sha256)"
b="$(openssl x509 -in "$CERT" -noout -pubkey | openssl sha256)"
if [[ "$a" != "$b" ]]; then
echo "Privater Schluessel passt nicht zum Zertifikat. Kein Reload." >&2
exit 1
fi
chmod 0710 "$SSL_DIR"
chmod 0600 "$KEY"
chmod 0644 "$CERT" "$FULLCHAIN"
if ! apachectl -t; then
echo "Apache-Konfiguration fehlerhaft. Kein Reload." >&2
exit 1
fi
systemctl reload apache2
echo "Aktiv bis: $(openssl x509 -in "$FULLCHAIN" -noout -enddate | cut -d= -f2)"chmod 0755 /usr/local/sbin/ssl-deploy.shBricht das Script ab, wird nicht reloadet und der Timer meldet die Unit als fehlgeschlagen. Der Server läuft in dem Fall mit dem alten Zertifikat weiter, was deutlich besser ist als ein Apache, der nicht mehr startet.
Schritt 7: Zielpfade und Reload-Hook verdrahten
/root/.acme.sh/acme.sh --home /root/.acme.sh \
--install-cert -d example.de \
--key-file /etc/apache2/ssl/example.de/example.de.key \
--cert-file /etc/apache2/ssl/example.de/example.de.cer \
--ca-file /etc/apache2/ssl/example.de/ca.cer \
--fullchain-file /etc/apache2/ssl/example.de/fullchain.cer \
--reloadcmd "/usr/local/sbin/ssl-deploy.sh"Das ist ein einmaliger Schritt. acme.sh merkt sich die Pfade und den
reloadcmd pro Zertifikat und wendet sie bei jeder künftigen Erneuerung selbst
an.
Schritt 8: Apache umstellen
<VirtualHost *:443>
ServerName example.de
ServerAlias www.example.de
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/example.de/fullchain.cer
SSLCertificateKeyFile /etc/apache2/ssl/example.de/example.de.key
# ...
</VirtualHost>SSLCertificateChainFile wird nicht mehr gebraucht, Apache 2.4 liest die
Zwischenzertifikate direkt aus der Fullchain.
apachectl -t && systemctl reload apache2Schritt 9: Automatische Erneuerung per systemd-Timer
acme.sh bringt einen eigenen Cronjob mit, den wir mit --nocron unterdrückt
haben. Ein systemd-Timer ist übersichtlicher, protokolliert ins Journal und
lässt sich mit systemctl abfragen.
nano /etc/systemd/system/acme-renew.service[Unit]
Description=ACME-Zertifikate erneuern
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Environment=IONOS_PREFIX=...
Environment=IONOS_SECRET=...
ExecStart=/root/.acme.sh/acme.sh --home /root/.acme.sh --cronnano /etc/systemd/system/acme-renew.timer[Unit]
Description=Taegliche Pruefung der ACME-Zertifikate
[Timer]
OnCalendar=*-*-* 03:17:00
RandomizedDelaySec=45m
Persistent=true
[Install]
WantedBy=timers.targetchmod 0600 /etc/systemd/system/acme-renew.service
systemctl daemon-reload
systemctl enable --now acme-renew.timer
systemctl list-timers acme-renew.timeracme.sh erneuert nur, was alt genug ist. Ein Lauf, der nichts zu tun hat, endet
mit Skipping. Next renewal time is: ... und das ist der Normalfall.
# Trockenlauf, erneuert nichts
/root/.acme.sh/acme.sh --home /root/.acme.sh --cron
journalctl -u acme-renew -n 50Mehrere Domains auf einem Server
Zwei Eigenschaften von acme.sh legen zusammen fest, wie das aussehen muss:
--cronerneuert immer alle Zertifikate im--home. Es gibt keine Möglichkeit, den Durchlauf auf eine Domain zu beschränken. Zwei Timer bedeuten also zwei vollständige Durchläufe, die sich gegenseitig ins Gehege kommen.- Der
reloadcmdsteht pro Zertifikat in<home>/<domain>/<domain>.conf, alsLe_ReloadCmd. Jede Domain kann also nach ihrer Erneuerung ein anderes Kommando ausführen.
Daraus folgt: ein Timer, ein --cron, aber ein eigener reloadcmd je
Domain. Für die zweite Domain also einfach noch einmal Schritt 4, 6 und 7 mit
eigenen Pfaden und einem eigenen Deploy-Script:
/root/.acme.sh/acme.sh --home /root/.acme.sh \
--issue --server https://acme.zerossl.com/v2/DV90 \
--dns dns_ionos --keylength 2048 \
-d zweite-domain.de -d '*.zweite-domain.de'
/root/.acme.sh/acme.sh --home /root/.acme.sh \
--install-cert -d zweite-domain.de \
--key-file /etc/apache2/ssl/zweite-domain.de/zweite-domain.de.key \
--cert-file /etc/apache2/ssl/zweite-domain.de/zweite-domain.de.cer \
--ca-file /etc/apache2/ssl/zweite-domain.de/ca.cer \
--fullchain-file /etc/apache2/ssl/zweite-domain.de/fullchain.cer \
--reloadcmd "/usr/local/sbin/ssl-deploy-zweite-domain.sh"Am Timer ändert sich nichts, er deckt die neue Domain automatisch mit ab.
Kontrollieren, was tatsächlich gespeichert ist: der Wert steht base64-kodiert
in der Konfiguration, ein einfaches grep zeigt ihn also nicht im Klartext.
grep -i reloadcmd /root/.acme.sh/*/*.conf \
| grep -o '__ACME_BASE64__START_[^_]*' | sed 's/.*START_//' | base64 -dPrüfen
# Was liegt lokal
openssl x509 -in /etc/apache2/ssl/example.de/fullchain.cer \
-noout -subject -issuer -dates -ext subjectAltName
# Wie viele Zertifikate sind in der Kette (Leaf + Zwischenzertifikate)
grep -c 'BEGIN CERTIFICATE' /etc/apache2/ssl/example.de/fullchain.cer
# Was liefert der Server wirklich aus
for h in example.de www.example.de shop.example.de; do
echo "== $h"
echo | openssl s_client -servername "$h" -connect "$h":443 2>/dev/null \
| openssl x509 -noout -subject -issuer
done
# Was kennt acme.sh
/root/.acme.sh/acme.sh --home /root/.acme.sh --listDer s_client-Test mit -servername ist der wichtigste davon. Nur er zeigt,
was ein Browser tatsächlich bekommt, inklusive der Frage, ob der richtige vhost
greift.
Häufige Fehler
| Meldung | Ursache | Lösung |
|---|---|---|
Unknown parameter: ----home | Installation über get.acme.sh mit eigenen Flags | Tarball-Weg nehmen, siehe Schritt 2 |
No available certificate found for the requested domain(s). | IONOS-ACME, kein freies Kontingent im Konto | ZeroSSL oder Let's Encrypt nehmen |
| Registrierung bei ZeroSSL scheitert ohne klare Meldung | Server-URL nicht exakt | genau https://acme.zerossl.com/v2/DV90 |
AH02565: ... do not match | neue Fullchain mit altem Schlüssel kombiniert | vhost-Pfade prüfen, siehe Schritt 5 |
is already verified, skipping dns-01 | IONOS autorisiert eigene Domains vorab | kein Fehler, nur eine Info |
| Validierung läuft in einen Timeout | DNS-Propagation zu langsam | --dnssleep 180 anhängen |
AH02218: no OCSP URI in certificate | Let's Encrypt liefert seit 2025 keine OCSP-URL mehr | SSLUseStapling off oder CA wechseln |
--install-cert findet nichts | ECDSA-Zertifikat ohne --ecc angesprochen | --ecc ergänzen |
Warum sich das ohnehin lohnt
Manuelle Zertifikatspflege läuft branchenweit aus. Das CA/Browser Forum hat mit Ballot SC-081v3 einen Stufenplan für alle öffentlichen CAs beschlossen:
| ab | maximale Laufzeit |
|---|---|
| 15.03.2026 | 200 Tage |
| 15.03.2027 | 100 Tage |
| 15.03.2029 | 47 Tage |
Ein Zertifikat, das alle 47 Tage von Hand getauscht werden muss, tauscht irgendwann niemand mehr rechtzeitig. Die 90 Tage von ZeroSSL und Let's Encrypt sind vor diesem Hintergrund kein Nachteil, sondern nur der Zustand, auf den alles zuläuft.
Deinstallation
systemctl disable --now acme-renew.timer
rm /etc/systemd/system/acme-renew.{timer,service}
systemctl daemon-reload
/root/.acme.sh/acme.sh --home /root/.acme.sh --remove -d example.de
/root/.acme.sh/acme.sh --home /root/.acme.sh --uninstallDie Dateien unter /etc/apache2/ssl/ bleiben dabei liegen, --remove streicht
das Zertifikat nur aus der Erneuerungsliste. Erst den vhost umstellen, dann
löschen.