Skip to content

Kubernetes cert-manager mit Hetzner DNS nutzen

Mit Hilfe von cert-manager, können wir unsere Zertifikate auf einem Kubernetes Cluster automatisiert verwalten. Zusammen mit einem Ingress-Controller werden automatisch Let’s Encrypt Zertifikate für unsere Services erstellt und bei Ablauf automatisch erneuert. Cert-manager kann mit vielen verschiedenen DNS Anbietern ohne große Anpassung arbeiten, der DNS Dienst von Hetzner ist allerdings nicht dabei. Und darum geht es in diesem Beitrag.

Wer in seinem Heimnetz gerne vertrauenswürdige Zertifikate von Let’s Encrypt einsetzen möchte, der hat meist das Problem, dass der Dienst für den wir das Zertifikat einsetzen möchten, weder aus dem Internet erreichbar ist, noch unser lokaler DNS Server. Bei der Ausstellung des Zertifikats muss aber eines von Beiden gegeben sein, um die sogenannte DNS oder HTTP Challenge möglich zu machen. Bei der HTTP Challenge, wird der Webserver unseres Dienstes durch Let’s Encrypt nach einer Datei abgefragt. Let’s Encrypt kann unseren Webserver aber aus dem Internet in aller Regel nicht erreichen und wird somit kein Zertifikat ausstellen. Ähnlich sieht es bei der DNS Challenge aus. Hier muss ein TXT Record in die Zone unseres DNS Servers eingetragen werden. Da unsere DNS Zone nicht aus dem Internet aufgelöst werden kann und unser DNS Server ebenfalls nicht erreichbar ist, können wir diese Art der Challenge auch nicht verwenden.

Die Lösung für das Problem kann sein, dass wir die DNS Challenge über einen DNS Server durchführen, der eine Zone verwaltet, die aus dem Internet erreichbar ist. Das kann ein DynDNS Anbieter wie FreeDNS sein, oder ein DNS Anbieter wie Cloudflare oder eben, wie in meinem Fall Hetzner. Einzige Voraussetzung ist, dass die DNS Zone über eine API Schnittstelle mittels Token Authentifizierung verwaltbar ist.

Für alle Dienste, die ich in meinem Homelab auf meinem Kubernetes Cluster betreibe, möchte ich gerne automatisch vertrauenswürdige Zertifikate erstellen, sobald ich diese auf dem Cluster ausrolle. Hier kommt cert-manager zum Einsatz. Cert-Manager “lauscht” auf neue Ingress Regeln und Services in meinem Cluster und wird daraus eine Zertifikatsanforderung an Let’s Encrypt erstellen. Auf meinem lokalen DNS Server, “überschreibe” ich den öffentlichen DNS Eintrag mit einer lokalen IP aus meinem Heimnetz. Entsprechend muss der Hetzner DNS Server diese nicht gesetzt haben.

Am Ende wird also ein vertrauenswürdiges Zertifikat z.B. für einen DNS Namen myservice.cloud.thedatabaseme.de ausgestellt sein. Die Zone thedatabaseme.de wird über Hetzner verwaltet.

Standardmäßig kann cert-manager mit einer Reihe an DNS Anbietern sprechen (hier findet sich eine Liste), leider nicht Hetzner. Dafür gibt es jedoch eine Webhook Erweiterung, die uns das ermöglicht.

Folgende Voraussetzungen müssen erfüllt sein, damit wir diesen Webhook installieren können:

  • cert-manager muss installiert sein (Anleitung). Am Besten in einem eigenen Namespace (bei mir cert-manager)
  • DNS Zone muss über Hetzner DNS Konsole verwaltet sein (Nicht konsoleH). Der DNS Dienst, den Hetzner mit einer Webseite anbietet, reicht hier nicht aus. Eine Umstellung der DNS Server kann per Ticket an Hetzner kostenlos erfolgen.
  • Ein API Key in der DNS Konsole ist erstellt. (Im Dashboard der Hetzner DNS Konsole)
  • Helm 3 muss installiert sein

Achtung: Solltet ihr z.B. ein Webseiten Projekt bei Hetzner hosten und dieses mit einem Let’s Encrypt Wildcard Zertifikat (*domain.de) versehen haben, wird eine automatische Verlängerung des Zertifikats durch Hetzner nicht mehr möglich sein. Der Grund dafür ist, dass ein Wildcard Zertifikat nur über die mehrfach genannte DNS Challenge ausstellbar ist. Da die DNS Zone jetzt nicht mehr in eurem Webprojekt verwaltet wird, kann der benötigte TXT Record nicht mehr automatisch erstellt werden. Ihr müsst nun also entweder den TXT Record manuell erstellen (und das jedesmal, wenn ein Zertifikat ausgestellt werden soll), oder ihr wechselt auf ein Zertifikat je (Sub)-Domain. Damit habt ihr zwar mehr Zertifikate zu verwalten, es funktioniert aber automatisch.

Webhook für Hetzner installieren

Beginnen wir, indem wir das Git Repository clonen:

git clone https://github.com/mecodia/cert-manager-webhook-hetzner.git
cd cert-manager-webhook-hetzner

Solltet ihr den cert-manager in einem gleichnamigen Namespace installiert haben, müsst ihr keine Anpassungen bei der Installation des Webhooks vornehmen. Anderenfalls müsst ihr den korrekten Namespace in der values.yaml angeben. Diese könnt ihr unter charts/cert-manager-webhook-hetzner finden. In der gleichen Datei müsst ihr den Parameter groupName anpassen. Der groupName kann beliebig gewählt werden. Ich habe meinen Domain Namen eingetragen.

# The GroupName here is used to identify your company or business unit that
# created this webhook.
# For hetzner, this may be "acme.mycompany.com".
# This name will need to be referenced in each Issuer's `webhook` stanza to
# inform cert-manager of where to send ChallengePayload resources in order to
# solve the DNS01 challenge.
# This group name should be **unique**, hence using your own company's domain
# here is recommended.
groupName: <MYDOMAIN>.de

Anschließend installieren wir den Webhook.

helm install --namespace kube-system cert-manager-webhook-hetzner ./charts/cert-manager-webhook-hetzner

Nun konfigurieren wir uns zwei sogenannte ClusterIssuer. Einen für die Let’s Encrypt Staging Umgebung und einen für die Let’s Encrypt Production Umgebung. Da Let’s Encrypt recht strikte “Rate Limits” vorgibt (also wie oft in welchem Abstand ein Zertifikat beantragt werden kann), empfiehlt es sich immer, zunächst ein Zertifikat über den Staging Issuer ausstellen zu lassen. Erst wenn dort alles funktioniert, können wir auf den Production Issuer wechseln. Dazu später mehr.

---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
  namespace: cert-manager
spec:
  acme:
    email: <EMAIL@ADDRESS>
    privateKeySecretRef:
      name: letsencrypt-production
    server: https://acme-v02.api.letsencrypt.org/directory
    solvers:
    ### Use this for Hetzner DNS Challenge
    - dns01:
        webhook:
          groupName: <GROUPNAME_DEFINED_DURING_INSTALLATION>
          solverName: hetzner
          config:
            APIKey: <HETZNER_API_KEY_UNENCODED>
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
  namespace: cert-manager
spec:
  acme:
    email: <EMAIL@ADDRESS>
    privateKeySecretRef:
      name: letsencrypt-staging
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    solvers:
    ### Use this for Hetzner DNS Challenge
    - dns01:
        webhook:
          groupName: <GROUPNAME_DEFINED_DURING_INSTALLATION>
          solverName: hetzner
          config:
            APIKey: <HETZNER_API_KEY_UNENCODED>

Das erstellte Manifest wenden wir an:

kubectl -f clusterissuer.yaml

Wer genau hinsieht, der sieht eine unschöne Eigenschaft des Hetzner Webhooks, der API Key kann nicht auf ein Secret referenzieren in der aktuellen Version. Damit steht der API Key im Klartext in der YAML Datei. Ich möchte hier erwähnen, dass ich noch weitere Webhooks für Hetzner gefunden habe, bei denen dies nicht der Fall ist, aber diese habe ich nicht ans Laufen bekommen. Daher muss ich im Moment mit dieser Einschränkung leben.

Zertifikatsrequest erstellen

Nun können wir einen Zertifikatsrequest erstellen. Dazu erzeugen wir uns wieder ein Manifest.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: <TEST.MYDOMAIN.DE>
  namespace: cert-manager
spec:
  secretName: testrequestsecret
  issuerRef:
    name: letsencrypt-staging
    kind: ClusterIssuer
  commonName: '<TEST.MYDOMAIN.DE>'
  dnsNames:
  - <TEST.MYDOMAIN.DE>

Das Manifest wenden wir ebenfalls an:

kubectl apply -f testcertificate.yaml

Nun können wir den Status unseres Requests verfolgen, indem wir uns die erstellte Certificate und CertificateRequest Ressource anschauen. Auch sollten wir in den Logs des cert-manager Pods nützliche Informationen finden.

Ist der Zertifikatsrequest erfolgreich abgeschlossen, wird er als “READY” dargestellt.

kubectl get certificaterequests.cert-manager.io -n cert-manager
NAME                                             APPROVED   DENIED   READY   ISSUER                                  REQUESTOR                                         AGE
cert-manager-webhook-hetzner-ca-lk5ns            True                True    cert-manager-webhook-hetzner-selfsign   system:serviceaccount:cert-manager:cert-manager   5h16m
cert-manager-webhook-hetzner-webhook-tls-d5vcf   True                True    cert-manager-webhook-hetzner-ca         system:serviceaccount:cert-manager:cert-manager   5h16m
test.mydomain.de-h8f2h                           True                True    letsencrypt-staging                     system:serviceaccount:cert-manager:cert-manager   5m15s

kubectl get certificate -n cert-manager
NAMESPACE      NAME                                       READY   SECRET                                     AGE
cert-manager   test.mydomain.de                           True    testthedatabasemede                        112s

kubectl logs -n cert-manager cert-manager-cdc85d4c4-kkxcj
...
cert-manager/controller/certificates-trigger "msg"="Certificate must be re-issued" "key"="cert-manager/test.homelab.thedatabaseme.de" "message"="Issuing certificate as Secret does not exist" "reason"="DoesNotExist"
Setting lastTransitionTime for Certificate "test.mydomain.de" condition "Issuing"
Setting lastTransitionTime for CertificateRequest "test.mydomain.de-h8f2h" condition "Approved"
Setting lastTransitionTime for CertificateRequest "test.mydomain.de-h8f2h" condition "Ready"
...

Zertifikat für einen Ingress erzeugen

Wie oben erwähnt, können wir einen Zertifikatsrequest implizit erzeugen, indem wir die entsprechende Annotation in der Ingress Regel hinterlegen. In meinem Fall habe ich einen NGINX Ingress-Controller installiert. Eine Anleitung dazu findet ihr hier. Nun erzeugen wir uns ein Deployment und einen Service dazu.

kubectl create deployment demo --image=httpd --port=80
kubectl expose deployment demo

Die Ingress Definition passen wir folgendermaßen an:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: demo
  annotations:
    kubernetes.io/ingress.class: "nginx"    
    cert-manager.io/cluster-issuer: "letsencrypt-staging"
    ### Uncomment for production usage
#    cert-manager.io/cluster-issuer: "letsencrypt-production"
spec:
  tls:
  - hosts:
    - demo.<MYDOMAIN.DE>
    secretName: demo.<MYDOMAIN.DE>
  rules:
  - host: demo.<MYDOMAIN.DE>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demo
            port:
              number: 80

Nun wenden wir das Ganze an und nach ein paar Minuten sollten wir ein Zertifikat bekommen haben. Nun tragen wir den von uns gewünschten DNS Namen noch in unseren DNS Server ein und rufen die Seite im Browser auf. Wenn wir den Let’s Encrypt Production Issuer verwendet haben, sollten wir folgendes in den Sicherheitsangaben unserer Seite sehen.

Philip

Leave a Reply

Your email address will not be published. Required fields are marked *