TLS-Zertifikate¶
Unter Administration > Betrieb > Zertifikate (/v2/admin/operations) steht,
ob diese Anlage wirklich über HTTPS läuft — und was zu tun ist, wenn nicht.
Diese Seite wurde im September 2026 vollständig neu geschrieben
Die frühere Fassung beschrieb eine Oberfläche zum Hochladen von
Zertifikaten, eine Warnung 30 Tage vor Ablauf, einen certbot-Container,
eine Protokollierung im Audit-Log und eine Operation admin_settings.
Nichts davon existierte. Was es gab, war ein Endpunkt ohne Oberfläche,
der Datenbankzeilen entgegennahm — ein eingetipptes Ablaufdatum sah darin
aus wie ein Befund.
Was calServer hier tut, und was nicht¶
calServer stellt keine Zertifikate aus. Das erledigt der ACME-Dienst
(letsencrypt-nginx-proxy, Image nginxproxy/acme-companion) neben dem
Reverse Proxy. Er beantragt sie bei Let's Encrypt, legt sie im
Zertifikatsverzeichnis ab und erneuert sie 30 Tage vor Ablauf von selbst.
calServer liest dieses Verzeichnis und vergleicht es mit den Domänen, die
ausgeliefert werden sollen. Der API-Container hat dabei weder Zugriff auf den
Docker-Socket noch Schreibrechte auf die .env oder auf das
Zertifikatsverzeichnis. Beides zu vergeben hiesse, über die Oberfläche mehr
Rechte erreichbar zu machen, als die Rolle hergibt.
Die eine Ausnahme ist ein selbst mitgebrachtes Zertifikat. Auch dort schreibt die Anwendung nicht selbst: Sie prüft und reicht an den Updater-Dienst weiter, der als einziger Container den Docker-Socket hat. Ausstellung und Erneuerung bleiben in jedem Fall beim ACME-Dienst.
Der Ausfall, den sonst niemand bemerkt
Fehlt für eine Domäne die Zertifikatsdatei, baut der Reverse Proxy für diesen Namen gar keinen HTTPS-Block. Die Anlage antwortet dann weiter über HTTP, alle Container sind grün, und in keinem Protokoll steht etwas. Genau diesen Fall macht diese Seite sichtbar.
Berechtigung¶
Lesen: basic_setting_admin — dieselbe Operation wie für den Tab
Dateispeicher. Ohne sie erscheint der Tab nicht.
Einspielen: certificate_edit (Zertifikate einspielen). Bewusst getrennt:
Wer Ablagepfade pflegen darf, soll nicht schon deshalb den privaten
TLS-Schlüssel der Anlage austauschen können. Ohne diese Operation fehlt der
Knopf Eigenes Zertifikat, die Seite bleibt aber lesbar.
Der Balken ganz oben¶
Ein Satz, der die Frage beantwortet, um die es geht.
| Meldung | Bedeutung |
|---|---|
| HTTPS ist für alle Domänen abgedeckt | Jede konfigurierte Domäne trägt ein gültiges, vertrauenswürdiges Zertifikat. Dabei steht, wann das nächste abläuft. |
| Ein Zertifikat läuft demnächst ab | Weniger als 21 Tage Restlaufzeit. Der ACME-Dienst erneuert 30 Tage vorher — bis hierher hat das also schon mehrfach nicht geklappt. |
| Eine Domäne wird ohne Zertifikat ausgeliefert | Für einen vhost gibt es keine passende Datei. Dieser Name läuft über HTTP. |
| Ausgeliefert wird ein Zertifikat, dem kein Browser traut | Es stammt aus der Let's-Encrypt-Testumgebung oder ist selbst signiert. Für Besucher sieht das aus wie ein Angriff. |
| Ein Zertifikat ist abgelaufen | Der Browser bricht ab, bevor die Anmeldung erscheint. |
| Es ist keine Domäne bekannt | CALSERVER_HOST ist leer. Ohne diese Variable baut der Reverse Proxy überhaupt keinen Server-Block. |
| Diese Anlage verwaltet keine Zertifikate | LETSENCRYPT steht nicht auf true, und es liegt keine Datei bereit. Endet TLS vorgelagert (Load Balancer, Ingress), ist das richtig so. |
| Der Zertifikatszustand ist nicht ermittelbar | Das Auslieferungsverzeichnis ist für den API-Container nicht sichtbar. Unter Kubernetes ist das der Normalfall, siehe unten. |
„Nicht ermittelbar" und „keine Zertifikate" sind bewusst zwei verschiedene Meldungen. Aus einem fehlenden Verzeichnis „ungesichert" zu machen wäre auf jeder Kubernetes-Installation durchgehend falscher Alarm.
Steht dort gar kein Balken, ist der Abruf gescheitert
Dann zeigt die Seite stattdessen den Grund samt HTTP-Status und einen Knopf zum erneuten Versuch. Das ist keine Feinheit: Die Zustandsauskunft antwortet absichtlich auch mit 200, wenn sie überhaupt kein Zertifikatsverzeichnis sieht. Ein Fehlschlag bedeutet deshalb nie „keine Zertifikate", sondern immer etwas anderes — 403 fehlende Berechtigung, 404 ein älterer Stand im API-Container als in der Oberfläche, 5xx ein Grund im Anwendungslog, kein Status eine Anfrage, die nicht angekommen ist.
Je Domäne¶
Für jede konfigurierte Domäne steht, welche Datei der Reverse Proxy heranzöge und ob sie taugt.
| Zustand | Bedeutung |
|---|---|
| Gültig | Passt, gilt für diesen Namen, läuft nicht demnächst ab. |
| Läuft ab | Weniger als 21 Tage Restlaufzeit. |
| Abgelaufen | Der Browser lehnt ab. |
| Nicht vertrauenswürdig | Testumgebung oder selbst signiert. |
| Kein Zertifikat | Keine Datei für diesen vhost — dieser Name läuft über HTTP. |
| Falscher Name | Eine Datei ist da, gilt aber nicht für diesen Namen. Der Reverse Proxy sucht nur nach dem Dateinamen und liefert sie trotzdem aus; der Browser lehnt sie ab. |
| Unbekannt | Das Verzeichnis ist nicht lesbar, es lässt sich nichts sagen. |
Dazu Aussteller, Ablaufdatum mit Restlaufzeit, die Namen, für die das Zertifikat wirklich gilt (die SAN-Liste), und der SHA-256-Fingerprint.
default.crt zählt nicht mit
Der Reverse Proxy legt sich beim Start selbst ein selbstsigniertes
default.crt an. Es gehört zu keiner Domäne — für einen benannten vhost
ohne Datei baut er gar keinen TLS-Block, statt dieses Zertifikat
auszuliefern. Diese Seite zählt es deshalb nicht als vorhandenes
Zertifikat; die Zahl in der Fusszeile kann also kleiner sein als ein ls
im Verzeichnis.
Dateiname und Geltungsbereich sind zwei verschiedene Dinge
Der Reverse Proxy wählt die Datei allein nach ihrem Namen: für
app.example.com sucht er app.example.com.crt, sonst example.com.crt,
sonst eine Ebene höher. Ob das gefundene Zertifikat den Namen wirklich
führt, prüft er nicht — der Browser schon. Deshalb gibt es den Zustand
Falscher Name getrennt von Kein Zertifikat.
Wenn eine Domäne kein Zertifikat hat¶
Zu jeder Lücke nennt die Seite die Umgebungsvariable, an der sie hängt, und den Deploy-Befehl zum Kopieren. Sie führt bewusst nichts aus.
| Domäne | Variable | Container, der neu erzeugt wird |
|---|---|---|
| Hauptanwendung | CALSERVER_HOST |
calserver (Parallelbetrieb) bzw. calserver-frontend (V2 allein) |
| Datenbankwerkzeug | ADMINER_HOST |
adminer |
| MCP-Server | MCP_HOST |
calserver-mcp (aktiviert zugleich das Profil v2-mcp) |
Der Ablauf:
- Die Variable in der
.envdes Docker-Verzeichnisses setzen. - Prüfen, dass der Name per DNS auf diesen Server zeigt und Port 80 von aussen erreichbar ist. Ohne Port 80 kann Let's Encrypt den Namen nicht prüfen, und es entsteht kein Zertifikat.
- Neu ausrollen — erst dabei bekommt der Container die geänderte Umgebung:
Eine Änderung nur in der .env bewirkt nichts: Der Reverse Proxy liest den
vhost aus der Umgebung des laufenden Containers.
Es gibt kein Feld, in das ein Befehl eingetragen wird
Dieselbe Regel wie bei den geplanten Aufgaben: Ein Feld, dessen Inhalt der Server ausführt, wäre ein Weg, sich über die Oberfläche mehr Rechte zu verschaffen, als die Rolle hergibt.
Die Staging-Falle¶
LETSENCRYPT_TEST schaltet auf die Testumgebung von Let's Encrypt. Der
Schalter wertet den Wert nicht aus — er prüft nur, ob überhaupt etwas darin
steht:
LETSENCRYPT_TEST=false schaltet die Testumgebung also ein. Zum Abschalten
muss die Variable geleert oder ganz entfernt werden. Zertifikate von dort
tragen (STAGING) im Aussteller und lösen im Browser eine Warnseite aus. Die
Seite meldet das ausdrücklich, sobald es zutrifft.
Die Tabelle darunter (Spiegel)¶
Abgleichen liest das Auslieferungsverzeichnis und schreibt die Tabelle
ssl_certificate fort. Sie ist ein Spiegel, keine Verwaltung — nach demselben
Muster wie die Siegel-Credentials:
- Das Zertifikat gewinnt bei allem, was darin steht: Inhaber, Aussteller, Seriennummer, Fingerprint, Gültigkeit, Selbstsignierung. Diese Felder werden bei jedem Abgleich überschrieben.
- Die Datenbank gewinnt bei Anzeigename und Bemerkung. Die fasst kein Abgleich an; sie werden nur beim ersten Auftauchen vorbelegt.
- Verschwundenes wird nicht gelöscht. Der Eintrag behält seinen letzten Sichtungszeitpunkt und gilt als nicht mehr ausgeliefert. Ein Mount, der beim Start kurz fehlt, käme sonst als Datenverlust daher.
Der Abgleich ist ein ausdrücklicher Knopf und passiert nicht nebenbei beim Laden: Die Anzeige oben liest ohnehin live aus dem Verzeichnis.
Ist die Tabelle selbst nicht lesbar — auf einer teilmigrierten Anlage fehlt sie
oder ihr fehlt die Spalte last_seen_at —, steht das in der Fusszeile mit der
Meldung der Datenbank. Der Zustand oben bleibt davon unberührt: Er kommt aus dem
Verzeichnis, nicht aus der Datenbank. „Noch nie abgeglichen" und „Tabelle nicht
lesbar" sind zwei verschiedene Sachverhalte, und die Fusszeile hält sie
auseinander.
Eigenes Zertifikat einspielen¶
Nicht jede Anlage geht über Let's Encrypt: gekauftes Zertifikat, Zertifikat aus
der eigenen Unternehmens-CA, Wildcard aus dem Konzern. Dafür trägt jede Domäne
den Knopf Eigenes Zertifikat. Er verlangt die Berechtigung
Zertifikate einspielen (certificate_edit) — getrennt von den
Grundeinstellungen, weil das Austauschen des privaten Schlüssels eine andere
Grössenordnung ist als das Lesen dieser Seite.
Zuerst: dem ACME-Dienst den Namen abnehmen¶
Das ist der Schritt, den man nicht überspringen darf. Solange der ACME-Dienst für einen Namen zuständig ist, stellt er weiter aus und ersetzt die Datei beim nächsten Lauf. Ein eigenes Zertifikat liegt dann zwar sofort im Verzeichnis und sieht bis dahin völlig richtig aus — und ist danach weg.
Jede Domäne trägt deshalb eine Marke: Automatisch oder Selbst gepflegt.
Steht dort noch „Automatisch", gehört in die .env:
# Die Variable SETZEN und LEER LASSEN. Ein Wert wäre ein anderer Name,
# ein Fehlen hiesse "wie bisher".
ACME_APP_HOST=
| Variable | Domäne |
|---|---|
ACME_APP_HOST |
Hauptanwendung (CALSERVER_HOST) |
ACME_ADMINER_HOST |
Datenbankwerkzeug (ADMINER_HOST) |
ACME_MCP_HOST |
MCP-Server (MCP_HOST) |
Danach neu ausrollen — eine Änderung nur in der .env bewirkt nichts, der
Container liest seine Umgebung erst beim Neuerstellen:
Wichtig: Die Variable leer zu lassen schaltet nur die Ausstellung ab. Der vhost selbst bleibt bestehen; die Anlage ist unter demselben Namen erreichbar.
Dann hochladen¶
Der Dialog nimmt drei Felder, je wahlweise als Datei oder als eingefügter Text:
| Feld | Inhalt |
|---|---|
| Zertifikat | Der PEM-Block. Enthält die Datei bereits die Kette (fullchain), reicht dieses Feld. |
| Zwischenzertifikate | Die Kette bis zur Wurzel. Ohne sie lehnen manche Clients ab, auch wenn der Browser akzeptiert. |
| Privater Schlüssel | Unverschlüsselt. Ein mit Passphrase geschützter Schlüssel muss vorher entschlüsselt werden — der Reverse Proxy kann sie nicht abfragen. |
Geprüft wird vor dem Ablegen, zweimal und unabhängig:
- Gehören Zertifikat und Schlüssel zusammen?
- Gilt das Zertifikat für diesen Namen? (Wildcards eingeschlossen.)
- Ist es gültig, also weder abgelaufen noch erst später wirksam?
Jede dieser Fragen mit „nein" nimmt dem vhost seinen HTTPS-Server-Block, und die Anlage liefe danach über HTTP weiter, während alle Container grün bleiben. Deshalb wird nichts abgelegt, was auch nur eine davon nicht besteht.
Wo der Schlüssel bleibt¶
Er wird durchgereicht und nirgends gespeichert: nicht in der Datenbank,
nicht in einer Sicherung, nicht im Protokoll. Geschrieben wird er einzig als
Datei im Auslieferungsverzeichnis, mit Rechten 0600. Protokolliert wird, was
im Zertifikat steht: Name, Aussteller, Fingerprint, Laufzeit.
Schreiben darf dort weder die Anwendung noch der API-Container: Das erledigt der
Updater-Dienst (calserver-updater), der einzige Container mit
Docker-Socket. Läuft er auf dieser Anlage nicht, meldet der Dialog das
ausdrücklich — einzurichten über UPDATER_TOKEN in der .env und das
Compose-Profil v2-updater.
Ganz ohne Let's Encrypt¶
Eine Anlage, die ihre Zertifikate ausschliesslich selbst mitbringt, lässt
LETSENCRYPT auf false — der ACME-Dienst fährt dann gar nicht mit. Der
Reverse Proxy bekommt sein Zertifikatsverzeichnis trotzdem: Es hängt seit
August 2026 am Basis-Dateisatz und nicht mehr nur am HTTPS-Overlay. Vorher gab
es diesen Weg nicht — ohne LETSENCRYPT=true hatte nginx-proxy kein
Verzeichnis, baute keinen TLS-Server-Block und lieferte still über HTTP aus.
Dasselbe galt bis Anfang 2027 für das Routing-Verzeichnis
(nginx/vhost.d), aus dem nginx-proxy die default_location zieht. Auch das
hing nur am HTTPS-Overlay, und ohne es gab es weder /api/v2/ noch /v2/ noch
den V1-root-Block — im Parallelbetrieb also eine dunkle Anlage, obwohl alle
Container liefen. Beide Verzeichnisse hängen jetzt am Basis-Dateisatz. Wer eine
ältere docker-compose.yml auf dem Host führt, holt sie sich vor der Umstellung
auf LETSENCRYPT=false.
Zwei Wege, die Dateien dorthin zu bekommen:
| Weg | Wann |
|---|---|
| Dialog „Eigenes Zertifikat" (oben) | Der Normalfall. Braucht den Updater-Dienst, prüft das Material vorher und legt es unter dem Namen ab, den der Reverse Proxy sucht. |
Hostverzeichnis über TLS_CERTIFICATE_HOST_PATH |
Wenn die Zertifikate schon auf dem Host gepflegt werden (Konzern-CA, eigenes Ausrollverfahren, Zertifikate aus einem anderen System). |
# Absoluter Pfad. Reverse Proxy, API-Container und Updater lesen danach
# dasselbe Verzeichnis — sonst beschreibt diese Seite eine andere Ablage als
# die ausgelieferte.
TLS_CERTIFICATE_HOST_PATH=/etc/ssl/calserver
Danach neu ausrollen (cd /var/calserver && ./deploy.sh). Die Dateien müssen
dort so heissen, wie der Reverse Proxy sie sucht: <vhost>.crt und
<vhost>.key, also z. B. metweb.example.com.crt. Ein Wildcard-Zertifikat
liegt unter dem Namen der Ebene, für die es gilt (example.com.crt).
Der Balken sagt „keine Zertifikate", obwohl HTTPS läuft?
Dann liest die Anwendung ein anderes Verzeichnis als der Reverse Proxy
ausliefert — der klassische Fall, wenn die Zertifikate von Hand in einen
eigenen Container gemountet wurden. Der Balken nennt deshalb den Pfad, den
er gelesen hat; die Zeile darunter zählt die gefundenen Dateien. Stimmt der
Pfad nicht, ist TLS_CERTIFICATE_HOST_PATH die Stelle.
TLS_EXPIRY_WARNING_DAYS verschiebt zusätzlich die Vorwarnzeit (Vorgabe 21
Tage). Für eine selbst gepflegte Kette ist mehr Vorlauf sinnvoll, weil
Beschaffung und Freigabe dazukommen — Let's Encrypt erneuert dagegen von selbst.
Danach¶
Der Reverse Proxy liest die Datei sofort neu ein. Klappt das nicht, sagt die Meldung es: Die Datei liegt dann, wird aber noch nicht ausgeliefert. In dem Fall hilft der Blick in sein Protokoll:
Zurück zur automatischen Ausstellung geht über denselben Weg rückwärts:
ACME_*_HOST aus der .env entfernen, neu ausrollen. Der ACME-Dienst übernimmt
den Namen dann wieder. Eine Oberfläche dafür gibt es nicht — der Vorgang kommt
selten vor und ist eine Entscheidung, keine Routine.
Kubernetes¶
Dort läuft ein völlig getrennter Weg: cert-manager stellt aus, ein
ClusterIssuer entscheidet die Quelle, und das Zertifikat liegt in einem
Secret am Ingress. Das Verzeichnis, das diese Seite liest, gibt es nicht — sie
meldet dann Zustand nicht ermittelbar samt Grund, statt Leere als „kein
Zertifikat" auszugeben. calServer steuert dort nichts, und das soll die Seite
auch sagen.
Nicht auf dieser Seite¶
- Siegel und Berichtssignatur. Die Signatur von Kalibrierscheinen läuft
über eine eigene Verwaltung mit eigenen Regeln: Siegel
einrichten. Der Unterschied ist wesentlich — der private
Schlüssel eines Siegels liegt im Siegel-Dienst (
calserver-seal), nicht in der Datenbank, und er verlässt ihn nicht, auch nicht in eine Sicherung (ADR-042). - Erneuerung eines automatisch ausgestellten Zertifikats anstossen. Entscheidet der ACME-Dienst allein.
- Erneuerung anstossen. Entscheidet der ACME-Dienst allein. Bleibt eine Erneuerung aus, steht der Grund in seinem Protokoll:
Verwandte Themen¶
- Betrieb — die Seite, auf der dieser Tab liegt
- Siegel einrichten — Signatur von Kalibrierscheinen
- Konfiguration — die Umgebungsvariablen im Überblick