Ce que Caddy devrait savoir sur ton site Astro
بِسْمِ ٱللَّهِ ٱلرَّحْمَـٰنِ ٱلرَّحِيمِ
L’écart entre le build et la livraison
Tu as un site. Il se charge, il fonctionne, il montre ce que tu voulais montrer. Mais sais-tu si les visiteurs qui reviennent voient tes pages se charger instantanément ou retéléchargent tout à chaque clic ? Si la connexion peut être rétrogradée en HTTP ? Si ta page d’accueil apparaît comme un domaine ou comme deux dans Google ? Pour la plupart des sites, les réponses sont : non, oui, et oui. Cette dernière couche du stack, c’est la configuration du serveur web, et la plupart des entreprises n’y touchent jamais.
Astro fait beaucoup de travail au moment du build. Il compile les pages en HTML statique, ajoute un content-hash au nom de chaque asset dans le dossier _astro/, et produit des fichiers prêts pour n’importe quel serveur statique. La pipeline de CI ajoute encore une couche : elle pré-compresse tout en fichiers sidecar .br, .gz et .zst pour que le serveur n’ait jamais à compresser on the fly.
Tout ce travail peut être silencieusement défait par la dernière couche de la chaîne : la configuration du serveur web. Caddy sert les fichiers, mais il ne sait pas ce qu’il sert. Il ne sait pas que les fichiers dans _astro/ portent un content-hash dans leur nom. Il ne sait pas que le HTML change à chaque déploiement alors que le CSS non. Il ne sait pas que le site est uniquement en HTTPS et qu’il n’y a aucune raison qu’il apparaisse un jour dans un iframe.
C’est pour cet écart de connaissance que la config existe. Et la mienne a ressemblé à ceci pendant un temps :
(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 }}Ça fonctionnait. Les fichiers étaient servis, les assets pré-compressés étaient pris en compte, la page 404 s’affichait quand il le fallait. Mais guère plus. Pas d’en-têtes de sécurité, pas de stratégie de cache au-delà du favicon, pas de redirection www. Le navigateur prenait des décisions qui auraient dû être les miennes. Et quiconque lançait un scanner de sécurité ou mesurait la vitesse de la page voyait précisément tout ce qui était laissé de côté.
Ce qui compte vraiment pour un site statique
J’ai parcouru les directives Caddy que j’évaluais : headers, caching, compression, logging, options TLS, timeouts serveur, métriques Prometheus. Logging, TLS, timeouts et métriques n’appelaient aucune décision pour un site statique de cette taille. Les défauts de Caddy suffisent. Les choix intéressants se trouvent dans les headers, le caching et la compression. La question n’était pas ce que Caddy peut faire, mais ce dont mon site a vraiment besoin.
Un site statique sans login, sans formulaires, sans données utilisateur et sans backend a un modèle de menaces très différent de celui d’une application web. La plupart des guides de sécurité sont écrits pour cette dernière. Les appliquer aveuglément à un site statique revient à ajouter de la complexité qui ne protège de rien.
J’ai donc évalué chaque option avec un test simple : est-ce que ça adresse une vraie menace, ou est-ce que ça se contente de plaire à un scanner ?
Trois en-têtes de sécurité, pas six
Sur internet, les listes qui te disent d’ajouter chaque en-tête de sécurité existant ne manquent pas. Ce qui est entré, et ce qui ne l’est pas.
Strict-Transport-Security adresse une vraie menace. Sans elle, un visiteur sur un WiFi public pourrait voir sa connexion rétrogradée en HTTP et interceptée. Le site est déjà uniquement en HTTPS, donc cette en-tête dit juste au navigateur de ne jamais essayer HTTP. Un an, sous-domaines inclus, avec preload pour entrer dans la liste HSTS preload que les navigateurs embarquent en dur. Le coût est réel : chaque sous-domaine, actuel et futur, doit servir en HTTPS ou devenir inaccessible. Sortir de la liste preload n’est pas entre tes mains : ça se passe dans les cycles de release des navigateurs, pas dans ta config. Accepté ici parce que ce site est uniquement HTTPS par intention, et Caddy fournit automatiquement un certificat pour chaque sous-domaine via Let’s Encrypt.
X-Content-Type-Options: nosniff empêche les navigateurs de deviner les types de contenu. Sans lui, un navigateur pourrait interpréter un fichier texte comme du code exécutable. Une valeur, aucune configuration, aucun compromis.
X-Frame-Options: DENY empêche quiconque d’intégrer le site dans un iframe. Protège du clickjacking, où un attaquant superpose une UI invisible par-dessus ta page pour voler des clics. Ce site n’a aucune raison d’apparaître dans un iframe, donc DENY est la bonne réponse.
header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY"}Voilà. Trois en-têtes qui couvrent trois vecteurs d’attaque réels. Les options rejetées et pourquoi :
Referrer-Policy a déjà par défaut strict-origin-when-cross-origin dans les navigateurs principaux. La fixer explicitement ne change rien. Les sites externes voient ton domaine comme referrer (bon pour le SEO), mais pas le chemin complet. Le bon comportement est déjà là, sans toucher à la config.
Permissions-Policy désactive des API du navigateur comme caméra, micro, géolocalisation. Mais un site statique n’utilise pas ces API. Bloquer quelque chose que tu n’utilises pas ne te protège de rien. Si un attaquant peut injecter du JavaScript pour accéder à la caméra, tu as des problèmes bien plus gros qu’un en-tête manquant. Et si tu intègres des vidéos YouTube avec plein écran activé, comme je le fais, il faudrait ensuite découper des exceptions avec soin. Plus de complexité pour zéro gain de sécurité.
Cross-Origin-Opener-Policy empêche d’autres sites d’obtenir une référence à ta fenêtre de navigateur. Ça compte pour des sites avec des flux d’authentification où un popup pourrait voler des tokens. Un site statique n’a pas d’auth, pas de tokens, pas de popups.
Content-Security-Policy est une allowlist à l’exécution pour les scripts, les styles, les fontes et les images. Elle contient le cross-site scripting, bloque l’exfiltration de données et attrape du code tiers compromis, quelle que soit la façon dont ce code a atterri sur la page. Pour les sites qui rendent des données non fiables (formulaires, contenu utilisateur, API externes), elle réduit une surface d’attaque active ; pour les sites sans ces chemins d’entrée, elle protège contre des événements moins probables mais à plus fort impact : un script d’analytics compromis, un build falsifié, un CDN empoisonné, un changement futur qui ajoute discrètement un chemin d’entrée. Le coût est réel : chaque ressource doit être inventoriée, et la moindre erreur casse la page en silence. Astro 6 a ajouté une intégration qui couvre ce qu’Astro émet ; les iframes, les fontes externes et les services tiers à l’exécution restent manuels. Inshallah CSP suivra quand l’audit trouvera sa place dans le calendrier, ou plus tôt si le modèle de menaces change.
Le schéma : chaque en-tête écartée, soit duplique un default du navigateur, soit bloque quelque chose qui n’est pas une menace, soit demande un travail qui n’a pas encore été fait. Aucune n’a été rejetée parce que la sécurité ne compte pas. Elles ont été rejetées parce qu’elles ne font pas ce que les gens croient qu’elles font. Du moins pas sur un site statique.
Stratégie de cache : laisse le build te guider
C’est là que connaître la sortie de ton framework compte le plus.
Astro place chaque asset traité dans le dossier _astro/ avec un content-hash dans le nom : JS, CSS, fontes, SVG, images. arc.FDYzXBdN.js. Si le contenu change, le hash change, et le nom aussi. L’ancienne URL n’est jamais réutilisée.
Ça veut dire que le navigateur peut mettre ces fichiers en cache pour toujours. Pas «longtemps». Littéralement pour toujours. Le hash garantit que, si le contenu change, l’URL change, donc le navigateur demandera toujours la nouvelle version. Aucun risque de servir du contenu périmé.
Sans cette règle, chaque visiteur récurrent retélécharge ton CSS, ton JS et tes fontes à chaque chargement de page. Sur une connexion mobile, c’est la différence entre un site qui paraît instantané et un site qui paraît lent. Le contenu n’a pas changé depuis sa dernière visite, mais le navigateur ne le sait pas.
header /_astro/* Cache-Control "public, max-age=31536000, immutable"max-age=31536000, c’est un an. immutable dit au navigateur de ne même pas tenter une revalidation. Pas de requête conditionnelle, pas de vérification, juste utiliser la copie en cache. C’est la politique habituelle pour les assets avec content-hash.
Les fichiers HTML, c’est différent. Ils n’ont pas de hash dans leur nom. /en/blog/some-post/index.html reste la même URL à travers les déploiements. Quand je publie une nouvelle version, le HTML change mais l’URL non. Donc le navigateur doit interroger le serveur avant d’utiliser une copie en cache.
header ?Cache-Control "no-cache"no-cache ne veut pas dire «ne pas mettre en cache». Ça veut dire «mets en cache, mais demande d’abord au serveur». Si le fichier n’a pas changé, Caddy renvoie un 304 Not Modified, une toute petite réponse, quasiment gratuite. S’il a changé, le navigateur récupère la nouvelle version. Le préfixe ? signifie que cette règle ne s’applique que si aucun autre Cache-Control n’a été posé, donc elle n’écrase pas les règles _astro/* ni favicon.
Les favicons sont entre les deux. Pas de hash, mais ils changent rarement :
header /favicon.svg Cache-Control "max-age=3600, must-revalidate"header /favicon.ico Cache-Control "max-age=3600, must-revalidate"Trois règles, et chacune découle directement de ce que le build Astro produit. Les fichiers avec hash sont mis en cache pour toujours. Les fichiers sans hash sont revalidés. C’est toute la stratégie.
Les petits détails
Sans redirection www, www.javedab.com et javedab.com sont deux sites séparés aux yeux de Google. Le link equity se partage entre deux domaines. Une redirection 301 permanente consolide tout sur une seule URL canonique et conserve le chemin complet.
www.javedab.com { redir https://javedab.com{uri} permanent}La pipeline de CI pré-compresse tous les assets, et Caddy sert les fichiers sidecar directement. Mais si un fichier n’a, pour une raison ou une autre, pas été pré-compressé, un fallback de compression devrait le compresser on the fly plutôt que de le servir sans compression. La directive encode est un filet de sécurité. Elle devrait rarement se déclencher, et si elle le fait, ça signifie que quelque chose dans la pipeline de build a besoin d’attention.
encode zstd gzipL’email ACME est facile à oublier. Caddy gère les certificats TLS automatiquement via Let’s Encrypt. Si le renouvellement échoue, le site tombe avec une erreur TLS. Les visiteurs voient un avertissement du navigateur, et certains ne reviennent pas. Sans email sur le compte ACME, cet échec est silencieux. Tu t’en rends compte quand un client te dit que ton site a l’air cassé. Une ligne dans la config globale le transforme en un avertissement qui t’arrive avant que ça se produise.
{ email javed@javedab.com}À quoi ressemble la config maintenant
(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 }}Chaque ligne a une raison. Rien n’est là parce qu’un article de blog a dit de l’ajouter. Les en-têtes de sécurité adressent des menaces réelles. Les règles de cache collent à la sortie du build. Le fallback de compression est un filet de sécurité, pas la voie principale.
Un appel curl confirme que les règles sont actives au moment de l’écriture :
$ 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: DENYL’essentiel
La configuration de ton serveur web devrait refléter ce que produit ta pipeline de build. Les défauts génériques fonctionnent, mais ils laissent au navigateur des décisions qui devraient être les tiennes. Les défauts de Caddy couvrent le difficile : HTTP/3 est actif, le TLS est automatique, les fichiers sidecar pré-compressés sont supportés nativement. Mais le caching, les en-têtes de sécurité et les redirections sont des choses que toi seul peux configurer, parce que toi seul sais ce que ton framework produit et ce dont ton site a besoin.
Lire la doc, évaluer chaque option, implémenter celles qui comptent, vérifier avec curl. La config est passée de quelques lignes fonctionnelles à quelque chose qui reflète ce site précis, pas un template générique.
Rien de tout ça n’est visible pour un visiteur de passage. Le site avait l’air identique avant et après. Mais c’est visible pour quiconque vérifie. Un partenaire potentiel qui passe ton domaine dans securityheaders.com. Un client qui ouvre DevTools et voit immutable sur tes assets. Le genre de personne qui remarque si l’infrastructure derrière la page est aussi pensée que la page elle-même.
Voilà à quoi ça ressemble, d’avoir son infrastructure entre ses mains. Pas compliqué. Juste intentionnel.
Ingénieur aérospatial
Entrepreneur éthique en public
Vous gérez votre activité
Je m'occupe du côté numérique
Travailler avec moi
- IA avec honnêteté
- Infrastructure privée
- Sites web performants
Parlez-moi de votre situation :
javed@javedab.com En savoir plus