Caddy

Caddy ist ein Reverseproxy, der besonders wegen seiner Einfachheit beliebt ist. Die Konfiguration erfolgt vollständig über eine Datei, das Caddyfile. Man kann Caddy per Docker oder nativ als Binary auf dem Host installieren. Das Erstere erleichtert die Einbindung von Plugins. Da ich es außerdem auf einem Server nutzen möchte, auf dem bereits Docker läuft, betreibe ich es unter Docker. Ich nutze Caddy, um die Weblösungen, die ich in meinem lokalen Homelab betreibe, mit Domains und Let's Encrypt Zertifikaten zu verknüpfen. Ich nutze Domains, die bei den Providern Netcup und All-inkl gehostet werden. Hier dokumentiere ich, wie ich Domains bei diesen beiden Providern für mein lokales Netz nutze. Ich brauche dazu Caddy und einen DNS-Server (in meinem Fall dnsmasq).



Installation

Ich bin auf meinem Server per SSH angemeldet und besitze auch die Sudo-Rechte (siehe hierzu: server-absichern.html). Zunächst ist eine Dockerumgebung zu installieren. Dazu gibt es verschiedene Methoden. Meine habe ich hier: Docker beschrieben. Dann erstelle ich ein Verzeichnis namens "caddy" und wechsele in selbiges. Hier erstelle ich zunächst mit meinem Lieblings-Editor nano eine Datei namens docker-compose.yml mit folgendem Inhalt:


services:
  caddy:
    build:
      context: .
      dockerfile_inline: |
        FROM caddy:builder AS builder
        # Caddy mit dem All-Inkl Plugin
        RUN xcaddy build \
            --with github.com/caddy-dns/netcup \
            --with github.com/caddy-dns/all-inkl
        FROM caddy:latest
        COPY --from=builder /usr/bin/caddy /usr/bin/caddy

    container_name: caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp" # Wichtig für HTTP/3 Support
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./data:/data
      - ./config:/config
    environment:
      - NETCUP_CUSTOMER_NUMBER="******"  # Netcup-Kundennummer
      - NETCUP_API_KEY="Zn************************ND"
      - NETCUP_API_PASSWORD="Zk5*********************************dlRK"

      - KAS_USERNAME=********
      - KAS_PASSWORD=********
            
Hier zeigt sich eine besondere Eigenschaft und Stärke von Docker. Ich kann Plugins in eine Lösung integrieren, indem ich den dafür vorgesehenen Build-Prozess von Docker nutze. Hier habe ich zwei Plugins, nämlich für die Nutzung der APIs von Netcup und All-Inkl eingebunden. Weitere Plugins findet man auf der entsprechenden Github-Seite. Die Portdefinitionen sind selbsterklärend und die Volumes lege ich an, damit ich persistente Daten habe, die nach einem Neustart der Container und insbesondere einem Rebuild erhalten bleiben. Sollte ich also künftig weitere Provider einbinden wollen, dann muss ich nicht befürchten, dass alle bisher generierten Daten weg sind.

In der Environment-Sektion definiere ich natürlich die Credentials für meine Provider. Bei Netcup ist das die Kundennummer, der API-Key und das API-Passwort. Letztere kann ich im Customer Control Panel von Netcup generieren. Für All-Inkl. brauche ich die Zugangsdaten für das KAS (keine Ahnung, wofür die Abkürzung steht).

Wenn ich nun den Container mit

docker compose up -d --build
starte, dann wird ein neues Binary kompiliert, das die Plugins für Netcup und All-Inkl. enthält.



Definition der Proxy Hosts

Die Proxy Hosts werden in Caddy im Caddyfile definiert. Wenn ich Caddy direkt auf dem Host installiert hätte, dann läge das Caddyfile unter /etc/caddy. In meiner Docker-Umgebung liegt es im gleichen Verzeichnis, wie die docker-compose.yml. Das Caddyfile heißt schlicht "Caddyfile" und hat folgenden exemplarischen Inhalt:


{
    # Globale Einstellungen
    email mail@sechzig-veedel.de
}

# --- ALL-INKL DOMAIN ---
*.korte-kernke.koeln, korte-kernke.koeln {
    tls {
        issuer acme {
            dns allinkl {
                kas_username {$KAS_USERNAME}
                kas_password {$KAS_PASSWORD}
            }
            propagation_delay 120s
            propagation_timeout -1
        }
    }

    # Proxmox VE
    @pve00 host pve00.korte-kernke.koeln
    handle @pve00 {
        reverse_proxy https://192.168.1.140:8006 {
            transport http {
                tls_insecure_skip_verify
            }
        }
    }

    # Interner Webserver
    @intranet host intranet.korte-kernke.koeln
    handle @intranet {
        reverse_proxy 192.168.1.40:80
    }

    # Fallback innerhalb dieses Blocks: Alles andere abweisen
    handle {
        abort
    }
}

# --- NETCUP DOMAIN ---
*.korte-koeln.de, korte-koeln.de {
    tls {
        issuer acme {
            dns netcup {
                customer_number {$NETCUP_CUSTOMER_NUMBER}
                api_key {$NETCUP_API_KEY}
                api_password {$NETCUP_API_PASSWORD}
            }
            propagation_delay 30s
            resolvers 1.1.1.1
        }
    }

    # Loki Logging
    @loki host loki.korte-koeln.de
    handle @loki {
        reverse_proxy 192.168.1.60:3002 {
            header_up Host {host}
            header_up X-Real-IP {remote_host}
            header_up Connection {>Connection}
            header_up Upgrade {>Upgrade}
        }
    }

    # Fallback innerhalb dieses Blocks
    handle {
        abort
    }
}
            
Ich habe die Domain korte-kernke.koeln bei All-inkl und die Domain korte-kernke.de bei Netcup gehostet. Mit den Eintragungen in der Sektion tls definiere ich die notwendigen Parameter, um für diese Domains ein gültiges Let's Encrypt-Zertifikat zu bekommen. Dafür brauche ich natürlich Domains, die im Internet auflösbar sind. Darunter definiere ich die Proxy Hosts. Die einfachste Variante ist:

    # Interner Webserver
    @intranet host intranet.korte-kernke.koeln
    handle @intranet {
        reverse_proxy 192.168.1.40:80
    }
            
Dabei definiere ich einfach den Namen und die Subdomain sowie das Ziel zu dem Caddy dann die Datenströme durchreicht. Für Systeme, die intern über ein selbstsigniertes Zertifikat angesprochen werden (z.B. Proxmox Virtual Environments), muss der Eintrag wie folgt aussehen:

    # Proxmox VE
    @pve00 host pve00.korte-kernke.koeln
    handle @pve00 {
        reverse_proxy https://192.168.1.140:8006 {
            transport http {
                tls_insecure_skip_verify
            }
        }
    }
          
Hier wird das System per HTTPS angesprochen (Standard wäre HTTP) und Caddy wird angewiesen, das selbstsignierte Zertifikat nicht zu bemängeln. Eine dritte Besonderheit stellen Proxy Hosts dar, für die die Durchleitung von Headerdaten beim Aufruf der Web-Oberfläche wichtig ist. Hierfür sehen die Einträge dann wie folgt aus:

    # Loki Logging
    @loki host loki.korte-koeln.de
    handle @loki {
        reverse_proxy 192.168.1.60:3002 {
            header_up Host {host}
            header_up X-Real-IP {remote_host}
            header_up Connection {>Connection}
            header_up Upgrade {>Upgrade}
        }
    }
          
Caddy ist sehr strikt, was insbesondere die Formatierung des Caddyfiles angeht. Dafür hilft es aber auch dabei, eine saubere Formatierung hinzubekommen. Ich gebe einfach

docker exec -w /etc/caddy caddy caddy fmt --overwrite
            
ein und mein Caddyfile wird aufgehübscht. Damit die Eintragungen im Caddyfile wirksam werden, gebe ich

docker exec -w /etc/caddy caddy caddy reload
            
ein. Nun werden, falls noch nicht geschehen, die Zertifikate bei Let's Encrypt bezogen. Das kann etwas dauern. Dann wartet Caddy darauf, dass es mit einer Subdomain, die im Caddyfile definiert ist, angesprochen wird. Damit das passiert, muss die Subdomain lokal auf die IP-Adresse des Servers auflösen, auf dem Caddy läuft. Dazu nutze ich einen lokalen DNS-Server. Wenn die Subdomains im lokalen Netz sauber mit der IP-Adresse des Caddyservers aufgelöst werden, dann kann ich die jeweiligen Websites per https aufrufen und sie werden mit einem gültigen Zertifikat verknüpft. Caddy kümmert sich auch selbständig um die rechtzeitige Erneuerung der Zertifikate. Ich rufe also z.B. https://loki.korte-kernke.de auf und mein Browser zeigt mir an, dass ich eine sichere Verbindung nutze.

Mein lokaler DNS löst Subdomains, die ich dort eintrage mit IP-Adressen aus meinem lokalen Netzwerk auf. Für die Domains habe ich bei den Providern einen sogenannten Wildcard A-Record eingerichtet. Wenn ein System aus dem Internet bei den öffentlich verfügbaren DNS-Servern die Adresse einer Subdomain dieser Domains nachfragt, wird die bei diesen Providern hinterlegte IP-Adresse ausgegeben. Wenn ein Client aus meinem lokalen Netz irgendeine Domain oder Subdomain aufruft, dann wird zunächst der lokale DNS-Server befragt. Wenn er einen dazu passenden Eintrag hat, dann liefert er den aus, ansonsten routet er an öffentlich verfügbare DNS-Server weiter. Wenn ich also eine Subdomain unter einer der og. Domains aufrufe, dann wird zunächst geprüft, ob sie lokal definiert ist bevor sie im WWW nachgeschlagen wird. So kann ich unter diesen Domains lokale Dienste hosten und mit einem sicheren Zertifikat verbinden, ohne sie ins Internet exponieren zu müssen. Außerdem kann ich bei Bedarf auch Dienste mit Subdomains unter diesen Domains im Internet veröffentlichen. So will ich das haben.