// Tutorial · Web · TLS · Teil 2

nginx + acme.sh Teil 2: ECC-Zertifikate und ein Webroot für alle Dienste

📅 19.09.2026 ⏱ 18 Min. Lesezeit

Im ersten Teil haben wir acme.sh eingerichtet und ein Zertifikat mit den Standard-Einstellungen ausgestellt. Das funktioniert – ist aber die bequeme Variante. Dieser Teil geht einen Schritt weiter: ECC-Zertifikate mit ec-384, ein zentraler Webroot, der für jeden künftigen Dienst wiederverwendet wird, und die vollständige nginx-Konfiguration am Beispiel der Domain pdf.meine.domain – inklusive der Fehler, die man dabei typischerweise macht.

Was Teil 1 offen gelassen hat

Der Unterschied liegt nicht in „besser oder schlechter“, sondern im Anspruch. Teil 1 hat gezeigt, wie man ein Zertifikat bekommt. Teil 2 zeigt, wie man es wiederholt bekommt – für den zweiten, dritten und zwanzigsten Dienst –, und zwar mit einem Schlüsseltyp, der zu einem modernen Stack passt.

ThemaTeil 1Teil 2
SchlüsseltypStandard (RSA)ECDSA P-384 (-k ec-384)
WebrootDocumentRoot der Seite (/var/www/html)Eigener Sammel-Webroot für alle Dienste
Zielpfad/etc/nginx/ssl/… (per --install-cert kopiert)/etc/ssl/private/<domain>_ecc/ – direkt von acme.sh, ohne Kopieren
nginx-BlockStatische SeiteReverse Proxy mit Sicherheits-Headern und großen Uploads
Rechteacme.sh als normaler Nutzeracme.sh als root – wegen /etc/ssl/private und reloadcmd
Kurz gesagt: Teil 1 ist die Einrichtung, Teil 2 ist die Werkstatt. Wenn du vorhast, mehr als einen Dienst hinter nginx zu betreiben, spare dir das Kopieren von HTTP-Blöcken und Challenge-Pfaden – nimm den zentralen Webroot aus diesem Artikel.

ECDSA statt RSA – was das heißt

Ein TLS-Zertifikat ist im Kern ein Schlüsselpaar plus eine Unterschrift der Zertifizierungsstelle. Der Algorithmus dieses Schlüssels ist frei wählbar: klassisch RSA oder ECDSA (Elliptic Curve). acme.sh kann beides – gesteuert über -k bzw. --keylength.

Wert für -kTypKurve / Länge
ec-256ECDSAsecp256r1 (P-256) – der übliche, schnelle Einstieg
ec-384ECDSAsecp384r1 (P-384) – höhere Sicherheitsmarge, etwas größer
ec-521ECDSAsecp521r1 (P-521) – maximal, wird selten gebraucht
2048 / 3072 / 4096RSAklassische RSA-Schlüssel

Warum ec-384? Drei Gründe, die im Alltag wirklich zählen:

  • Kleinere Zertifikate: Ein P-384-Schlüssel ist wenige hundert Byte groß, ein RSA-4096-Schlüssel ein Vielfaches. Der TLS-Handshake überträgt entsprechend weniger Daten.
  • Weniger CPU: Der rechenintensive Teil des Handshakes – die Entschlüsselung mit dem privaten Schlüssel – ist bei ECDSA deutlich günstiger. Bei vielen Verbindungen pro Sekunde merkt man das.
  • Modern ist ohnehin EC: In TLS 1.3 ist der Schlüsselaustausch immer ECDHE. Ein ECDSA-Zertifikat passt schlicht besser zu diesem Stack als ein RSA-Zertifikat.
Zwei Kurven, die man leicht verwechselt: Der Parameter -k ec-384 bestimmt die Kurve des Zertifikatsschlüssels. Die nginx-Direktive ssl_ecdh_curve X25519:P-384:P-256:P-521 bestimmt dagegen die Kurven des Schlüsselaustauschs (ECDHE). Beide gehören zusammen, dürfen aber nicht gleichgesetzt werden – X25519 steht dort bewusst zuerst, weil es der schnellste moderne Austausch ist.
Kompatibilität: Jeder Browser seit etwa 2014 spricht ECDSA. Ältere Clients – etwa Windows 7 mit nicht gepatchtem IE, sehr alte Java-Runtimes oder manche E-Mail-Gateways – können scheitern. Solange dein Dienst nur von modernen Browsern genutzt wird, ist das kein Thema. Wenn du auf Nummer sicher gehen willst, fährst du beide Zertifikate gleichzeitig (Tipp am Ende).

Ein Webroot für alle Dienste

Beim Webroot-Verfahren legt acme.sh eine Prüfdatei ab und Let’s Encrypt ruft sie über http://deine.domain/.well-known/acme-challenge/<token> ab. Der Trick ist, dafür einen einzigen Ordner zu nutzen, den jeder Serverblock auf Port 80 ausliefert:

sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/letsencrypt
sudo chmod 755 /var/www/letsencrypt

Jeder Serverblock, der ein Zertifikat bekommen soll, liefert diese Challenge-Location aus – und schickt alles andere auf HTTPS. Der Pfad ist für alle Dienste gleich, du kopierst also nur noch einen Block:

server {
  listen 80;
  listen [::]:80;
  server_name pdf.meine.domain;

  location /.well-known/acme-challenge {
    root /var/www/letsencrypt;
    default_type "text/plain";
    try_files $uri =404;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}
  • root /var/www/letsencrypt sucht die Datei unter /var/www/letsencrypt/.well-known/acme-challenge/… – genau dort legt der -w-Parameter sie ab.
  • Die Challenge-Location steht vor dem Redirect. Andernfalls landet auch die Prüfdatei auf HTTPS – und die Ausstellung scheitert mit einer 404.
  • default_type "text/plain" und try_files $uri =404; sorgen für eine saubere Auslieferung bzw. eine klare 404 statt der Startseite – das macht das Debuggen deutlich einfacher.
Warum ein eigener Ordner statt /var/www/html? Der DocumentRoot einer Seite ändert sich beim Umzug oder Redesign – der Challenge-Pfad darf das nicht. Ein fester Sammelordner macht die Erneuerung unabhängig davon, was mit der eigentlichen Website passiert.
Falls doch einmal eine 404 kommt: Prüfe, ob im Serverblock eine reguläre Location existiert, die früher greift. Ein ^~ vor dem Challenge-Pfad (location ^~ /.well-known/acme-challenge) gibt ihm dann Vorrang vor allen regulären Ausdrücken. Nebenbei: Ohne abschließenden Schrägstrich matcht die Location auch /.well-known/acme-challenge-datei – harmlos, aber gut zu wissen.

Das EC-Zertifikat ausstellen

Zuerst acme.sh als root betreiben. Das klingt grob, ist hier aber richtig: Die Zertifikate sollen nach /etc/ssl/private/ – ein Verzeichnis, in das nur root schreiben darf –, und der --reloadcmd ruft systemctl reload nginx auf, was ebenfalls root braucht.

sudo -s    # oder: sudo bash
curl https://get.acme.sh | sh -s email=admin@meine.domain
acme.sh --set-default-ca --server letsencrypt

Danach das Zertifikat für die Domain – mit EC-Schlüssel und dem zentralen Webroot:

acme.sh --issue \
  -k ec-384 \
  -w /var/www/letsencrypt \
  -d pdf.meine.domain \
  --reloadcmd "systemctl reload nginx.service"
OptionBedeutung
-k ec-384Erzeugt einen ECDSA-Schlüssel auf der Kurve P-384 (lang: --keylength ec-384).
-w /var/www/letsencryptWebroot-Modus: acme.sh schreibt den Token dorthin, Let’s Encrypt liest ihn über Port 80 zurück.
-d pdf.meine.domainDie Domain im Zertifikat. Mehrfach angebbar – dann landen alle Namen im selben Zertifikat (SAN).
--reloadcmdBefehl, der nach Ausstellung und nach jeder Erneuerung läuft – ohne ihn bedient nginx weiter das alte Zertifikat.

Was dabei passiert:

  • acme.sh legt alles in einem eigenen Unterordner pdf.meine.domain_ecc/ ab – standardmäßig unter ~/.acme.sh/, im nächsten Abschnitt verschieben wir das dauerhaft nach /etc/ssl/private. Der Suffix _ecc ist wichtig – dazu später mehr.
  • Dort entstehen pdf.meine.domain.key (privater Schlüssel), pdf.meine.domain.cer (Zertifikat), ca.cer (Zwischenzertifikat) und fullchain.cer (Zertifikat + Kette).
  • Ein Kontingent-Hinweis: Let’s Encrypt begrenzt Zertifikate pro Domain und Woche. Für Tests vorab --staging ergänzen und erst danach produktiv ausstellen.
Port 80 muss erreichbar sein – auch später. Die HTTP-Challenge läuft immer über Port 80, auch wenn dein Dienst nur auf 443 lauscht. Wer Port 80 dichtmacht, bekommt beim nächsten Erneuern einen Fehler – und merkt es erst nach 90 Tagen. Alternative ohne Port 80: --dns mit einem DNS-API-Provider.

Zertifikate direkt nach /etc/ssl/private

Standardmäßig legt acme.sh alles unter ~/.acme.sh/<domain>_ecc/ ab – man müsste die Dateien danach per --install-cert an einen festen Ort kopieren. Das lässt sich abkürzen: acme.sh bekommt einmal gesagt, wo sein Zertifikatsverzeichnis liegt, und legt die Zertifikate direkt dort ab, wo nginx sie erwartet.

Dafür trägst du in der Konfigurationsdatei von acme.sh – bei einer root-Installation /root/.acme.sh/account.conf – den Parameter CERT_HOME ein:

# /root/.acme.sh/account.conf

CERT_HOME='/etc/ssl/private'

#LOG_FILE="/root/.acme.sh/acme.sh.log"
#LOG_LEVEL=1

Ab dem nächsten Lauf entstehen die Zertifikate unter /etc/ssl/private/<domain>_ecc/ – also genau dort, wo dein Serverblock sie ohnehin erwartet:

acme.sh --issue -k ec-384 -w /var/www/letsencrypt \
  -d pdf.meine.domain --reloadcmd "systemctl reload nginx.service"

ls -l /etc/ssl/private/pdf.meine.domain_ecc/
# pdf.meine.domain.key   pdf.meine.domain.cer   ca.cer   fullchain.cer
Vorteil / FolgeWas es bedeutet
Kein Kopieren mehr--install-cert mit --key-file und --fullchain-file entfällt komplett. nginx zeigt direkt auf die Dateien, die acme.sh selbst pflegt – es gibt nur eine Quelle der Wahrheit.
--reloadcmd bleibt PflichtOhne den Befehl bedient nginx nach einer Erneuerung weiter das alte Zertifikat. Er wird bei --issue mitgegeben und für alle künftigen Läufe gespeichert.
acme.sh zieht komplett umCERT_HOME verschiebt alles: Account-Schlüssel, CA-Daten und die Zertifikate. Unter /etc/ssl/private liegt danach also auch account.key.
Nur vor dem ersten Zertifikat sinnvollBereits ausgestellte Zertifikate liegen im alten Verzeichnis und werden nicht automatisch gefunden. Wer umstellt, stellt einmal neu aus.
Der --ecc-Schalter bleibtBei EC-Zertifikaten gehört --ecc weiterhin in jeden acme.sh-Befehl – nur eben ohne --key-file und --fullchain-file.

Rechte prüfen – der private Schlüssel gehört ausschließlich root:

sudo chmod 700 /etc/ssl/private
sudo chmod 600 /etc/ssl/private/*_ecc/*.key
sudo ls -l /etc/ssl/private/
Gegenprobe nach dem ersten Ausstellen: ls -d /etc/ssl/private/pdf.meine.domain_ecc/ muss den Ordner zeigen. Taucht er dort nicht auf, hat acme.sh die Einstellung nicht gelesen – dann prüfen, wo die Installation liegt (ls -d /root/.acme.sh) und ob CERT_HOME exakt so geschrieben ist wie oben. Nebenbei: Mit CERT_HOME liegt der Account-Schlüssel account.key ebenfalls in /etc/ssl/private – das ist gewollt und wandert mit ins Backup.

Die nginx-Konfiguration für pdf.meine.domain

Zuerst der Teil, der in vielen Konfigurationen fehlt und ohne den die Erneuerung scheitert: der Port-80-Serverblock. Er liefert die Challenge aus und schickt alles andere auf HTTPS.

# /etc/nginx/sites-available/pdf.meine.domain.conf
server {
    listen 80;
    listen [::]:80;
    server_name pdf.meine.domain;

    location /.well-known/acme-challenge {
        root /var/www/letsencrypt;
        default_type "text/plain";
        try_files $uri =404;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Die Challenge-Location steht vor dem Redirect. Damit kann der Redirect die Prüfdatei nicht abfangen – und du musst den Pfad nie wieder anfassen. Jetzt der eigentliche Serverblock, kommentiert in der Reihenfolge, in der nginx ihn liest:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name pdf.meine.domain;

    access_log off;
    error_log /var/log/nginx/pdf.meine.domain.error.log;

    # ---------------------------------------------------------------- Zertifikat (ECDSA)
    ssl_certificate     /etc/ssl/private/pdf.meine.domain_ecc/fullchain.cer;
    ssl_certificate_key /etc/ssl/private/pdf.meine.domain_ecc/pdf.meine.domain.key;

    # ---------------------------------------------------------------- TLS-Feinschliff
    ssl_buffer_size 1400;                 # passt gut zu 1500-Byte-Ethernet-MTU
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;              # Session-Tickets aus: bessere Forward Secrecy
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;        # TLS 1.3 entscheidet der Client; aktueller Rat
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_ecdh_curve X25519:P-384:P-256:P-521;
    
    # OPTIMIERUNG 2: Lokalen Resolver (oder Quad9/Cloudflare) eintragen für besseren Datenschutz als Google
    resolver 9.9.9.9 1.1.1.1 valid=300s;
    resolver_timeout 5s;

    # ---------------------------------------------------------------- Sicherheits-Header
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;    # PDFs muessen nie geframed werden
    add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
    add_header Referrer-Policy "same-origin" always;
    add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), usb=(), web-share=(), xr-spatial-tracking=()" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; worker-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' blob:; frame-ancestors 'none'; upgrade-insecure-requests;" always;

    proxy_cookie_path / "/; HTTPOnly; Secure; SameSite=Lax";

    keepalive_timeout 70;
    sendfile on;

    # ---------------------------------------------------------------- Grosse PDF-Uploads
    client_max_body_size 0;               # kein Limit (siehe Warnbox unten)

    location / {
        log_not_found off;
        proxy_cache_valid 200 120m;

        # Hohe Timeouts verhindern 504 bei OCR und Komprimierung
        proxy_read_timeout    3600s;
        proxy_send_timeout    3600s;
        proxy_connect_timeout 300s;

        proxy_buffering off;              # grosse Dateien direkt streamen
        proxy_request_buffering off;

        proxy_set_header Host              $http_host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Scheme          $scheme;

        # Doppelte Header der Anwendung ausblenden (wichtig fuer ein sauberes A+)
        proxy_hide_header X-Frame-Options;
        proxy_hide_header X-Content-Type-Options;
        proxy_hide_header Strict-Transport-Security;
        proxy_hide_header Content-Security-Policy;

        proxy_pass http://127.0.0.1:50514/;
    }
}

Diese Zeilen sind die interessanten – und ihre Begründung:

ZeileWas sie wirklich tut
resolver … valid=300snginx liest für OCSP-Anfragen keine /etc/resolv.conf. Ohne resolver bleibt ssl_stapling wirkungslos. valid=300s heißt: fünf Minuten cachen, dann neu auflösen.
ssl_stapling onnginx lädt selbst den OCSP-Status und hängt ihn an den Handshake – der Client muss den Status nicht separat abfragen. Achtung: Let’s Encrypt hat seine OCSP-Responder abgeschafft; die Zeilen schaden nicht, bringen bei LE-Zertifikaten aber nichts mehr.
ssl_session_tickets offOhne Tickets wird der Sitzungsschlüssel nicht an den Client ausgelagert – besser für die Forward Secrecy, minimal langsamer.
ssl_prefer_server_ciphers offBei TLS 1.3 entscheidet immer der Client; die Direktive gilt nur für TLS 1.2. Der heutige Rat lautet off, weil moderne Clients die besseren Cipher-Reihenfolgen mitbringen.
client_max_body_size 0Kein Limit für Uploads. Praktisch für große PDFs – aber ein offenes Tor, wenn der Dienst von außen erreichbar ist. Nur sinnvoll, wenn davor eine Anmeldung steht (siehe Warnbox).
proxy_buffering offnginx puffert die Antwort nicht auf der Platte. Bei mehreren hundert MB PDFs verhindert das genau den Speicher- und Plattenstau, der sonst zu 504 oder 500 führt.
proxy_hide_header …Die Anwendung setzt eigene X-Frame-Options- und CSP-Header. Diese werden ausgeblendet, damit nur die Header aus nginx gelten – doppelte Header werfen einige Scanner durcheinander.
proxy_pass http://127.0.0.1:50514/Der Dienst lauscht nur lokal – das ist gut so. Der abschließende Schrägstrich schneidet den Location-Pfad ab; fehlt er, landen doppelte Slashes beim Backend.
client_max_body_size 0 gehört hinter eine Zugangsschwelle. Ein Upload-Limit von „unbegrenzt“ ist bei einem öffentlich erreichbaren Dienst eine Einladung: Jeder kann die Platte füllen. Bei pdf.meine.domain sitzt davor ein Login – sonst besser einen konkreten Wert setzen (z. B. 512m) und in der Firewall bzw. per Basic Auth absichern.

Aktivieren und testen:

sudo ln -s /etc/nginx/sites-available/pdf.meine.domain.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Reihenfolge beim ersten Mal: Zuerst nur den Port-80-Block aktivieren, nginx -t ausführen und neu laden. Erst dann das Zertifikat ausstellen, dann den 443-Block dazu. So kann dich kein fehlendes Zertifikat daran hindern, überhaupt eine Challenge auszuliefern.

Was du mit EC weglassen kannst

In deiner Vorlage standen noch zwei Zeilen aus der RSA-Zeit. Mit einem reinen ECDSA-Zertifikat sind sie wirkungslos – und man kann sie ersatzlos streichen:

# ssl_dhparam /etc/nginx/dhparams.pem;   # nur fuer DHE-Cipher noetig, die EC nicht nutzt
# ssl_ciphers ECDHE-ECDSA-…:ECDHE-RSA-…:DHE-RSA-…;   # RSA/DHE-Anteile sind hier Ballast
  • ssl_dhparam konfiguriert die Diffie-Hellman-Parameter für Cipher der Art DHE-RSA-*. Ein ECDSA-Zertifikat kann diese Cipher gar nicht verwenden – die Datei wird also nie gelesen. (Sie zu erzeugen kostet zudem Minuten CPU-Zeit.)
  • RSA- und DHE-Cipher in ssl_ciphers sind bei einem reinen EC-Zertifikat toter Code. Wer es aufgeräumt mag, reduziert auf die EC-Suiten:
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305;
Was du unbedingt behalten solltest: ssl_session_cache (spart Handshakes), ssl_session_tickets off (Forward Secrecy), ssl_buffer_size 1400, resolver und ssl_ecdh_curve. Diese Zeilen bringen auch mit EC echten Nutzen.
Muss man ssl_ciphers überhaupt noch setzen? Für TLS 1.3 nein – die dortigen Suiten sind fest verdrahtet und sicher. Die Direktive betrifft nur noch den TLS-1.2-Fallback. Wenn du also alte Clients ausschließen kannst, indem du nur ssl_protocols TLSv1.3; nimmst, wird der ganze Block deutlich kürzer.

Prüfen, was nginx wirklich ausliefert

Der häufigste Fehler ist nicht das Zertifikat, sondern die Annahme, nginx habe es neu geladen. Diese drei Befehle beantworten die Frage endgültig:

# 1. Welchen Typ hat die Datei auf der Platte?
sudo openssl x509 -in /etc/ssl/private/pdf.meine.domain_ecc/fullchain.cer \
  -noout -subject -issuer -dates
sudo openssl x509 -in /etc/ssl/private/pdf.meine.domain_ecc/fullchain.cer \
  -noout -text | grep -E 'Public Key Algorithm|ASN1 OID|NIST CURVE'

Erwartete Ausgabe: id-ecPublicKey und secp384r1 bzw. NIST CURVE: P-384.

# 2. Welches Zertifikat schickt nginx im Handshake?
openssl s_client -connect pdf.meine.domain:443 \
  -servername pdf.meine.domain < /dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates -text | grep -E 'subject=|Public Key|Signature Algorithm'

# 3. Protokoll und Cipher einer echten Verbindung
openssl s_client -connect pdf.meine.domain:443 -servername pdf.meine.domain < /dev/null 2>/dev/null | grep -E '^\s*(Protocol|Cipher)'

Für eine vollständige Bewertung (Header, Cipher-Reihenfolge, HSTS):

curl -I https://pdf.meine.domain          # Header und Redirect pruefen
testssl.sh --fast pdf.meine.domain        # falls installiert: detaillierter Report
Die Challenge testweise selbst abrufen: Lege eine Datei unter /var/www/letsencrypt/.well-known/acme-challenge/test an und öffne http://pdf.meine.domain/.well-known/acme-challenge/test im Browser. Kommt der Inhalt? Dann ist der Weg für acme.sh frei. Kommt die Startseite oder ein Redirect, fehlt der include oder das ^~.

Erneuern – und der --ecc-Fallstrick

acme.sh installiert bei der Einrichtung einen Cron, der täglich prüft und 30 Tage vor Ablauf erneuert. Diese Befehle zeigen, ob das klappt:

acme.sh --list                             # alle verwalteten Zertifikate
acme.sh --info -d pdf.meine.domain --ecc   # Details EINES Zertifikats (Ablauf, Kurve, Pfade)
acme.sh --cron                             # manuellen Erneuerungslauf starten (auch fuer Tests)
FallstrickErklärung
--ecc vergessenAlle Befehle, die ein EC-Zertifikat betreffen, brauchen --ecc--info, --renew und auch --install-cert, falls du doch einmal manuell kopierst. Ohne den Schalter landet acme.sh im RSA-Verzeichnis und meldet, das Zertifikat existiere nicht.
Zwei Verzeichnisse pro DomainWenn du für dieselbe Domain RSA und EC ausstellst, gibt es domain/ und domain_ecc/. Beide werden unabhängig erneuert – praktisch für den Dual-Betrieb, verwirrend beim Debuggen.
Cron läuft als der falsche NutzerWurde acme.sh als root installiert, läuft der Cron als root und darf nach /etc/ssl/private schreiben sowie nginx neu laden. Bei einer Nutzer-Installation scheitern genau diese beiden Schritte – deshalb: als root installieren.
--renew --force als ReflexJeder erzwungene Lauf zählt gegen das Let’s-Encrypt-Limit (Duplikate pro Woche). Nur nutzen, wenn wirklich etwas geändert wurde.
# Erneuerung erzwingen (sparsam!) – mit --ecc!
acme.sh --renew -d pdf.meine.domain --ecc --force
Teste die Erneuerung, bevor du sie brauchst: acme.sh --cron einmal von Hand ausführen, danach systemctl reload nginx beobachten und mit openssl s_client prüfen, ob das neue Zertifikat ausgeliefert wird. Wer das einmal gemacht hat, schläft bei „noch 12 Tage gültig“ deutlich ruhiger.

Neue Dienste später: die Checkliste

Genau dafür hast du den zentralen Webroot gebaut. Ein neuer Dienst braucht kein neues Verfahren, sondern diese vier Schritte – hier für einen hypothetischen Dienst notes.meine.domain:

# 1) DNS: A- (und AAAA-)Record auf die Server-IP zeigen lassen
dig +short notes.meine.domain

# 2) Serverblock anlegen: Port 80 + Challenge + Redirect (443 folgt in Schritt 4)
sudo tee /etc/nginx/sites-available/notes.meine.domain.conf > /dev/null <<'EOF'
server {
    listen 80;
    listen [::]:80;
    server_name notes.meine.domain;

    location /.well-known/acme-challenge {
        root /var/www/letsencrypt;
        default_type "text/plain";
        try_files $uri =404;
    }

    location / { return 301 https://$host$request_uri; }
}
EOF

sudo ln -s /etc/nginx/sites-available/notes.meine.domain.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

# 3) Zertifikat ausstellen – EC-384, zentraler Webroot.
#    Dank CERT_HOME landen die Dateien direkt in /etc/ssl/private/notes.meine.domain_ecc/
acme.sh --issue -k ec-384 -w /var/www/letsencrypt \
  -d notes.meine.domain --reloadcmd "systemctl reload nginx.service"

# 4) 443-Block ergaenzen und testen
#    (Vorlage: der Block aus dem vorigen Abschnitt, server_name anpassen)
sudo nginx -t && sudo systemctl reload nginx
curl -I https://notes.meine.domain

Für den 443-Block kopierst du den bestehenden Block und änderst nur vier Dinge:

WasBeispiel
server_namenotes.meine.domain
Zertifikatspfade/etc/ssl/private/notes.meine.domain_ecc/…
proxy_passDer lokale Port des neuen Dienstes
error_logEigene Logdatei – so findest du Fehler sofort dem Dienst zu
Ein Muster, das sich lohnt: Halte die gemeinsamen Header (HSTS, nosniff, Referrer-Policy, Permissions-Policy) in einem zweiten Snippet – z. B. snippets/security-headers.conf. Dann enthält jeder Serverblock nur noch das, was für diesen Dienst speziell ist, und eine Änderung an den Headern gilt sofort überall.

Betriebs- und Sicherheits-Tipps

  • Backup: Mit CERT_HOME liegt alles an einer Stelle: /etc/ssl/private enthält die Zertifikate und den acme.sh-Account (inklusive account.key). Ein Serverumzug ist damit eine Sache von Minuten – pass auf, dass dieses Verzeichnis nicht in einem öffentlichen Backup landet.
  • Rechte: chmod 700 /etc/ssl/private, chmod 600 auf alle .key-Dateien. nginx läuft beim Laden als root und kann sie danach weiter nutzen – die Anwendung selbst sieht sie nie.
  • Dual-Betrieb (RSA + EC): Wenn du alte Clients nicht ausschließen willst, stelle beide Zertifikate aus und trage sie beide im Serverblock ein. nginx wählt dann automatisch das passende:
    # RSA und EC parallel - nginx waehlt anhand des Client-Hello selbst
    ssl_certificate     /etc/ssl/private/pdf.meine.domain_rsa/fullchain.cer;
    ssl_certificate_key /etc/ssl/private/pdf.meine.domain_rsa/pdf.meine.domain.key;
    ssl_certificate     /etc/ssl/private/pdf.meine.domain_ecc/fullchain.cer;
    ssl_certificate_key /etc/ssl/private/pdf.meine.domain_ecc/pdf.meine.domain.key;
  • TLS 1.3 0-RTT (ssl_early_data): Der erste Datenblock darf ohne vollen Handshake fliegen – das spart Latenz, ist aber wiederholbar (Replay). Für lesende Dienste unkritisch, bei Uploads und POSTs riskant. Wenn du es nutzt, informiere den Upstream mit proxy_set_header Early-Data $ssl_early_data; und lehne nicht-idempotente 0-RTT-Anfragen ab.
  • OCSP-Stapling: Aus historischen Gründen noch in vielen Vorlagen. Let’s Encrypt hat seine OCSP-Responder abgeschafft – die Zeilen sind bei LE-Zertifikaten wirkungslos. Wer den Block aufräumt, kann ssl_stapling, ssl_stapling_verify und den resolver streichen.
  • Regex-Locations und Header: add_header wird in einer location neu gesetzt und ersetzt die Header der Serverebene. Prüfe deshalb nach jedem neuen location-Block mit curl -I, ob HSTS & Co. noch ankommen.
  • Deploy-Hooks statt reloadcmd-Blindflug: acme.sh kann nach der Installation mehrere Befehle ausführen (--reloadcmd akzeptiert eine Kette). Wer mag, startet dort zusätzlich einen Health-Check, etwa curl -sf https://pdf.meine.domain >/dev/null.

Häufige Probleme (FAQ)

ProblemLösung
acme.sh findet das EC-Zertifikat nicht--ecc fehlt. Prüfe mit ls -d /etc/ssl/private/pdf.meine.domain_ecc/, ob es dort liegt, und ergänze den Schalter im Befehl.
Zertifikate landen weiter unter /root/.acme.shCERT_HOME wurde nicht gelesen: Schreibweise in account.conf prüfen (genau CERT_HOME='/etc/ssl/private') und einmal neu ausstellen – bereits ausgestellte Zertifikate bleiben im alten Verzeichnis liegen.
Challenge endet mit 404Wurde der Ordner /var/www/letsencrypt/.well-known/acme-challenge angelegt? Greift ein Rewrite oder Redirect vor der Location? Antwortet die Domain auf Port 80 wirklich?
nginx -t: „cannot load certificate“Pfad prüfen. Die Dateien entstehen erst beim Ausstellen – mit ls -l /etc/ssl/private/pdf.meine.domain_ecc/ nachsehen, ob sie dort liegen. Zweithäufigste Ursache: ein Tippfehler im Dateinamen der ssl_certificate-Zeile.
Browser zeigt weiter das alte Zertifikatsystemctl reload nginx ausführen und mit openssl s_client gegenprüfen. Browser-Caches für Zertifikate sind hartnäckig – Zwischenspeicher leeren oder einen anderen Browser testen.
„Too many certificates already issued“Let’s-Encrypt-Limit. Eine Woche warten oder mit --staging testen (erzeugt ungültige, aber vollständige Zertifikate).
Nach dem Neustart ist die Challenge nicht erreichbarDer Port-80-Serverblock ist nicht aktiviert (kein Symlink in sites-enabled) oder der Eintrag wurde beim Aufräumen entfernt.
Erneuerung läuft, aber nginx bedient das alte Zertifikat--reloadcmd fehlt oder wurde nicht gespeichert. Mit acme.sh --info -d … --ecc nachsehen, welcher Befehl hinterlegt ist – im Zweifel einmal mit --issue plus --reloadcmd neu ausstellen.
Zwei verschiedene Zertifikate für dieselbe DomainDu hast RSA und EC parallel ausgestellt. Entscheide dich, welches der Serverblock nutzt – oder trage bewusst beide ein (Dual-Betrieb).

Fazit

Der Unterschied zwischen „ein Zertifikat besorgt“ und „ein Zertifikatssystem betrieben“ steckt in drei Dingen: ein Schlüsseltyp, der zum modernen Stack passt, ein Challenge-Pfad, der für alle Dienste gleich funktioniert, und eine nginx-Konfiguration, die das Zertifikat genau einmal referenziert. Danach ist jeder neue Dienst ein Fünf-Minuten-Job.

Die fünf Merksätze dieses Teils:-k ec-384 für den Schlüssel, ssl_ecdh_curve für den Austausch – zwei verschiedene Dinge. ② Bei EC-Zertifikaten gehört --ecc in jeden acme.sh-Befehl. ③ Ein Webroot (/var/www/letsencrypt) und CERT_HOME='/etc/ssl/private' ersetzen sowohl das Kopieren von Pfaden als auch das Kopieren von Zertifikaten. ④ ssl_dhparam und RSA-Cipher sind mit EC Ballast – ersatzlos streichen. ⑤ Einmal acme.sh --cron von Hand testen, dann nie wieder über Zertifikate nachdenken.
📝
HuuuHosting-Redaktion

Open-Source-Tools für den eigenen Server – getestet, dokumentiert und in der Debian-13-Serie Schritt für Schritt erklärt.