Automatisierte DNS- und SSL-Zertifikatsverwaltung in Kubernetes

Hallo zusammen! Nachdem wir in den vorherigen Posts über die Installation von k3s und die sichere Verwaltung von Secrets gesprochen haben, widmen wir uns heute der Automatisierung der DNS- und Zertifikatsverwaltung in unserem Heim-Kubernetes-Cluster. Mit Hilfe von ExternalDNS, Cert-Manager und Let’s Encrypt, alles gesteuert durch GitOps und FluxCD, schaffen wir eine robuste und automatisierte Infrastruktur für unsere Dienste.

Graffiti an einer Hauswand mit dem Schriftzug DNS in großen Buchstaben
DNS. Von universaldilettant, CC BY-NC-SA 2.0

Warum ExternalDNS, Cert-Manager und Let’s Encrypt?

ExternalDNS synchronisiert automatisch entdeckte DNS-Einträge mit einer Vielzahl von DNS-Providern, basierend auf den Ressourcen in unserem Kubernetes-Cluster. Dies erleichtert die DNS-Verwaltung erheblich, indem es sicherstellt, dass DNS-Einträge immer aktuell sind, ohne dass manuelle Eingriffe nötig sind.

Cert-Manager automatisiert das Management und die Erneuerung von SSL-Zertifikaten innerhalb von Kubernetes. Es nutzt Let’s Encrypt, um kostenlose und vertrauenswürdige Zertifikate bereitzustellen, die automatisch erneuert werden, um die Sicherheit zu gewährleisten.

Let’s Encrypt ist ein kostenloser, automatisierter und offener Zertifikataussteller, der es uns ermöglicht, SSL-Zertifikate zu erhalten, die von den meisten Browsern als vertrauenswürdig angesehen werden, ohne Kosten oder manuelle Prozesse.

Installation und Konfiguration

Hinweis: Die Anlage von Namespaces, Helmrepositories und Kustomizations habe ich in den vorherigen Beiträgen gezeigt. Diese Komponenten sehen sich immer sehr ähnlich. Um die Beiträge hier kürzer zu halten, werde ich die Definitionen hier nicht zeigen.

ExternalDNS

  1. Einrichten von ExternalDNS: Zuerst fügen wir die ExternalDNS-Konfiguration zu unserem Git-Repository hinzu.Dieses HelmRelease stellt ExternalDNS so ein, dass es mit dem spezifizierten DNS-Provider netcup interagiert und DNS-Einträge für die Domain example.com verwaltet. Die Einträge werden als TXT-Records selbst im DNS verwaltet (die sogenannte Registry). Um Konflikte bei cnames zu vermeiden, muss ein Prefix für die Registry-Records verwendet werden.
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: external-dns
  namespace: external-dns
spec:
  chart:
    spec:
      chart: external-dns
      version: 7.3.2
      sourceRef:
        kind: HelmRepository
        name: bitnami
        namespace: default 
  interval: 15m
  timeout: 5m
  releaseName: external-dns  
  values: # paste icontents of upstream values.yaml below, indented 4 spaces
    image:
      registry: docker.io
      repository: bitnami/external-dns
      #tag: 0.14.1-debian-12-r2
      digest: ""
      pullSecrets: []
    revisionHistoryLimit: 10
    sources:
      - crd
      # - service
      - ingress
      # - contour-httpproxy
    provider: webhook 
    initContainers: []
    
    dnsConfig: {}
    sidecars:
      - name: netcup-webhook
        image: ghcr.io/mrueg/external-dns-netcup-webhook:latest
        imagePullPolicy: Always
        args:
        - --log-level=debug
        - --domain-filter=example.com
        - --netcup-customer-id=<customer id>
        env:
        - name: NETCUP_API_KEY
          valueFrom:
            secretKeyRef:
              key: NETCUP_API_KEY
              name: netcup-credentials
        - name: NETCUP_API_PASSWORD
          valueFrom:
            secretKeyRef:
              key: NETCUP_API_PASSWORD
              name: netcup-credentials

    ## @param registry Registry method to use (options: txt, aws-sd, dynamodb, noop)
    ## ref: https://github.com/kubernetes-sigs/external-dns/blob/master/docs/proposal/registry.md
    ##
    registry: "txt"
    ## @param txtPrefix When using the TXT registry, a prefix for ownership records that avoids collision with CNAME entries (optional)<CNAME record> (Mutual exclusive with txt-suffix)
    ##
    txtPrefix: "k3s."
    ## @param txtSuffix When using the TXT registry, a suffix for ownership records that avoids collision with CNAME entries (optional)<CNAME record>.suffix (Mutual exclusive with txt-prefix)
    ##
...Code-Sprache: HTML, XML (xml)

Cert-Manager

  1. Installation von Cert-Manager mit FluxCD: Füge die die Cert-Manager Konfiguration zu deinem Repository hinzu. Dies installiert Cert-Manager im Cluster und konfiguriert ihn zur Verwaltung und Erneuerung von SSL-Zertifikaten:
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: cert-manager
  namespace: cert-manager
spec:
  chart:
    spec:
      chart: cert-manager
      version: v1.14.x
      sourceRef:
        kind: HelmRepository
        name: jetstack
        namespace: default
  interval: 15m
  timeout: 5m
  releaseName: cert-manager
  values:
    # füge die Inhalte der default values.yaml hier ein.
    # installCRDs sollte auf true gesetzt werden, um die 
    # Custom Resource Definitions auf diesem Weg zu installieren

Let’s Encrypt ClusterIssuer

  1. Konfigurieren eines Let’s Encrypt ClusterIssuer: Dieser ClusterIssuer wird verwendet, um Zertifikate von Let’s Encrypt anzufordern und automatisch zu erneuern. Um angeforderte Zertifikate verifizieren zu können, gibt es verschiedene Methoden. Die bekanntesten sind die http-01-challenge und die dns-01-challenge. Ich habe mich hier für die dns-01-challenge entschieden. Das erspart mir, Port 80 in meinem Heimetz freigeben zu müssen. Voraussetzung für die Challenge ist, dass es einen Provider für den DNS-Anbieter gibt. Ich nutze netcup, wofür es einen Webhook gibt.

HelmRelease für den Webhook:

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: cert-manager-webhook-netcup
  namespace: cert-manager 
spec:
  chart:
    spec:
      chart: cert-manager-webhook-netcup
      version: 1.0.x
      sourceRef:
        kind: HelmRepository
        name: cert-manager-webhook-netcup
        namespace: default 
  interval: 15m
  timeout: 5m
  releaseName: cert-manager-webhook-netcup  
  values: # paste icontents of upstream values.yaml below, indented 4 spaces
    # The GroupName here is used to identify your company or business unit that
    # created this webhook.
    # For example, 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: com.netcup.webhook
    
    certManager:
      namespace: cert-manager
      serviceAccountName: cert-manager
    
    image:
      repository: ghcr.io/aellwein/cert-manager-webhook-netcup
      # set version here for upcoming release
      # tag: 1.0.22
      # sha hash can be used to specify image version, instead of tag
      hash: ""
      pullPolicy: IfNotPresent
    
    nameOverride: ""
    fullnameOverride: ""
    
    service:
      type: ClusterIP
      port: 443
    
    resources:
      {}
    nodeSelector: {}
    tolerations: []
    affinity: {}

Der Clusterissuer sorgt dafür, dass Zertifikate clusterweit ausgestellt werden können.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: <mail adress>
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - selector:
        dnsZones:
          - "<your dns zone>"
      dns01:
        webhook:
            groupName: com.netcup.webhook
            solverName: netcup
            config:
                secretRef: <name of the secret containing your provider credentials>
                secretNamespace: cert-managerCode-Sprache: HTML, XML (xml)

Nun müssen wir noch ein Zertifikat erzeugen lassen. Bis das Zertifikat einsatzbereit ist kann es erfahrungsgemäß bis zu einer Stunde dauern. In diesem Beispiel wird ein Wildcardzertifikat angelegt. Die reflector-Annotationen sollen dafür sorgen, dass das erzeugte Zertifikat in andere Namespaces kopiert werden kann. Dazu später mehr.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: letsencrypt-wildcard-cert-prod
  namespace: letsencrypt-wildcard-cert
spec:
  # secretName doesn't have to match the certificate name, but it may as well, for simplicity!
  secretName: letsencrypt-wildcard-cert-prod
  secretTemplate:
    annotations:
      reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
      reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - "my.example.com"
    - "*.my.example.com"

Fortgeschrittene DNS- und Zertifikatsstrategien

Nutzung von Wildcard-Domains

Die Verwendung von Wildcard-Domains ist eine effektive Strategie, um den Bedarf an ständig neuen DNS-Einträgen zu minimieren. Durch die Konfiguration eines Wildcard-Zertifikats für eine Hauptdomain (z.B. *.example.com) können viele Subdomains ohne zusätzliche DNS-Konfigurationen abgedeckt werden. Dies vereinfacht die Verwaltung erheblich, da nicht für jede neue Subdomain ein separater DNS-Eintrag erforderlich ist.

Dynamische DNS-Einträge mit ExternalDNS

Für Szenarien, in denen spezifische oder zahlreiche Subdomains benötigt werden, bietet ExternalDNS die Möglichkeit, DNS-Einträge dynamisch zu erzeugen. Dies kann auf zwei Arten geschehen:

  1. CRD-Objekte: Indem Custom Resource Definitions (CRDs) verwendet werden, die speziell für DNS-Einträge in ExternalDNS definiert sind.
  2. Annotations in Ingress-Objekten: Durch das Hinzufügen spezifischer Annotations wie external-dns.alpha.kubernetes.io/hostname und external-dns.alpha.kubernetes.io/target zu Ingress-Objekten. Dies ermöglicht es ExternalDNS, entsprechende DNS-Einträge automatisch zu erstellen und zu aktualisieren, basierend auf den Ingress-Konfigurationen.

Verwaltung von Let’s Encrypt Zertifikaten über Namespaces

Die Zertifikate von Let’s Encrypt, einschließlich der Wildcard-Zertifikate, müssen im richtigen Kubernetes Namespace platziert werden, um von den entsprechenden Services genutzt werden zu können. Da das Wildcard-Zertifikat typischerweise nur in einem Namespace vorhanden ist, nutze ich Kubernetes Reflector zur Synchronisation:

Kubernetes Reflector ist ein Tool, das Secrets und ConfigMaps zwischen verschiedenen Namespaces repliziert. Dies ist besonders nützlich, um das Wildcard-Zertifikat über mehrere Namespaces hinweg verfügbar zu machen, ohne es manuell in jedem Namespace neu erstellen zu müssen. Hier ein Beispiel, wie ein solches Setup konfiguriert sein könnte:

<code>apiVersion: v1
kind: Secret
metadata:
  name: wildcard-cert
  annotations:
    reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
    reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
data:
  tls.crt: <base64-encoded-cert>
  tls.key: <base64-encoded-key>
</code>
Code-Sprache: JavaScript (javascript)

Fazit

Mit der erfolgreichen Integration von ExternalDNS und Cert-Manager in unser GitOps-Workflow und der Nutzung von Let’s Encrypt über DNS-Challenges haben wir eine vollautomatische Lösung für das Management von DNS und SSL-Zertifikaten geschaffen. Durch die Verwendung von Wildcard-Zertifikaten und die sorgfältige Planung der Zertifikatserneuerungen minimieren wir die Anzahl der notwendigen Anfragen und vereinfachen die Verwaltung. Dies stellt sicher, dass unsere Services sicher, erreichbar und gut geschützt sind.

Weitere Blogposts aus der k3s-Reihe

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert