Caddy de A à Z pour experts : Admin API REST, HTTPS automatique (ACME, DNS challenge, PKI interne), reverse proxy avancé, modules, on-demand TLS, clustering et production.
Le Caddyfile de base est documenté partout. Ce chapitre couvre les fonctionnalités qui font la différence en production : matchers nommés, snippets réutilisables, import, expressions CEL, et les patterns de config que vous allez copier-coller dans vos Caddyfiles.
Le bloc { } en tête de Caddyfile configure l'instance entière :
{
# Admin API
admin localhost:2019
# Email pour ACME (obligatoire en prod)
email admin@example.com
# Type de clé TLS — p256 plus rapide, rsa4096 plus compatible
key_type p256
# CA ACME par défaut (Let's Encrypt staging pour les tests)
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
# Désactiver le rate limiting ACME par IP (pour les clusters)
acme_ca_root /etc/caddy/acme-root.pem
# OCSP stapling
ocsp_stapling off # désactiver si réseau interne sans accès OCSP
# Log global
log {
output file /var/log/caddy/access.log {
roll_size 100mb
roll_keep 10
roll_keep_for 720h
}
format json
level INFO
}
# Storage backend pour les certificats
storage file_system {
root /var/lib/caddy
}
# Répertoire des snippets à importer
# (via import ./snippets/*.caddy dans les blocs)
}Les matchers nommés permettent de réutiliser des conditions complexes :
example.com {
# Définir des matchers nommés avec @
@api {
path /api/*
method GET POST PUT DELETE PATCH
}
@static {
path /static/* /assets/* /favicon.ico
not {
path /static/private/*
}
}
@authenticated {
header Authorization "Bearer *"
}
@admin {
remote_ip 10.0.0.0/8 192.168.0.0/16
path /admin/*
}
@bot {
header_regexp User-Agent `(?i)(bot|crawler|spider|scraper)`
}
# Utiliser les matchers
handle @static {
file_server {
root /var/www/static
}
}
handle @api {
reverse_proxy localhost:3000
}
handle @admin {
basic_auth {
admin $2a$14$... # bcrypt hash
}
reverse_proxy localhost:4000
}
handle @bot {
respond "Go away" 403
}
# Fallback
handle {
reverse_proxy localhost:3000
}
}Pour les conditions complexes, Caddy supporte Common Expression Language :
@complex_rule expression `{header.X-Forwarded-For}.matches("^10\\.") && {path}.startsWith("/api/")`
@jwt_valid expression `{vars.jwt_claims}.contains("admin")`
@ratelimit_exempt expression `{header.X-API-Key} in ["key1", "key2", "key3"]`# Déclarer un snippet
(common_headers) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-Server # supprimer l'en-tête Server
}
}
(cors_headers) {
@cors_preflight method OPTIONS
handle @cors_preflight {
header {
Access-Control-Allow-Origin "{header.Origin}"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
Access-Control-Max-Age "86400"
}
respond "" 204
}
header {
Access-Control-Allow-Origin "{header.Origin}"
Access-Control-Allow-Credentials "true"
}
}
(proxy_headers) {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
header_down -X-Powered-By
header_down -Server
}
(health_check) {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 3s
health_status 2xx
}
# Utiliser les snippets
api.example.com {
import common_headers
import cors_headers
reverse_proxy localhost:3000 localhost:3001 localhost:3002 {
import proxy_headers
import health_check
}
}# Importer un fichier entier
import /etc/caddy/sites/*.caddy
# Importer avec substitution de paramètres
(proxy_backend) {
reverse_proxy {args[0]} {
health_uri /health
header_up Host {upstream_hostport}
}
}
api.example.com {
handle /v1/* {
import proxy_backend "localhost:3001 localhost:3002"
}
handle /v2/* {
import proxy_backend "localhost:4001 localhost:4002"
}
}Caddy expose des placeholders {...} pour accéder au contexte de la requête :
example.com {
log {
output file /var/log/caddy/access.log
format json {
time_format rfc3339
message_key msg
fields {
request>method {method}
request>uri {uri}
request>host {host}
request>remote_ip {remote_host}
request>proto {proto}
request>headers>user-agent {request>headers>User-Agent}
response>status {status}
response>size {size}
duration {duration}
}
}
}
# Réécrire l'URI avant proxy
rewrite * /internal{uri}
# Réponse dynamique avec placeholder
respond "Hello {query.name}, your IP is {remote_host}" 200
}Placeholders disponibles (sélection) :
{method} GET, POST, ...
{uri} /path?query
{path} /path
{query} key=value&...
{query.key} valeur d'un paramètre spécifique
{host} example.com
{scheme} http ou https
{remote_host} IP du client
{remote_port} port source
{status} code de réponse
{size} taille de la réponse
{duration} durée en secondes
{header.Name} valeur d'un header de requête
{resp_header.Name} valeur d'un header de réponse
{vars.key} variables définies par set_var
{upstream_hostport} hôte:port de l'upstream proxy*.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
@tenant1 host tenant1.example.com
@tenant2 host tenant2.example.com
handle @tenant1 {
reverse_proxy tenant1-app:3000
}
handle @tenant2 {
reverse_proxy tenant2-app:3000
}
handle {
respond "Unknown tenant" 404
}
}api.example.com {
# Module caddy-ratelimit (xcaddy)
@expensive_routes path /api/export/* /api/report/*
handle @expensive_routes {
rate_limit {
zone expensive {
key {remote_host}
events 5
window 1m
}
}
reverse_proxy localhost:3000
}
handle {
rate_limit {
zone default {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
}example.com {
@maintenance file {
root /var/www
try_files /maintenance.html
}
handle @maintenance {
header Cache-Control "no-store"
file_server {
root /var/www
}
}
handle {
reverse_proxy localhost:3000
}
}Activer la maintenance : touch /var/www/maintenance.html. Désactiver : rm /var/www/maintenance.html. Zéro reload Caddy.
basic_auth est intégré dans Caddy — aucun module externe. Il protège des routes avec un username/password via HTTP Basic Authentication.
# Via Caddy directement
caddy hash-password --plaintext "monmotdepasse"
# $2a$14$...hash...
# Via htpasswd (Apache utils)
htpasswd -nB admin
# admin:$2y$05$...admin.example.com {
basic_auth {
# username bcrypt hash
admin $2a$14$hObMVdOl2LOb/djsGVIhu.dh9OHkPt5y9O1GJ4JLLGP9NN6/6TSe
deploy $2a$14$Qq5mRr5qYH.xK2rTEJkXTeOLCr.EFn77Yj1Lr4ZL6VbIMqhlPsra
}
reverse_proxy localhost:4000
}example.com {
# /admin/* protégé, reste public
handle /admin/* {
basic_auth {
admin $2a$14$hObMVdOl2LOb/djsGVIhu.dh9OHkPt5y9O1GJ4JLLGP9NN6/6TSe
}
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:3000
}
}example.com {
@protected {
path /admin/* /api/internal/* /metrics
not {
remote_ip 10.0.0.0/8 # IP interne exemptée
}
}
handle @protected {
basic_auth {
admin $2a$14$hObMVdOl2LOb/djsGVIhu.dh9OHkPt5y9O1GJ4JLLGP9NN6/6TSe
}
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:3000
}
}{
"handler": "authentication",
"providers": {
"http_basic": {
"realm": "Admin Area",
"accounts": [
{
"username": "admin",
"password": "$2a$14$hObMVdOl2LOb/djsGVIhu.dh9OHkPt5y9O1GJ4JLLGP9NN6/6TSe"
}
],
"hash": {"algorithm": "bcrypt"}
}
}
}Une fois authentifié, Caddy expose le username via {http.auth.user.id} :
example.com {
basic_auth {
admin $2a$14$...
}
header X-Authenticated-User {http.auth.user.id}
reverse_proxy localhost:3000
}basic_auth convient pour les outils internes, dashboards, endpoints de monitoring. Pour de l'authentification utilisateur en production, préférer forward_auth (chapitre 7) avec Authelia ou Authentik.
Le Caddyfile avancé est maîtrisé. Le chapitre suivant couvre l'Admin API en détail — lecture, modification, et remplacement de config en temps réel via REST.