Was gleich bleibt wie Let’s Encrypt

Wenn du jemals ein Let’s-Encrypt-Zertifikat bezogen hast, kannst du die interne CA bereits benutzen. Das Protokoll ist identisch — ACME RFC 8555. Die Clients, die du schon kennst, funktionieren ohne Modifikation.

Gleich:

  • HTTP-01- und DNS-01-Challenges.
  • Account-Registrierung und Schlüsselverwaltung.
  • 90-Tage-Default-Zertifikatslaufzeit.
  • Automatische Erneuerung bei 60 Tagen.
  • Wildcard-Zertifikate via DNS-01.

Was anders ist

Zwei kleine Unterschiede, beide einmalige Setup-Schritte:

  1. Die ACME-Directory-URL zeigt auf die interne CA, nicht auf https://acme-v02.api.letsencrypt.org/directory.
  2. Das Root-Zertifikat der internen CA muss vom anfragenden Client als vertrauenswürdig akzeptiert werden. Das wird in der Regel via Konfigurations-Management (Ansible, Puppet, MDM) beim Host-Provisioning eingerichtet.

Das ist der gesamte Unterschied. Alles andere ist derselbe Let’s-Encrypt-Workflow, den du schon kennst.

Client auf die interne CA zeigen

Der interne ACME-Endpoint ist unter einer stabilen internen URL veröffentlicht (typischerweise https://acme.example.internal/acme/acme/directory, aber der genaue Pfad hängt vom Deployment ab). Hier folgen Rezepte für gängige Clients.

certbot

certbot --server https://acme.example.internal/acme/acme/directory \
        --email ops@example.internal \
        --no-eff-email \
        --agree-tos \
        certonly --standalone -d service.example.internal

Beim Renewal wird das --server-Flag in renewal/*.conf persistiert, sodass der periodische certbot renew Cron es automatisch übernimmt.

acme.sh

acme.sh --register-account \
        --server https://acme.example.internal/acme/acme/directory \
        -m ops@example.internal

acme.sh --issue \
        --server https://acme.example.internal/acme/acme/directory \
        --standalone \
        -d service.example.internal

lego

lego --server=https://acme.example.internal/acme/acme/directory \
     --email=ops@example.internal \
     --domains=service.example.internal \
     --http run

Caddy

In deinem Caddyfile:

{
  acme_ca https://acme.example.internal/acme/acme/directory
  email ops@example.internal
}

service.example.internal {
  reverse_proxy localhost:8080
}

Caddy holt und erneuert das Zertifikat automatisch.

Traefik

In deiner dynamischen Konfiguration:

certificatesResolvers:
  internal:
    acme:
      caServer: "https://acme.example.internal/acme/acme/directory"
      email: ops@example.internal
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web

certResolver: internal in den Router-Definitionen referenzieren.

Root-Zertifikat auf den Clients installieren

Damit die von der CA ausgestellten Zertifikate ohne Warnung vertraut werden, muss der CA-Root im System-Truststore jedes Hosts liegen, der die Zertifikate sieht.

Linux (Debian-/Devuan-Familie)

sudo cp internal-ca-root.crt /usr/local/share/ca-certificates/internal-ca-root.crt
sudo update-ca-certificates

Linux (RHEL-Familie)

sudo cp internal-ca-root.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust extract

OpenBSD / FreeBSD

# OpenBSD
sudo install -m 0644 internal-ca-root.crt /etc/ssl/internal-ca-root.crt
sudo cat /etc/ssl/cert.pem internal-ca-root.crt > /etc/ssl/cert.pem.new
sudo mv /etc/ssl/cert.pem.new /etc/ssl/cert.pem

macOS

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain internal-ca-root.crt

Windows

certutil.exe -addstore -f "ROOT" internal-ca-root.crt

Für Browser, die eigene Truststores pflegen (Firefox), muss das Root zusätzlich über Einstellungen → Datenschutz → Zertifikate → Zertifizierungsstellen importiert werden.

Diese Installation wird in der Regel durch Konfigurations- Management übernommen. Per Hand ist es für einen Einzelfall okay, skaliert aber nicht für eine Flotte.

Wildcard-Zertifikate

Wildcards werden über die DNS-01-Challenge ausgestellt — der Client weist Kontrolle über eine Domain nach, indem er einen TXT-Record anlegt. Das funktioniert für rein interne Domains, weil die interne CA TXT-Records auf jedem DNS-Server validieren kann, den du kontrollierst.

Beispiel mit acme.sh und dem BIND-nsupdate-Provider:

acme.sh --issue \
        --server https://acme.example.internal/acme/acme/directory \
        --dns dns_nsupdate \
        -d '*.example.internal' \
        -d 'example.internal'

Der nsupdate-Provider muss gegen deinen internen DNS konfiguriert sein — siehe die acme.sh-Dokumentation für die spezifischen Umgebungsvariablen.

Erneuerung

Erneuerung funktioniert genauso wie bei Let’s Encrypt: der Client prüft die verbleibende Gültigkeit und erneuert, wenn der Schwellwert unterschritten ist (60 Tage für ein 90-Tage-Zert).

Für die meisten Deployments funktioniert der bestehende Renewal-Cron einfach (certbot renew, acme.sh --cron, Caddys automatische Erneuerung).

Bei Renewal-Fehlern:

  1. Ist die Directory-URL noch erreichbar? Eine Netzwerkänderung kann den Pfad vom Client zur CA gebrochen haben.
  2. Ist das Root-Zertifikat abgelaufen oder rotiert worden? Roots sind typischerweise 10 Jahre gültig; Intermediates kürzer. Eine Rotation, die nicht zu allen Clients propagiert ist, äußert sich als Renewal-Fehler.
  3. Ist der Challenge-Type noch angemessen? Ein Service, der früher für HTTP-01 erreichbar war, kann jetzt hinter einem Load-Balancer sitzen, der die Challenge-Antwort bricht.

Limits und Policies

Die interne CA setzt Per-Account-Policies durch — welche Domains jedes Konto Zertifikate anfragen darf, welche maximale Laufzeit erlaubt ist, welche Challenge-Types akzeptiert werden.

Wenn ein Request, der gestern funktioniert hat, plötzlich mit policy denied-Fehler scheitert, hat sich die Policy geändert (oder dein Account-Binding). Wende dich an deinen CA-Operator. Der Betrieb-Service ist der Eskalationspfad, falls dein CA-Operator wir sind.