
Viele Unternehmen stehen immer wieder vor großen Herausforderungen beim Management einer internen Zertifikatsverwaltung.
Der Prozess, ein internes Zertifikat zur Absicherung der Kommunikation eines Services zu erhalten, ist oft manuell und dementsprechend zeitraubend.
Im schlimmsten Fall wird der interne Netzwerkverkehr komplett unverschlüsselt übertragen, was natürlich ein erhebliches Sicherheitsrisiko darstellt.
Eine Certificate Authority, kurz CA, bietet jedoch nicht nur die Möglichkeit, Zertifikate automatisch über das ACME-Protokoll auszustellen und zu erneuern, was den Prozess wesentlich vereinfacht, sondern kann auch die SSH-Authentifizierung, die häufig auf einem Private-/Public-Key-Ansatz oder sogar auf Passwörtern basiert, sicherer gestalten.
Wie eine interne Zertifikatsverwaltung im Detail umgesetzt werden kann, zeigt das folgende minimalistische, aber vollständig funktionsfähige Setup einer Step-CA auf Basis von docker-compose.
Aufsetzen einer internen CA mit docker-compose
Das Setup einer CA auf Basis von Step-CA lässt sich mit dem folgenden docker-compose File schnell und einfach realisieren:
version: "3.7"
volumes:
step-ca-data:
networks:
step-ca-net:
secrets:
init_secret:
file: ./init_secret.txt
services:
step-ca:
container_name: step-ca
image: smallstep/step-ca:0.27.2
volumes:
- step-ca-data:/home/step
command:
secrets:
- init_secret
environment:
DOCKER_STEPCA_INIT_PASSWORD_FILE: /run/secrets/init_secret
DOCKER_STEPCA_INIT_NAME: exampleca
DOCKER_STEPCA_INIT_DNS_NAMES: localhost,192.168.xxx.xxx,ca.example.com
DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT: true
DOCKER_STEPCA_INIT_PROVISIONER_NAME: admin
DOCKER_STEPCA_INIT_SSH: true
DOCKER_STEPCA_INIT_ACME: true
ports:
- 9000:9000 # Port ggf. anpassen
networks:
- step-ca-net
Dabei werden folgende Umgebungsvariablen gesetzt:
DOCKER_STEPCA_INIT_PASSWORD_FILE: voll qualifizierender Pfad zum Init-Secret welches zur verschlüsselung der Private Keys als auch für den CA-Provisioner genutzt wird.
DOCKER_STEPCA_INIT_DNS_NAMES: Liste von Hostnamen oder IPs auf welchen die CA-Anfragen annimmt
DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT: optional, siehe auch https://smallstep.com/docs/step-ca/provisioners/index.html#enable-remote-provisioner-management
DOCKER_STEPCA_INIT_PROVISIONER_NAME: Label für den Standard Admin-Provisioner
DOCKER_STEPCA_INIT_SSH: SSH-Zertifikats-Ausstellung aktivieren
DOCKER_STEPCA_INIT_ACME: Einen initialen ACME Provisioner anlegen
Das DOCKER_STEPCA_INIT_PASSWORD_FILE sollte unter keinen Umständen in einem Repository gespeichert werden!
Idealerweise wird dieses Secret in einem sicheren Vault abgelegt (z.B. Infisical oder HashiCorp Vault) und nur im Rahmen des Deployments per Pipeline bereitgestellt.
Nach dem Starten des oben beschriebenen Setups mit
docker compose up -d
ist die CA über https://localhost:9000 erreichbar.
Der Status der CA kann wie folgt abgefragt werden:
curl -k https://localhost:9000/health
Da das Root-Zertifikat zu diesem Zeitpunkt noch nicht importiert wurde, muss die Zertifikatswarnung über den Parameter -k ignoriert werden.
Um das ROOT-Zertifikat in den Trust-Store zu importieren, muss der CA-Fingerprint bekannt sein:
CA_FINGERPRINT=$(docker exec -it step-ca step certificate fingerprint certs/root_ca.crt)
step ca bootstrap --ca-url https://localhost:9000 --fingerprint $CA_FINGERPRINT --install
Über diesen Befehl wird das Root-Zertifikat bezogen und mithilfe des Parameters --install direkt in den Trust-Store importiert.
Beim erneuten Aufruf von
curl https://localhost:9000/health
sollte nun keine Zertifikatswarnung mehr erscheinen.
Tipp: das Root-Zertifikat kann auch explizit in den Truststore importiert werden, z.B. mit:
step certificate install /example/.step/certs/root_ca.crt --all
Zertifikate ausstellen
Zertifikate können auf verschiedenen Wegen von der CA angefordert werden. Eine Möglichkeit ist es, sich über die Angabe des Provisioner-Passworts bei der CA zu authentifizieren und somit ein Zertifikat zu erhalten.
Dies kann direkt über den Step-Client erfolgen:
Anfrage per Admin-Provisioner
step ca certificate --ca-url=https://<ca-fqdn> --san <cert-request-domain> --san <alternative-dns-entry> <cert-request-domain> <cert-request-domain>.crt <cert-request-domain>.key
step ca certificate --ca-url=https://ca.example.com --san bar.example.com --san 192.168.xxx.xxx foo.example.com foo.example.com.crt foo.example.com.key
Im anschließenden Dialog wählt man den Admin-Provisioner und gibt den entsprechenden Provisioner-Key ein. Als Resultat werden das gewünschte Zertifikat und der private Schlüssel im aktuellen Arbeitsverzeichnis gespeichert.
Anfrage per ACME-Challenge
Einfacher ist die Ausstellung des Zertifikats über eine ACME-Challenge (Automated Certificate Management Environment), wie sie unter anderem von Let’s Encrypt und Certbot verwendet wird.
Die ACME-Challenge kann entweder per HTTP oder DNS erfolgen. Im Folgenden ist die HTTP-Challenge beschrieben.
Zunächst erfolgt dabei die einmalige Registrierung eines ACME-Accounts:
certbot register [--no-verify-ssl] --server https://ca.example.com:9000/acme/acme/directory
Im Rahmen der ACME-Challenge startet der Client (in diesem Fall Certbot) standardmäßig einen Standalone-Server,
um einen zufällig generierten Wert unter der angegebenen Domain bereitzustellen.
Wenn die CA diesen Wert erfolgreich unter der Domain abfragen kann, gilt die Authentifizierung als erfolgreich und das Zertifikat für die Domain wird ausgestellt.
REQUESTS_CA_BUNDLE=/example/.step/certs/root_ca.crt certbot -n -d foo.example.com --server https://ca.example.com:9000/acme/acme/directory
Der Standalone-Server von Certbot läuft standardmäßig auf Port 80. Sollte auf demselben System bereits ein Webserver aktiv sein, kann es zu einer Port-Kollision kommen.
Dieses Problem lässt sich am einfachsten durch die Nutzung eines der verfügbaren Certbot-Plugins lösen. Im Falle von Nginx sieht der Befehl dann wie folgt aus:
REQUESTS_CA_BUNDLE=/example/.step/certs/root_ca.crt certbot --nginx -n -d foo.example.com --server https://ca.example.com:9000/acme/acme/directory --post-hook 'systemctl reload nginx.service'
Dabei wird das Zertifikat nicht nur heruntergeladen, sondern auch direkt in die Nginx-Konfiguration eingebunden.
Da das Nginx-Plugin möglicherweise Konfigurationsänderungen vornimmt, sollte die bestehende Nginx-Konfiguration zuvor gesichert werden!
Validierung
Die Letsencrypt-Zertifikate liegen unter /etc/letsencrypt/live/ und können mit dem Step-Client inspiziert werden:
step certificate inspect /etc/letsencrypt/live/foo.example.com/cert.pem --short
Zertifikatsaktualisierung
Zertifikate von Step-CA sind standardmäßig nur 24 Stunden gültig. Certbot legt allerdings in der Regel automatisch einen cronjob oder einen systemd-timer an, welcher die Zertifikate automatisch aktualisiert. Die Informationen zu Aktualisierungintervallen etc. für jedes Zertifikat sind hier hinterlegt und können bei Bedarf angepasst werden:
/etc/letsencrypt/renewal/
Die bestehenden Systemd-Timer-Einträge lassen sich mit folgendem Befehl anzeigen:
systemctl list-timers
Abhängig vom Betriebssystem kann es auch sein, dass kein systemd-timer, sondern klassische Cron-Einträge angelegt werden.
- Anpassen der Zertifikatsgültigkeit durch die CA: Erhöhen der Gültigkeitsdauer des Zertifikats direkt über die Step-CA-Konfiguration, siehe auch https://smallstep.com/docs/step-ca/renewal/#creating-short-lived-certificates
- Manuelles Bearbeiten der Renewal-Datei: Anpassen des Erneuerungsintervalls in der jeweiligen Datei unter
/etc/letsencrypt/renewal
- Automatisierte Anpassung über einen Post-Hook: Verwenden eines Post-Hooks, um die Renewal-Datei nach der Zertifikatsausstellung automatisch zu modifizieren, z.B. wie folgt:
REQUESTS_CA_BUNDLE=/root/.step/certs/root_ca.crt certbot --nginx -n -d foo.example.com --server https://ca.example.com:9000/acme/acme/directory --deploy-hook 'systemctl reload nginx.service' --post-hook 'sed -i "s/.*renew_before_expiry.*/renew_before_expiry = 4 hours/" /etc/letsencrypt/renewal/foo.example.com.conf'
#!/bin/bash
DOMAIN=$1
CA_DOMAIN=ca.example.com
CA_PORT=9000
ROOT_CERT=/root/.step/certs/root_ca.crt
REQUESTS_CA_BUNDLE=${ROOT_CERT} certbot --nginx -n -d ${DOMAIN} \
--server https://${CA_DOMAIN}:${CA_PORT}/acme/acme/directory \
--deploy-hook 'systemctl reload nginx.service' \
--post-hook "sed -i 's/.*renew_before_expiry.*/renew_before_expiry = 4 hours/' /etc/letsencrypt/renewal/${DOMAIN}.conf"
Der Aufruf von ./installCert.sh foo.example.com bezieht dann ein neues Zertifikat, installiert dieses im Nginx-Kontext inkl. Reload und legt die zugehörigen Renewal-Konfigurationen an, welche dann vom systemd-timer regelmäßig genutzt werden.
Um auch den Renewel-Konfigurationen die Root-CA bekannt zu machen, ist es am einfachsten, in der Service-Deklaration des Certbot-Timers folgende Zeile zu ergänzen:
# /etc/systemd/system/snap.certbot.renew.service
[Service]
...
Environment="REQUESTS_CA_BUNDLE=/root/.step/certs/root_ca.crt"