nginx + acme.sh Teil 2: ECC-Zertifikate und ein Webroot für alle Dienste
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.
| Thema | Teil 1 | Teil 2 |
|---|---|---|
| Schlüsseltyp | Standard (RSA) | ECDSA P-384 (-k ec-384) |
| Webroot | DocumentRoot 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-Block | Statische Seite | Reverse Proxy mit Sicherheits-Headern und großen Uploads |
| Rechte | acme.sh als normaler Nutzer | acme.sh als root – wegen /etc/ssl/private und reloadcmd |
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 -k | Typ | Kurve / Länge |
|---|---|---|
ec-256 | ECDSA | secp256r1 (P-256) – der übliche, schnelle Einstieg |
ec-384 | ECDSA | secp384r1 (P-384) – höhere Sicherheitsmarge, etwas größer |
ec-521 | ECDSA | secp521r1 (P-521) – maximal, wird selten gebraucht |
2048 / 3072 / 4096 | RSA | klassische 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.
-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.
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/letsencryptsucht 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"undtry_files $uri =404;sorgen für eine saubere Auslieferung bzw. eine klare 404 statt der Startseite – das macht das Debuggen deutlich einfacher.
/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.
^~ 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"
| Option | Bedeutung |
|---|---|
-k ec-384 | Erzeugt einen ECDSA-Schlüssel auf der Kurve P-384 (lang: --keylength ec-384). |
-w /var/www/letsencrypt | Webroot-Modus: acme.sh schreibt den Token dorthin, Let’s Encrypt liest ihn über Port 80 zurück. |
-d pdf.meine.domain | Die Domain im Zertifikat. Mehrfach angebbar – dann landen alle Namen im selben Zertifikat (SAN). |
--reloadcmd | Befehl, 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_eccist wichtig – dazu später mehr. - Dort entstehen
pdf.meine.domain.key(privater Schlüssel),pdf.meine.domain.cer(Zertifikat),ca.cer(Zwischenzertifikat) undfullchain.cer(Zertifikat + Kette). - Ein Kontingent-Hinweis: Let’s Encrypt begrenzt Zertifikate pro Domain und Woche. Für Tests vorab
--stagingergänzen und erst danach produktiv ausstellen.
--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 / Folge | Was 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 Pflicht | Ohne 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 um | CERT_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 sinnvoll | Bereits ausgestellte Zertifikate liegen im alten Verzeichnis und werden nicht automatisch gefunden. Wer umstellt, stellt einmal neu aus. |
Der --ecc-Schalter bleibt | Bei 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/
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:
| Zeile | Was sie wirklich tut |
|---|---|
resolver … valid=300s | nginx 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 on | nginx 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 off | Ohne Tickets wird der Sitzungsschlüssel nicht an den Client ausgelagert – besser für die Forward Secrecy, minimal langsamer. |
ssl_prefer_server_ciphers off | Bei 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 0 | Kein 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 off | nginx 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
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_dhparamkonfiguriert die Diffie-Hellman-Parameter für Cipher der ArtDHE-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_cipherssind 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;
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.
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
/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)
| Fallstrick | Erklärung |
|---|---|
--ecc vergessen | Alle 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 Domain | Wenn 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 Nutzer | Wurde 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 Reflex | Jeder 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
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:
| Was | Beispiel |
|---|---|
server_name | notes.meine.domain |
| Zertifikatspfade | /etc/ssl/private/notes.meine.domain_ecc/… |
proxy_pass | Der lokale Port des neuen Dienstes |
error_log | Eigene Logdatei – so findest du Fehler sofort dem Dienst zu |
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_HOMEliegt alles an einer Stelle:/etc/ssl/privateenthält die Zertifikate und den acme.sh-Account (inklusiveaccount.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 600auf 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 mitproxy_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_verifyund denresolverstreichen. - Regex-Locations und Header:
add_headerwird in einerlocationneu gesetzt und ersetzt die Header der Serverebene. Prüfe deshalb nach jedem neuenlocation-Block mitcurl -I, ob HSTS & Co. noch ankommen. - Deploy-Hooks statt reloadcmd-Blindflug: acme.sh kann nach der Installation mehrere Befehle ausführen (
--reloadcmdakzeptiert eine Kette). Wer mag, startet dort zusätzlich einen Health-Check, etwacurl -sf https://pdf.meine.domain >/dev/null.
Häufige Probleme (FAQ)
| Problem | Lö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.sh | CERT_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 404 | Wurde 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 Zertifikat | systemctl 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 erreichbar | Der 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 Domain | Du 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.
-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.