Was Caddy über deine Astro-Site wissen sollte
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
Die Lücke zwischen Build und Auslieferung
Du hast eine Website. Sie lädt, sie funktioniert, sie zeigt das, was sie zeigen soll. Aber weißt du, ob wiederkehrende Besucher deine Seiten sofort sehen oder bei jedem Klick alles neu herunterladen? Ob die Verbindung auf HTTP heruntergestuft werden kann? Ob deine Startseite bei Google als eine Domain oder als zwei auftaucht? Für die meisten Sites lauten die Antworten: nein, ja und ja. Diese letzte Schicht des Stacks ist die Web-Server-Konfiguration, und die meisten Unternehmen fassen sie nie an.
Astro leistet beim Build viel Arbeit. Es kompiliert Seiten zu statischem HTML, versieht jedes Asset im _astro/-Verzeichnis mit einem Content-Hash im Dateinamen und liefert Dateien, die jeder Static-File-Server ausliefern kann. Die CI-Pipeline legt noch eine Schicht drauf: Sie komprimiert alles vorab in .br-, .gz- und .zst-Sidecar-Dateien, damit der Server nie on the fly komprimieren muss.
All diese Arbeit kann die letzte Schicht in der Kette stillschweigend ungeschehen machen: die Web-Server-Konfiguration. Caddy liefert die Dateien aus, aber weiß nicht, was es da ausliefert. Es weiß nicht, dass _astro/-Dateien Content-Hashes in ihren Dateinamen tragen. Es weiß nicht, dass sich HTML bei jedem Deploy ändert, CSS aber nicht. Es weiß nicht, dass die Seite HTTPS-only läuft und kein Grund existiert, sie jemals in einem iframe einzubetten.
Für diese Wissenslücke ist die Config da. Und meine sah eine Weile so aus:
(jav_static) { header /favicon.svg Cache-Control "max-age=3600, must-revalidate" file_server { precompressed br gzip zstd } handle_errors { rewrite * /404.html file_server }}Es hat funktioniert. Dateien wurden ausgeliefert, vorkomprimierte Assets wurden aufgegriffen, die 404-Seite erschien, wenn sie sollte. Aber damit hatte es sich auch. Keine Security-Header, keine Cache-Strategie über das Favicon hinaus, kein www-Redirect. Der Browser traf Entscheidungen, die meine hätten sein sollen. Und wer einen Security-Scan oder einen Page-Speed-Test laufen ließ, sah genau, wie viel hier ungenutzt blieb.
Was bei einer statischen Site wirklich zählt
Ich bin die Caddy-Direktiven durchgegangen, die für mich infrage kamen: Headers, Caching, Kompression, Logging, TLS-Optionen, Server-Timeouts, Prometheus-Metriken. Logging, TLS, Timeouts und Metriken brauchen bei einer statischen Site dieser Größe keine eigene Entscheidung. Caddys Defaults greifen. Die interessanten Entscheidungen liegen bei Headers, Caching und Kompression. Die Frage lautete nicht, was Caddy alles kann, sondern was meine Site wirklich braucht.
Eine statische Site ohne Login, ohne Formulare, ohne Nutzerdaten und ohne Backend hat ein ganz anderes Bedrohungsmodell als eine Webanwendung. Die meisten Security-Guides sind für Letztere geschrieben. Wer sie blind auf eine statische Site anwendet, fügt Komplexität hinzu, die vor nichts schützt.
Also habe ich jede Option gegen einen einfachen Test geprüft: Adressiert das eine echte Bedrohung, oder macht es nur einen Scanner zufrieden?
Drei Security-Header, nicht sechs
Im Netz kursieren genug Listen, die dir sagen, du sollst jeden existierenden Security-Header setzen. Was hineinkam, und was nicht.
Strict-Transport-Security adressiert eine echte Bedrohung. Ohne diesen Header könnte bei einem Besucher in einem öffentlichen WLAN die Verbindung auf HTTP heruntergestuft und abgefangen werden. Die Seite läuft ohnehin nur über HTTPS, also sagt dieser Header dem Browser schlicht: versuch niemals HTTP. Ein Jahr, inklusive Subdomains, mit preload, um sich in die HSTS-Preload-Liste einzutragen, die Browser fest ausgeliefert bekommen. Der Preis ist real: jede aktuelle und zukünftige Subdomain muss HTTPS ausliefern, sonst wird sie unerreichbar. Die Entfernung aus der Preload-Liste liegt nicht in deiner Hand: sie passiert in den Release-Zyklen der Browser, nicht in deiner Config. Hier bewusst akzeptiert, weil diese Seite bewusst HTTPS-only ist und Caddy für jede Subdomain automatisch Zertifikate über Let’s Encrypt bereitstellt.
X-Content-Type-Options: nosniff verhindert, dass Browser Content-Types erraten. Ohne ihn könnte ein Browser eine Textdatei als ausführbaren Code interpretieren. Ein Wert, keine Konfiguration, keine Trade-offs.
X-Frame-Options: DENY verhindert, dass irgendjemand die Seite in einem iframe einbettet. Schützt vor Clickjacking, bei dem ein Angreifer unsichtbare UI über deine Seite legt, um Klicks abzugreifen. Für diese Seite gibt es keinen Grund, in einem iframe zu stehen, also ist DENY die richtige Antwort.
header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY"}Das war’s. Drei Header, die drei echte Angriffsvektoren adressieren. Die abgelehnten Optionen und warum:
Referrer-Policy hat in den großen Browsern bereits strict-origin-when-cross-origin als Default. Den Header explizit zu setzen ändert nichts. Externe Seiten sehen deine Domain als Referrer (gut für SEO), aber nicht den vollen Pfad. Das richtige Verhalten ist also schon da, ohne die Config anzufassen.
Permissions-Policy deaktiviert Browser-APIs wie Kamera, Mikrofon und Geolocation. Eine statische Site nutzt diese APIs aber nicht. Etwas zu blockieren, was du gar nicht einsetzt, schützt vor gar nichts. Wenn ein Angreifer JavaScript einschleusen kann, um auf die Kamera zuzugreifen, hast du ganz andere Probleme als einen fehlenden Header. Und wer YouTube-Videos mit aktiviertem Fullscreen einbettet, wie ich, müsste danach sorgfältig Ausnahmen definieren. Mehr Komplexität für null Sicherheitsgewinn.
Cross-Origin-Opener-Policy verhindert, dass andere Seiten eine Referenz auf dein Browser-Fenster bekommen. Das zählt bei Seiten mit Authentifizierungs-Flows, bei denen ein Popup Tokens abgreifen könnte. Eine statische Site hat keine Auth, keine Tokens, keine Popups.
Content-Security-Policy ist eine Runtime-Allowlist für Skripte, Styles, Fonts und Bilder. Sie hält Cross-Site-Scripting in Schach, blockiert Datenabfluss und fängt kompromittierten Third-Party-Code ab, egal wie er in die Seite gekommen ist. Für Seiten, die nicht vertrauenswürdige Daten rendern (Formulare, User-Content, externe APIs), entschärft sie eine aktive Angriffsfläche; für Seiten ohne solche Eingangspfade schützt sie vor unwahrscheinlicheren, aber schwerwiegenderen Ereignissen: ein kompromittiertes Analytics-Skript, ein manipulierter Build, ein vergiftetes CDN, eine künftige Änderung, die unbemerkt einen Eingangspfad hinzufügt. Der Preis ist real: jede Ressource muss inventarisiert werden, und jeder Fehler bricht die Seite stillschweigend. Astro 6 hat eine Integration ergänzt, die abdeckt, was Astro selbst ausgibt; iframes, externe Fonts und Third-Party-Runtime-Services bleiben manuell. CSP wird inshallah folgen, wenn das Audit in den Zeitplan passt, oder früher, falls sich das Bedrohungsmodell ändert.
Das Muster: Jeder übersprungene Header dupliziert entweder einen Browser-Default, blockiert etwas, das keine Bedrohung ist, oder erfordert Arbeit, die noch nicht gemacht wurde. Keiner wurde verworfen, weil Sicherheit nicht zählt. Sie wurden verworfen, weil sie nicht das tun, was die meisten denken. Zumindest nicht bei einer statischen Site.
Cache-Strategie: lass den Build-Output führen
Hier zahlt sich das Wissen um den Output deines Frameworks am meisten aus.
Astro legt jedes verarbeitete Asset im _astro/-Verzeichnis ab, mit einem Content-Hash im Dateinamen: JS, CSS, Fonts, SVGs, Bilder. arc.FDYzXBdN.js. Ändert sich der Inhalt, ändert sich der Hash, und damit der Dateiname. Die alte URL wird nie wiederverwendet.
Das heißt: Der Browser kann diese Dateien für immer cachen. Nicht „eine lange Zeit“. Buchstäblich für immer. Der Hash garantiert, dass sich bei jeder Inhaltsänderung auch die URL ändert, also fragt der Browser stets die neue Version an. Es besteht kein Risiko, veralteten Content auszuliefern.
Ohne diese Regel lädt jeder wiederkehrende Besucher dein CSS, JS und deine Fonts bei jedem Seitenaufruf neu herunter. Auf einer mobilen Verbindung entscheidet genau das darüber, ob sich eine Seite sofort anfühlt oder langsam. Der Inhalt hat sich seit dem letzten Besuch nicht geändert, aber der Browser weiß das nicht.
header /_astro/* Cache-Control "public, max-age=31536000, immutable"max-age=31536000 ist ein Jahr. immutable sagt dem Browser, dass er gar nicht erst revalidieren soll. Keinen bedingten Request schicken, nicht prüfen, ob sich etwas geändert hat, einfach die gecachte Kopie nehmen. Das ist die übliche Policy für Assets mit Content-Hash.
HTML-Dateien sind anders. Sie haben keinen Hash im Dateinamen. /en/blog/some-post/index.html bleibt über alle Deploys hinweg dieselbe URL. Wenn ich eine neue Version veröffentliche, ändert sich das HTML, aber die URL bleibt gleich. Also muss der Browser beim Server nachfragen, bevor er eine gecachte Kopie verwendet.
header ?Cache-Control "no-cache"no-cache bedeutet nicht „nicht cachen“. Es bedeutet „cache es, aber frag vorher den Server“. Hat sich die Datei nicht geändert, antwortet Caddy mit 304 Not Modified, einer winzigen Response, praktisch kostenlos. Hat sie sich geändert, bekommt der Browser die neue Version. Das ?-Prefix heißt: Die Regel greift nur, wenn noch kein anderes Cache-Control gesetzt wurde, und überschreibt die _astro/*- und Favicon-Regeln nicht.
Favicons liegen dazwischen. Kein Hash, aber sie ändern sich selten:
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"header /favicon.ico Cache-Control "max-age=3600, must-revalidate"Drei Regeln, und jede folgt direkt aus dem, was der Astro-Build produziert. Dateien mit Hash werden für immer gecacht. Dateien ohne Hash werden revalidiert. Das ist die ganze Strategie.
Die kleinen Dinge
Ohne einen www-Redirect sind www.javedab.com und javedab.com aus Googles Sicht zwei verschiedene Seiten. Link-Equity verteilt sich auf zwei Domains. Ein 301 Permanent Redirect bündelt alles auf eine kanonische URL und behält den vollen Pfad.
www.javedab.com { redir https://javedab.com{uri} permanent}Die CI-Pipeline komprimiert alle Assets vorab, und Caddy liefert die Sidecar-Dateien direkt aus. Falls eine Datei aus irgendeinem Grund nicht vorkomprimiert ist, sollte ein Kompressions-Fallback sie on the fly komprimieren, statt sie unkomprimiert auszuliefern. Die encode-Direktive ist ein Sicherheitsnetz. Sie sollte nur selten greifen, und wenn sie greift, heißt das: in der Build-Pipeline stimmt etwas nicht.
encode zstd gzipACME-Email ist leicht zu übersehen. Caddy verwaltet TLS-Zertifikate automatisch über Let’s Encrypt. Scheitert die Erneuerung, geht die Seite mit einem TLS-Fehler offline. Besucher sehen eine Browser-Warnung, manche kommen nicht wieder. Ohne Email im ACME-Account passiert dieses Versagen stillschweigend. Du erfährst es erst, wenn ein Kunde dir sagt, deine Seite sei kaputt. Eine Zeile in der globalen Config macht daraus eine Warnung, die dich erreicht, bevor das passiert.
{ email javed@javedab.com}Wie die Config jetzt aussieht
(jav_static) { header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" }
header /_astro/* Cache-Control "public, max-age=31536000, immutable" header /favicon.svg Cache-Control "max-age=3600, must-revalidate" header /favicon.ico Cache-Control "max-age=3600, must-revalidate" header ?Cache-Control "no-cache"
encode zstd gzip file_server { precompressed br gzip zstd } handle_errors { rewrite * /404.html; file_server }}Jede Zeile hat einen Grund. Nichts steht da, weil ein Blogpost gesagt hat, es solle da stehen. Die Security-Header adressieren echte Bedrohungen. Die Cache-Regeln passen zum Build-Output. Der Kompressions-Fallback ist ein Sicherheitsnetz, nicht der Hauptpfad.
Ein curl-Aufruf bestätigt, dass die Regeln zum Zeitpunkt des Schreibens live sind:
$ curl -sI https://javedab.com/ | grep -iE '^(cache-control|strict-transport|x-frame)'cache-control: no-cachestrict-transport-security: max-age=31536000; includeSubDomains; preloadx-frame-options: DENYWorum es geht
Deine Web-Server-Config sollte abbilden, was deine Build-Pipeline produziert. Generische Defaults funktionieren, aber sie überlassen dem Browser Entscheidungen, die dir zustehen. Caddys Defaults decken die harten Teile ab: HTTP/3 ist an, TLS ist automatisch, vorkomprimierte Sidecar-Dateien werden nativ unterstützt. Aber Caching, Security-Header und Redirects sind Dinge, die nur du konfigurieren kannst, weil nur du weißt, was dein Framework ausgibt und was deine Site braucht.
Die Doku lesen, jede Option prüfen, die wichtigen umsetzen, mit curl verifizieren. Die Config ist von ein paar funktionalen Zeilen zu etwas geworden, das diese konkrete Seite abbildet, nicht irgendein generisches Template.
Für einen flüchtigen Besucher ist davon nichts zu sehen. Die Seite sah vorher und nachher gleich aus. Aber für jeden, der hinschaut, schon. Ein potenzieller Partner, der deine Domain durch securityheaders.com schickt. Ein Kunde, der die DevTools öffnet und immutable auf deinen Assets sieht. Die Art Mensch, die merkt, ob die Infrastruktur hinter der Seite genauso durchdacht ist wie die Seite selbst.
So sieht es aus, wenn du deine Infrastruktur in der Hand hast. Nicht kompliziert. Einfach bewusst.
Luft- und Raumfahrtingenieur
Ethischer Unternehmer in der Öffentlichkeit
Sie kümmern sich um Ihr Geschäft
Ich kümmere mich um die digitale Seite
Zusammenarbeiten
- KI mit Ehrlichkeit
- Private Infrastruktur
- Websites die performen
Erzählen Sie mir von Ihrer Situation:
javed@javedab.com Mehr über mich