Wer lokale Kubernetes-Umgebungen betreibt, kennt das Problem: Echte TLS-Zertifikate von Let's Encrypt funktionieren nur mit öffentlich erreichbaren Domains. Für lokale Tests und Entwicklungsumgebungen braucht man eine andere Lösung. Mit mkcert lässt sich eine eigene lokale CA erstellen, die der Browser als vertrauenswürdig akzeptiert und mit cert-manager kann diese CA direkt im Cluster genutzt werden, um Zertifikate automatisch auszustellen.

Voraussetzungen

  • Ein laufender Kubernetes-Cluster (z. B. k3s, kind, minikube)
  • kubectl konfiguriert
  • cert-manager im Cluster installiert
  • macOS mit Homebrew (die Konzepte funktionieren aber plattformübergreifend)

Schritt 1: mkcert installieren

brew install mkcert
brew install nss   # Für Firefox-Unterstützung

nss wird benötigt, damit Firefox die CA ebenfalls als vertrauenswürdig akzeptiert.

Schritt 2: Lokale CA erstellen und im System installieren

mkcert -CAROOT    # Zeigt das Verzeichnis mit der CA
mkcert -install   # Installiert die CA im System-Truststore

mkcert -install trägt die CA in den System-Truststore ein, sodass alle Browser auf dem lokalen Rechner Zertifikate dieser CA ohne Warnung akzeptieren. Das CA-Verzeichnis liegt standardmäßig unter ~/Library/Application Support/mkcert (macOS) bzw. ~/.local/share/mkcert (Linux).

Schritt 3: CA-Zertifikat als Kubernetes Secret hinterlegen

cert-manager benötigt das CA-Zertifikat und den zugehörigen Private Key als kubernetes.io/tls-Secret im cert-manager-Namespace:

CAROOT=$(mkcert -CAROOT)

kubectl create secret tls mkcert-ca \
  --cert="$CAROOT/rootCA.pem" \
  --key="$CAROOT/rootCA-key.pem" \
  --namespace=cert-manager \
  --dry-run=client -o yaml | kubectl apply -f -

Der --dry-run=client -o yaml-Trick erzeugt das YAML, ohne es direkt anzuwenden – so lässt sich das Manifest erst prüfen oder per GitOps einchecken. Hier wird es direkt über eine Pipe nach kubectl apply weitergeleitet.

Hinweis: Der Private Key der CA (rootCA-key.pem) ist sensitiv. Für produktionsähnliche Umgebungen empfiehlt sich die Verschlüsselung mit SOPS/Age vor dem Einchecken in ein Git-Repository.

Schritt 4: ClusterIssuer anlegen

Mit dem Secret im Cluster kann jetzt ein ClusterIssuer erstellt werden, der cert-manager mitteilt, wie Zertifikate ausgestellt werden sollen:

cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: mkcert-ca-issuer
spec:
  ca:
    secretName: mkcert-ca
EOF

Ein ClusterIssuer ist clusterübergreifend verfügbar, im Gegensatz zu einem Issuer, der auf einen einzelnen Namespace beschränkt ist. Das Secret muss sich dabei immer im cert-manager-Namespace befinden.

Schritt 5: Beispiel-Deployment und Service

Zum Testen deployen wir einen einfachen nginx:

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-service
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-service
  template:
    metadata:
      labels:
        app: my-service
    spec:
      containers:
        - name: my-service
          image: nginx:alpine
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-service
  ports:
    - port: 80
      targetPort: 80
EOF

Schritt 6: Ingress mit TLS-Annotation

Der entscheidende Teil: Im Ingress wird über die Annotation cert-manager.io/cluster-issuer mitgeteilt, welcher Issuer für das Zertifikat zuständig ist. cert-manager erkennt die Annotation, fordert automatisch ein Zertifikat an und legt es im angegebenen Secret ab.

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: mkcert-ca-issuer
  name: my-ingress
spec:
  tls:
    - hosts:
        - myapp.mydomain.tld
      secretName: mkcert-tls
  rules:
    - host: myapp.mydomain.tld
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-service
                port:
                  number: 80
EOF

cert-manager erstellt kurz darauf ein Certificate-Objekt und das Secret mkcert-tls mit dem ausgestellten Zertifikat. Der Status lässt sich so prüfen:

kubectl get certificate
kubectl describe certificate mkcert-tls

Wie es funktioniert

mkcert (lokal)
└── rootCA.pem + rootCA-key.pem
│
▼
Kubernetes Secret "mkcert-ca"
(cert-manager Namespace)
│
▼
ClusterIssuer "mkcert-ca-issuer"
│
▼ (Ingress-Annotation löst aus)
cert-manager stellt Zertifikat aus
│
▼
Secret "mkcert-tls" → Ingress → HTTPS ✓

Der Browser auf dem lokalen Rechner vertraut dem Zertifikat, weil mkcert -install die Root-CA bereits in den System-Truststore eingetragen hat.

Fazit

Mit dieser Kombination aus mkcert und cert-manager lassen sich lokale Kubernetes-Entwicklungsumgebungen mit vollständig vertrauenswürdigen TLS-Zertifikaten ausstatten, ohne Browser-Warnungen, ohne öffentliche DNS-Einträge und ohne manuelle Zertifikatsverwaltung. cert-manager übernimmt dabei die Ausstellung und Erneuerung vollautomatisch, genau wie im Produktionsbetrieb mit Let's Encrypt.