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, tar und openssl (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:

VerfahrenWieWildcard möglich
HTTP-01Datei unter /.well-known/acme-challenge/ ablegennein
DNS-01TXT-Record _acme-challenge.example.de setzenja

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:

CAEAB nötigLaufzeitWurzel
ZeroSSLnein, acme.sh holt es selbst90 TageUSERTrust (über Sectigo)
Let's Encryptnein90 TageISRG
IONOS ACMEja, von Handwie gekauftSectigo

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 Authority

IONOS-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 --version

Schritt 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.de

acme.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.de

Schritt 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.sh

Bricht 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 apache2

Schritt 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 --cron
nano /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.target
chmod 0600 /etc/systemd/system/acme-renew.service
systemctl daemon-reload
systemctl enable --now acme-renew.timer
systemctl list-timers acme-renew.timer

acme.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 50

Mehrere Domains auf einem Server

Zwei Eigenschaften von acme.sh legen zusammen fest, wie das aussehen muss:

  • --cron erneuert 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 reloadcmd steht pro Zertifikat in <home>/<domain>/<domain>.conf, als Le_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 -d

Prü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 --list

Der 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

MeldungUrsacheLösung
Unknown parameter: ----homeInstallation über get.acme.sh mit eigenen FlagsTarball-Weg nehmen, siehe Schritt 2
No available certificate found for the requested domain(s).IONOS-ACME, kein freies Kontingent im KontoZeroSSL oder Let's Encrypt nehmen
Registrierung bei ZeroSSL scheitert ohne klare MeldungServer-URL nicht exaktgenau https://acme.zerossl.com/v2/DV90
AH02565: ... do not matchneue Fullchain mit altem Schlüssel kombiniertvhost-Pfade prüfen, siehe Schritt 5
is already verified, skipping dns-01IONOS autorisiert eigene Domains vorabkein Fehler, nur eine Info
Validierung läuft in einen TimeoutDNS-Propagation zu langsam--dnssleep 180 anhängen
AH02218: no OCSP URI in certificateLet's Encrypt liefert seit 2025 keine OCSP-URL mehrSSLUseStapling off oder CA wechseln
--install-cert findet nichtsECDSA-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:

abmaximale Laufzeit
15.03.2026200 Tage
15.03.2027100 Tage
15.03.202947 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 --uninstall

Die Dateien unter /etc/apache2/ssl/ bleiben dabei liegen, --remove streicht das Zertifikat nur aus der Erneuerungsliste. Erst den vhost umstellen, dann löschen.