Warum eine eigene CA
Schau dir an, was heute in einem Heimnetz hängt. Ein NAS, Proxmox, Home Assistant, ein Drucker, die Admin-Seite eines Routers. Jedes davon liefert HTTPS mit einem selbstsignierten Zertifikat aus. Jeder Browser, der vorbeikommt, wirft eine Warnung. Die Leute klicken sie weg. Dann die nächste, und die danach, bis das Wegklicken einer Zertifikatswarnung einfach das ist, was man eben tut. Genau diese Gewohnheit braucht ein Angreifer.
Eine öffentliche CA löst das nicht. Let’s Encrypt stellt für localhost nichts aus, „because nobody uniquely owns it“1, und dasselbe gilt für jeden Namen, der nur im Haus existiert. Selbst für eine echte Domain landet jedes ausgestellte Zertifikat in den öffentlichen Certificate-Transparency-Logs2. Hol dir eins für nas.yourname.uk, und der Name deines NAS ist ab jetzt öffentlich nachlesbar.
Eine eigene CA löst beides. Sie signiert jeden Namen, den du drinnen benutzt. Nichts wird irgendwo veröffentlicht. Sobald jedes Gerät ihrem Root vertraut, hören die Warnungen auf, und eine Warnung bedeutet wieder etwas.
step-ca ist Smallsteps Open-Source-CA, und sie läuft in einem Container. Alles hier lief auf step-ca 0.30.2 in Podman, mit Caddy 2.11.7 und nginx 1.30.5 als Clients.
| Anwendungsfall | Was step-ca dafür liefert | Abschnitt |
|---|---|---|
| Jedes Gerät vertraut deinen Zertifikaten | Ein Root, einmal installiert | Dem Root vertrauen |
| Internes HTTPS für alles, was ACME spricht | Dasselbe Protokoll wie Let’s Encrypt, für interne Namen | Internes HTTPS mit ACME |
| Dienste ohne ACME-Client | Zertifikate, die sich per Timer selbst erneuern | Dienste, die kein ACME können |
| Telefone und andere Client-Zertifikate | Schlüssel, mit denen ein Gerät hineinkommt, und ein Weg, sie zu widerrufen | Eigener Beitrag: die Voreinstellungen stehen im Weg |
| SSH-Anmeldungen | Zertifikate statt authorized_keys | Eigener Beitrag: ein anderer Aufbau |
Zwei davon haben eigene Beiträge, weil jeder ein eigener Aufbau ist. Client-Zertifikate von step-ca behandelt Telefone und mTLS, wo fünf Voreinstellungen geändert werden müssen. SSH-Anmeldungen, signiert von step-ca behandelt SSH, das andere Schlüssel und auf jeder Maschine andere Dateien benutzt.
Der Start
Das Image initialisiert sich beim ersten Start aus ein paar Umgebungsvariablen3. Zwei davon legen Dinge fest, die sich später nicht mehr ändern lassen, ohne neu anzufangen, also setz sie am ersten Tag.
| Variable | Bewirkt |
|---|---|
DOCKER_STEPCA_INIT_NAME | Den Ausstellernamen auf jedem Zertifikat |
DOCKER_STEPCA_INIT_DNS_NAMES | Die Namen, unter denen die CA antwortet |
DOCKER_STEPCA_INIT_ACME=true | Einen ACME-Provisioner für internes HTTPS |
DOCKER_STEPCA_INIT_SSH=true | Zusätzlich SSH-CA-Schlüssel für Hosts und Benutzer |
Die Schalter für ACME und SSH liest der Entrypoint des Images4. Schalt beide jetzt ein, auch wenn du heute nur einen davon willst. SSH später nachzurüsten heißt, eine neue CA zu bauen.
podman run -d --name step-ca \
-v step:/home/step \
-p 9000:9000 \
-e DOCKER_STEPCA_INIT_NAME="Home CA" \
-e DOCKER_STEPCA_INIT_DNS_NAMES="localhost,ca.home.example" \
-e DOCKER_STEPCA_INIT_ACME=true \
-e DOCKER_STEPCA_INIT_SSH=true \
docker.io/smallstep/step-ca:0.30.2
Es baut einen Root, eine von diesem Root signierte Zwischen-CA, die SSH-Schlüssel und drei Provisioner und lauscht dann auf Port 9000:
X.509 Root Fingerprint: cd2f8d4c60af32a0a534781cd63b4e2a9ce362e2c1ae5983b56a370bfa2c2489
SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAy...
Serving HTTPS on :9000 ...
| Provisioner | Typ | Signiert |
|---|---|---|
admin | JWK | Alles, was du mit seinem Passwort anforderst |
acme | ACME | Zertifikate für Clients, die beweisen, dass ihnen der Name gehört |
sshpop | SSHPOP | Erneuerungen von SSH-Host-Zertifikaten |
Der Provisioner ist die Vorstellung der CA davon, „wer fragt“. Jeder hat seine eigenen Regeln, und das meiste im Folgenden ist das Setzen dieser Regeln.
Pinne die Version. latest war 0.30.2 an dem Tag, an dem das hier geschrieben wurde, und eine CA ist das Letzte, was sich bei einem Pull unter dir ändern soll.
Zwei Passwörter, nicht eins
Smallsteps Docker-Anleitung sagt, ein erzeugtes Passwort decke beides ab, „the encrypted CA keys and the default CA provisioner“, abgelegt in secrets/password3. Bei diesem Image passiert das nicht. Versuch, mit diesem Passwort ein Zertifikat auszustellen, und der Provisioner lehnt ab:
failed to decrypt JWE: invalid password
Der Entrypoint des Images erklärt es selbst. Er erzeugt zwei zufällige Passwörter, eins für die CA-Schlüssel und eins für den Provisioner. Das des Provisioners wird einmal ins Log geschrieben und dann geschreddert4:
echo "👉 Your CA administrative password is: $(< $STEPPATH/provisioner_password )"
...
shred -u $STEPPATH/provisioner_password
| Passwort | Entsperrt | Wo es landet |
|---|---|---|
secrets/password | Die Schlüssel von Root und Zwischen-CA | Im Volume, benutzt bei jedem Start der CA |
| Provisioner-Passwort | Den admin-Provisioner, der jedes angeforderte Zertifikat signiert | Im Log des ersten Starts, sonst nirgends |
Lies also beim ersten Start das Log und leg das zweite Passwort an einen sicheren Ort. Verlierst du es, kannst du kein weiteres Zertifikat ausstellen, ohne den Provisioner von Hand herauszueditieren. Setz DOCKER_STEPCA_INIT_PASSWORD vor dem ersten Start, wenn du lieber selbst eins wählst; der Entrypoint benutzt es dann für beides4.
Dem Root vertrauen
Nichts vertraut deiner CA, bis du es ihm sagst. Ein Debian-Rechner fragt Caddy nach einer Seite, die step-ca signiert hat:
$ curl https://nas.home.example/
curl: (60) SSL certificate problem: unable to get local issuer certificate
Kopier den Root hinein, und dieselbe Anfrage geht durch:
cp root_ca.crt /usr/local/share/ca-certificates/home-ca.crt
update-ca-certificates
$ curl https://nas.home.example/
internal service, certificate from step-ca
Das ist die ganze Arbeit. Einmal pro Gerät. step certificate install tut dasselbe für das System, auf dem es läuft5. Nur der Root kommt in einen Trust Store. Die Zwischen-CA reist mit jedem Zertifikat mit, und ein Telefon, das ein Client-Zertifikat vorzeigen soll, braucht sie auch; das steht im Beitrag über Client-Zertifikate.
Behandle den Root wie den Haustürschlüssel, der er jetzt ist. Wer den privaten Schlüssel des Roots hat, kann ein Zertifikat für jeden beliebigen Namen prägen, und jedes Gerät, das ihm vertraut, wird es glauben.
Internes HTTPS mit ACME
ACME ist das Protokoll, das Let’s Encrypt benutzt, und step-ca spricht es für interne Namen6. Alles mit einem ACME-Client bekommt interne Zertifikate auf demselben Weg wie öffentliche, Erneuerungen eingeschlossen. Für Caddy sind es zwei globale Zeilen:
{
acme_ca https://ca.home.example:9000/acme/acme/directory
acme_ca_root /etc/caddy/root_ca.crt
}
nas.home.example {
reverse_proxy 192.0.2.30:5000
}
Was Caddy beim ersten Start geloggt hat:
"msg":"trying to solve challenge","identifier":"nas.home.example","challenge_type":"tls-alpn-01"
"msg":"certificate obtained successfully","identifier":"nas.home.example"
| Prüfung | Ergebnis |
|---|---|
| Challenge | tls-alpn-01, von Caddy auf Port 443 beantwortet |
| Aussteller | Home CA Intermediate CA |
| Laufzeit | 24 Stunden, von 17:31 bis 17:32 am nächsten Tag |
| Erneuerung | Caddys eigene Aufgabe, nichts einzurichten |
Ein Zertifikat für 24 Stunden klingt beunruhigend. Ist es nicht. Ein ACME-Client erneuert lange vor Ablauf, jeden Tag, ohne dass jemand eingreift. Ein gestohlener Schlüssel ist einen Tag wert. Um diese Voreinstellung herum wurde step-ca gebaut, und hier passt sie genau.
Die Challenge braucht allerdings eine CA, die den Namen erreicht, für den sie signiert. step-ca muss nas.home.example auflösen und sich auf Port 443 für tls-alpn-01 verbinden, oder auf Port 80 für http-01. Internes DNS, das beide Seiten benutzen, ist das, was zuerst stimmen muss.
Dienste, die kein ACME können
Jede Menge Geräte haben keinen ACME-Client: die Web-Oberfläche eines Hypervisors, ein Drucker, eine alte Appliance. Für die stellst du das Zertifikat mit dem admin-Provisioner aus und erneuerst es mit step ca renew:
step ca certificate proxmox.home.example pve.crt pve.key
step ca renew --force pve.crt pve.key
| Seriennummer | Gültig bis | |
|---|---|---|
| Ausgestellt | 241943140971… | 2026-10-10 17:33:03 |
Nach step ca renew | 206409156462… | 2026-10-10 17:33:06 |
Neue Seriennummer, frische 24 Stunden, und kein Passwort abgefragt. Das aktuelle Zertifikat beweist, wer fragt, und genau darum geht es: die Erneuerung braucht kein Geheimnis auf der Kiste außer dem Schlüssel, den sie schon hat. Sie braucht aber ein noch gültiges Zertifikat, also heißt eine Erneuerung, die ihr Fenster verpasst, von Hand neu ausstellen.
Lässt du es laufen, erneuert es sich selbst. --daemon erneuert standardmäßig nach zwei Dritteln der Laufzeit, und --exec führt nach jeder Erneuerung einen Befehl aus, und dort kommt das Reload hin7:
step ca renew --daemon --exec "systemctl reload nginx" pve.crt pve.key
Starte das aus einer systemd-Unit, und die Kiste kümmert sich um sich selbst. Vergiss es, und nach 24 Stunden wirft der Dienst genau die Warnung, die du loswerden wolltest.
Die CA ist der einfache Teil
step-ca zum Laufen zu bringen war ein Befehl. Einen Debian-Rechner dazu zu bringen, ihr zu vertrauen, waren zwei. Caddy brauchte zwei Zeilen und hat danach nie wieder etwas verlangt. Das ist der Teil, den Leute jahrelang vor sich herschieben, während sie mit einer Browserwarnung auf jeder internen Seite leben, und es ist die billigste Arbeit auf dieser Liste.
Was kostet, ist alles, was ein Zertifikat nach der Ausstellung berührt: der Root auf jedem Gerät, die Erneuerungen, auf die keiner schaut, der eine Dienst, der kein ACME kann und an den man denken muss. Nichts davon ist schwer. Alles davon ist die Art Arbeit, die nur ordentlich erledigt wird, wenn sie jemandem gehört.
Ein Heimnetz ist ein kleiner Betrieb. Es verdient dasselbe wie ein richtiger: einen Root, sorgfältig verwahrt, bewusst vertraut, und eine Warnung, die etwas bedeutet, wenn sie auftaucht. Die CA ist also nicht der clevere Teil. Der clevere Teil ist, nie wieder etwas wegzuklicken.
Let’s Encrypt — Certificates for localhost — kann nicht für „localhost“ ausstellen, „because nobody uniquely owns it, and it’s not rooted in a top level domain“. ↩︎
Let’s Encrypt — Certificate Transparency logs — „Let’s Encrypt submits all certificates we issue to CT logs.“ ↩︎
Smallstep — Run a private CA in Docker — die Variablen
DOCKER_STEPCA_INIT_*; sagt, das erzeugte Passwort sei für „the encrypted CA keys and the default CA provisioner“. ↩︎ ↩︎smallstep/certificates — docker/entrypoint.sh — erzeugt getrennte Dateien
passwordundprovisioner_password, gibt das Provisioner-Passwort einmal aus und schreddert es; ein vorgegebenes Passwort wird in beide geschrieben. ↩︎ ↩︎ ↩︎Smallstep — step certificate install — installiert ein Root-Zertifikat in den Trust Store des Systems. ↩︎
Smallstep — ACME basics — der ACME-Provisioner von step-ca und die Challenge-Typen, die er unterstützt. ↩︎
Smallstep — step ca renew — mit
--daemon: „By default, it will renew the certificate before 2/3 of the validity period of the certificate has elapsed“;--execsorgt für „certificate reloads on your services“. ↩︎