Caddy – Konzepte und Konfiguration

Aus Tuxipedia

Caddy – Konzepte und Konfiguration

Was macht ein Webserver?

Ein Webserver wartet auf eingehende Netzwerkanfragen (HTTP/HTTPS) und beantwortet sie. Das kann bedeuten:

  • Eine Datei ausliefern (HTML, Bild, CSS)
  • Eine Anfrage an einen anderen Dienst weiterzuleiten (Reverse Proxy)
  • Eine Anfrage umzuleiten (Redirect)
  • Eine Anfrage abzulehnen (403, 404)

Klassische Webserver wie Apache und nginx können all das – benötigen aber für HTTPS externe Werkzeuge (z.B. Certbot) und manuelle Zertifikatsverwaltung.

Caddy übernimmt die Zertifikatsverwaltung automatisch: Er bezieht TLS-Zertifikate von Let's Encrypt, erneuert sie selbstständig, und erzwingt HTTPS ohne manuellen Eingriff.

Das Caddyfile

Die Konfiguration von Caddy erfolgt in einer einzigen Textdatei: dem Caddyfile. Die Syntax ist bewusst einfach gehalten.

Grundstruktur:


example.com {
    # Direktiven für diese Domain
}

Alles innerhalb der geschweiften Klammern gilt nur für diese Domain. Mehrere Domains können in derselben Datei konfiguriert werden.


Direktiven

reverse_proxy

Kurzfassung: Leitet eingehende Anfragen an einen anderen Dienst weiter.

reverse_proxy localhost:8080 reverse_proxy myapp:3000

Erklärung:

Ein Reverse Proxy sitzt zwischen dem Browser des Nutzers und dem eigentlichen Dienst. Der Browser spricht immer mit Caddy – nie direkt mit dem Dienst dahinter. Caddy nimmt die Anfrage entgegen, leitet sie weiter, bekommt die Antwort, und schickt sie zurück an den Browser.

Vorteile:

  • Nur Caddy ist nach außen sichtbar – interne Dienste bleiben verborgen
  • TLS wird einmalig bei Caddy terminiert
  • Mehrere Dienste können hinter einer einzigen IP laufen

Bei Docker-Containern kann der Containername als Hostname verwendet werden:

reverse_proxy nextcloud:443

Caddy löst nextcloud über das interne Docker-Netz auf.


transport http

Kurzfassung: Steuert wie Caddy intern mit dem Zieldienst kommuniziert.

reverse_proxy nextcloud:443 {
    transport http {
        tls_insecure_skip_verify
    }
}

Erklärung:

Standardmäßig kommuniziert Caddy mit Zieldiensten über HTTP. Spricht der Zieldienst selbst HTTPS (z.B. ein Container der intern TLS verwendet), muss Caddy das wissen – und bekommt den transport http-Block.

tls_insecure_skip_verify deaktiviert die Zertifikatsprüfung für die interne Verbindung. Das klingt unsicher – ist es intern aber nicht, weil:

  • Die Verbindung innerhalb des Docker-Netzes stattfindet
  • Kein externer Angreifer Zugriff auf diesen internen Kanal hat
  • Das Zertifikat des Zieldienstes selbstsigniert sein darf

Nach außen (Browser → Caddy) bleibt HTTPS mit gültigem Zertifikat bestehen.


Kurzfassung: Setzt, entfernt oder verändert HTTP-Response-Header – also Header die Caddy an den Browser zurückschickt.

header {
    -Server
    Strict-Transport-Security "max-age=31536000"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "SAMEORIGIN"
    Referrer-Policy "no-referrer"
}

Erklärung:

HTTP-Header sind Metadaten die jede Antwort begleiten. Sie sagen dem Browser wie er die Antwort behandeln soll.

  • -Server – Das Minuszeichen entfernt den Header. Ohne diese Zeile würde Caddy Server: Caddy mitsenden – ein Hinweis für Angreifer welche Software läuft. Konkret: Caddy würde normalerweise mitteilen: "Hallo, ich bin Caddy v2.10.2". Der Minus löscht diesen Header. Der Besucher sieht nichts.
  • Strict-Transport-Security – Sagt dem Browser: „Verbinde dich für die nächsten 31536000 Sekunden (1 Jahr) ausschließlich über HTTPS mit dieser Domain." Konkret: Sagt dem Browser: "Diese Seite gibt es nur per HTTPS, für die nächsten 365 Tage – frag gar nicht erst per HTTP an."
  • X-Content-Type-Options "nosniff" – Verhindert dass der Browser den Dateityp einer Antwort „errät". Schutz gegen bestimmte Angriffe über präparierte Dateien. Konkret: Klassischer Angriffsvektor: Server schickt eine HTML-Datei als text/plain, Browser erkennt sie trotzdem als HTML und führt JavaScript aus.
  • X-Frame-Options "SAMEORIGIN" – Verhindert dass die Seite in einem <iframe> einer fremden Domain eingebettet wird. Schutz gegen Clickjacking. Konkret: Jemand baut deine Seite unsichtbar in seine ein und du klickst unwissentlich auf deren Buttons.
  • Referrer-Policy "no-referrer" – Der Browser sendet beim Klick auf einen Link keine Information woher der Nutzer kam. Konkret: Die externe Seite erfährt nicht, woher du kommst.

Diese Header bilden gemeinsam eine erste Verteidigungslinie: Jeder schützt gegen eine andere Angriffsart, jeder unabhängig vom anderen. Das Prinzip dahinter heißt Defense in Depth – Sicherheit durch mehrere unabhängige Schichten. Fällt eine aus, stehen die anderen noch. 🏰



header_up

Kurzfassung: Setzt Header die Caddy an den Zieldienst (upstream) weiterleitet – nicht an den Browser.

reverse_proxy myapp:8080 {
    header_up X-Real-IP {remote_ip}
}

Erklärung:

Ohne zusätzliche Header sieht ein Dienst hinter Caddy als Absender immer Caddys interne IP – nie die echte Client-IP. Mit header_up X-Real-IP {remote_ip} reicht Caddy die echte IP des Clients weiter.

{remote_ip} ist eine Caddy-Variable die zur Laufzeit durch die tatsächliche IP ersetzt wird.

Caddy setzt X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host automatisch – diese müssen nicht manuell gesetzt werden.


@matcher (Named Matchers)

Kurzfassung: Definiert eine benannte Bedingung die auf Anfragen zutreffen kann oder nicht.

@xmlrpc path /xmlrpc.php
respond @xmlrpc 403

@admin {
    method POST
    path /admin/*
}
handle @admin {
    reverse_proxy backend:8080
}

Erklärung:

Matcher sind das Herzstück von Caddy-Konfigurationen. Sie ermöglichen es, Regeln nur auf bestimmte Anfragen anzuwenden.

Das @-Zeichen leitet einen Matcher ein. Der Name danach ist frei wählbar. Matcher können prüfen:

  • path – welcher Pfad wird angefragt?
  • method – GET, POST, PUT, DELETE...?
  • query – enthält die URL bestimmte Parameter?
  • remote_ip – kommt die Anfrage von einer bestimmten IP?
  • header – enthält die Anfrage bestimmte Header?

Mehrere Bedingungen in einem Block werden mit UND verknüpft – alle müssen zutreffen.


handle

Kurzfassung: Führt Direktiven nur dann aus wenn ein Matcher zutrifft. Nicht-passende Anfragen fallen durch zum nächsten Block.

@static path /static/*
handle @static {
    file_server
}

handle {
    reverse_proxy backend:8080
}

Erklärung:

handle ohne Matcher ist der Standardfall – er greift für alles was kein anderer Block behandelt hat. In Kombination mit Matchern entsteht eine Art Weiche: statische Dateien werden direkt ausgeliefert, alles andere geht an den Backend-Dienst.


route

Kurzfassung: Wie handle, aber die Reihenfolge der Direktiven wird strikt eingehalten.

Erklärung:

Caddy verarbeitet Direktiven normalerweise in einer intern festgelegten Reihenfolge – unabhängig davon wie sie im Caddyfile stehen. route erzwingt die Reihenfolge wie sie im Caddyfile steht. Nützlich wenn die Reihenfolge der Verarbeitung wichtig ist.


redir

Kurzfassung: Leitet den Browser auf eine andere URL um.

redir https://new.example.com{uri} 301
redir https://new.example.com{uri} 308

Erklärung:

Der Unterschied zwischen 301 und 308:

Code Name Bedeutung
301 Moved Permanently Browser darf die HTTP-Methode ändern (POST → GET)
308 Permanent Redirect Browser behält die HTTP-Methode bei (POST bleibt POST)

Für einfache Browser-Weiterleitungen ist 301 der Klassiker. Für API-Weiterleitungen oder Formulare ist 308 wichtig.

{uri} ist eine Caddy-Variable für den vollständigen Pfad inkl. Query-String – so bleibt der ursprüngliche Pfad nach der Weiterleitung erhalten.


respond

Kurzfassung: Antwortet direkt mit einem HTTP-Statuscode – ohne den Zieldienst zu kontaktieren.

@xmlrpc path /xmlrpc.php
respond @xmlrpc 403

respond 404

Erklärung:

Nützlich um bestimmte Pfade hart zu blockieren ohne dass die Anfrage den eigentlichen Dienst erreicht. Caddy antwortet sofort – schnell und ressourcenschonend.

Gängige Statuscodes:

Code Bedeutung
200 OK
301/308 Weiterleitung (permanent)
403 Forbidden – Zugriff verweigert
404 Not Found – nicht vorhanden
500 Internal Server Error

file_server

Kurzfassung: Liefert statische Dateien aus einem Verzeichnis aus.

root * /var/www/html
file_server

Erklärung:

root legt das Basisverzeichnis fest. file_server aktiviert die Auslieferung. Caddy sucht dann für jeden Request die entsprechende Datei im Verzeichnis und liefert sie aus – wie ein klassischer Webserver.


tls

Kurzfassung: Steuert TLS-Einstellungen für eine Domain.

tls {
    alpn http/1.1
}

Erklärung:

Caddy verwaltet TLS-Zertifikate automatisch. Der tls-Block ist nur nötig wenn vom Standard abgewichen werden soll.

alpn (Application-Layer Protocol Negotiation) legt fest welche HTTP-Version ausgehandelt wird. Standardmäßig bietet Caddy HTTP/2 an. Mit alpn http/1.1 wird HTTP/2 deaktiviert – nützlich wenn ein Zieldienst damit nicht zurechtkommt.


Globale Optionen

Am Anfang des Caddyfiles können globale Einstellungen gesetzt werden:

{
    admin 127.0.0.1:2019
}

admin aktiviert die Caddy-Admin-API auf dem angegebenen Port. Mit 127.0.0.1:2019 ist sie nur lokal erreichbar – nicht von außen. Über diese API kann Caddy zur Laufzeit neu geladen werden:

<syntaxhighlight lang="bash"> caddy reload --config /etc/caddy/Caddyfile


Abschluss-"Meditation"

Auch, wenn's in diesem Beitrag "nur" um Caddy selbst geht, ist der Webserver dennoch mit allen anderen Schichten verbunden. Die Komplexität eines Webservers kommt daher dass jede dieser Schichten eine eigene Sprache spricht:

Docker denkt in Containern und Netzwerken Caddy denkt in Requests und Routen Collabora denkt in WOPI und Aliases Der Browser denkt in Headers und Policies

Die sprechen alle miteinander – aber keiner erklärt dem anderen was er tut. Die Übersetzung geschieht, bei einer Realisation über Docker-Container, im Zusammenspiel von Kombination aus docker-compose.yml und Caddyfile. Das ist das Schwierige.

Die beiden müssen zusammenpassen:

Compose definiert wer existiert und wie die Container heißen Caddy nutzt genau diese Namen als interne Adressen.

Beispiel (yml):

container_name: myAwesomeContainer


Im Caddyfile spricht Caddy myAwesomeContainer mit diesem Namen an:

reverse_proxy myAwesomeContainer:12345 (-> 12345 = Portnummer, unter der der Container erreichbar ist)


Docker's internes DNS löst myAwesomeContainer zur richtigen IP auf – automatisch. Das ist die unsichtbare Brücke zwischen beiden Files. Und das gemeinsame Docker-Netzwerk ist der Raum in dem alle miteinander reden dürfen. Wer nicht drin ist, hört nichts. 🏰

Siehe auch