diff --git a/contrib/Makefile b/contrib/Makefile index 88d090ce7..f16973af1 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -49,8 +49,9 @@ generate-service-units: generated/$(APP_INSTALL_PREFIX)yavscOrg.service generate # make generate-nginx -> generated/nginx/preprod-yavsc{Org,Blogs,Api} # make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= -> prod (yavsc/api/blogs.pschneider.fr) # -# HTTP/3 (QUIC) termine au edge nginx ; Kestrel reste en h1/h2 backend. -# Les vhosts générés vont dans generated/nginx/ (gitignoré, comme generated/*.service). +# HTTP/3 (QUIC) termine au edge nginx (plan contrib/quic.md). Requiert nginx +# avec http_v3_module + UDP 443 ouvert. reuseport sur Org uniquement +# (QUIC_OPTS). Les vhosts générés vont dans generated/nginx/ (gitignoré). NGINX_TEMPLATE := template.vhost NGINX_OUT_DIR := generated/nginx NGINX_HOSTS := Org Blogs Api @@ -83,6 +84,32 @@ generate-nginx-prod: $(NGINX_OUT_DIR): @mkdir -p $@ +# --- appsettings (générés depuis template.appsettings-*.json) --- +# make generate-appsettings -> generated/appsettings/appsettings-{org,api,blogs}.json +# +# Substitue $DOMAIN (racine du domaine, cf. DOMAIN_ROOT) dans toutes les +# chaînes, et $SMTP_PASSWORD (secret du .env) dans Smtp.Password. +# On utilise jq (et non envsubst) car envsubst n'échappe pas le JSON : un +# secret contenant \ ou " casserait le JSON (le mot de passe SMTP actuel +# contient un \). jq lit le secret depuis l'environnement (export) -> +# échappement JSON correct, aucun shell-quoting ni exposition en ligne de cmd. +# Les templates actuels sont issus de la preprod (préfixe preprod-, ports +# 88-93) ; pour la prod il faudra un template prod (ports 82-87, sans préfixe). +export ASPNETCORE_Smtp__Password +APPSETTINGS_OUT_DIR := generated/appsettings +APPSETTINGS_HOSTS := org api blogs + +$(APPSETTINGS_OUT_DIR)/appsettings-%.json: $(APPSETTINGS_OUT_DIR) template.appsettings-%.json .$(APP_INSTALL_ENV).env + @jq --arg domain "$(DOMAIN_ROOT)" \ + 'walk(if type=="string" then gsub("\\$$DOMAIN"; $$domain) else . end) | if has("Smtp") then .Smtp.Password = env["ASPNETCORE_Smtp__Password"] else . end' \ + template.appsettings-$*.json > $@ + @echo Created appsettings: $@ + +generate-appsettings: $(addprefix $(APPSETTINGS_OUT_DIR)/appsettings-,$(addsuffix .json,$(APPSETTINGS_HOSTS))) + +$(APPSETTINGS_OUT_DIR): + @mkdir -p $@ + install-service-%: /etc/systemd/system/$(APP_INSTALL_PREFIX)yavsc%.service sudo systemctl daemon-reload sudo systemctl enable $(APP_INSTALL_PREFIX)yavsc$* diff --git a/contrib/README.nginx.md b/contrib/README.nginx.md index f2ea5a156..9564d4ffe 100644 --- a/contrib/README.nginx.md +++ b/contrib/README.nginx.md @@ -3,11 +3,50 @@ Les vhosts nginx sont **générés** à l'image des unités de service systemd (`template.service` + `envsubst` → `generated/*.service`) : un template unique `contrib/template.vhost` est substitué par `envsubst` vers -`contrib/generated/nginx/`, qui est **gitignoré** (comme `generated/`). +`contrib/generated/nginx/`, qui est **gitignoré**. -HTTP/3 (QUIC) **termine au edge nginx** sur 443/udp. Kestrel reste en -h1/h2 sur son port backend ; nginx lui parle en HTTP/1.1 -(`proxy_http_version 1.1`). +## Rôle de nginx : edge HTTP/3 (plan `contrib/quic.md`) + +nginx termine HTTP/3 (QUIC) au edge sur 443/udp, en coexistence avec +h1/h2 sur 443/tcp. Kestrel reste backend h1/h2 (`proxy_http_version 1.1`). +Le « QUIC logiciel » Kestrel (`Protocols: Http1AndHttp2AndHttp3` dans les +`template.appsettings-*.json`) est une capacité complémentaire — non +joignable par les clients derrière nginx (nginx OSS = upstream h1.1 +uniquement), mais disponible si Kestrel est un jour exposé en direct. + +Directives générées (par vhost) : +```nginx +listen 443 ssl; listen [::]:443 ssl; http2 on; +listen 443 quic $QUIC_OPTS; listen [::]:443 quic $QUIC_OPTS; http3 on; +add_header Alt-Svc 'h3=":443"; ma=86400' always; +ssl_protocols TLSv1.3; +``` +`$QUIC_OPTS = reuseport` sur **Org uniquement**, vide sur Api/Blogs (règle +`contrib/quic.md` : `reuseport` une seule fois par addr:port, tous vhosts +confondus). + +## Prérequis côté host + +1. **nginx avec `http_v3_module`** (≥ 1.25 ; la directive `http3 on;` + exige ≥ 1.27.4 — sur une version 1.25–1.27.3, retirer `http3 on;`, + `listen ... quic` suffit à activer h3) : + ```bash + nginx -V 2>&1 | grep -o 'http_v3_module' # doit être non vide + ``` + Debian 13 « trixie » fournit `nginx` avec `http_v3_module`. Sinon, + backports ou `nginx-full`. +2. **UDP 443 ouvert** : + ```bash + sudo ufw allow 443/udp + sudo ufw status | grep '443/udp' # → ALLOW + ``` +3. **IPv6** : le template émet `listen [::]:443 ...` (et `[::]:80`). Si le + host n'a pas d'IPv6, retirer/commenter ces lignes (sinon `nginx -t` + échoue sur le bind IPv6). +4. Certbot / certs Let's Encrypt déjà en place (chemins + `/etc/letsencrypt/live//{fullchain,privkey}.pem` référencés). +5. (Optionnel, pour le QUIC logiciel Kestrel) `apt install libmsquic` — + sinon Kestrel désactive h3 silencieusement (fallback h1/h2 gracieux). ## Génération @@ -23,56 +62,28 @@ make generate-nginx-prod make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= ``` -Les ports backend et `HTTP_HOST` proviennent de `.preprod.env` / -`.production.env` (ex. prod Org=83, Api=87, Blogs=85 ; preprod 89/93/91). -Le schéma backend est `https` en prod (Kestrel écoute HTTPS, cert -Let's Encrypt) et `http` en preprod (Kestrel écoute HTTP simple). +Ports backend et `HTTP_HOST` depuis `.preprod.env` / `.production.env` +(prod Org=83, Api=87, Blogs=85 ; preprod 89/93/91). Schéma backend +`https` en prod, `http` en preprod. -| Hôte | Sous-domaine | `reuseport` | Backend prod | Backend preprod | -|-------|--------------|-------------|--------------|-----------------| -| Org | yavsc | **oui** | `https://localhost:83` | `http://localhost:89` | -| Api | api | non | `https://localhost:87` | `http://localhost:93` | -| Blogs | blogs | non | `https://localhost:85` | `http://localhost:91` | +| Hôte | Sous-domaine | `reuseport` | Backend prod | Backend preprod | +|-------|--------------|-------------|-------------------------|------------------------| +| Org | yavsc | **oui** | `https://localhost:83` | `http://localhost:89` | +| Api | api | non | `https://localhost:87` | `http://localhost:93` | +| Blogs | blogs | non | `https://localhost:85` | `http://localhost:91` | Domaines : `.pschneider.fr` -(prod : `yavsc.pschneider.fr` ; preprod : `preprod-yavsc.pschneider.fr`). +(prod `yavsc.pschneider.fr` ; preprod `preprod-yavsc.pschneider.fr`). ## Déploiement sur le host ```bash -# depuis le host, après avoir copié les fichiers générés : sudo cp generated/nginx/yavscOrg /etc/nginx/sites-available/yavscOrg sudo ln -sf /etc/nginx/sites-available/yavscOrg /etc/nginx/sites-enabled/yavscOrg -# (idem pour yavscApi, yavscBlogs) +# (idem yavscApi, yavscBlogs) sudo nginx -t && sudo systemctl reload nginx ``` -## Prérequis côté host - -1. **nginx ≥ 1.25** compilé avec `http_v3_module` : - ```bash - nginx -V 2>&1 | grep -o 'http_v3_module' # doit être non vide - ``` - Debian 13 « trixie » fournit `nginx` avec `http_v3_module`. Sinon, - `nginx-full` ou les backports. -2. **UDP 443 ouvert** : - ```bash - sudo ufw allow 443/udp - sudo ufw status | grep '443/udp' # → ALLOW - ``` -3. Certbot / certs Let's Encrypt déjà en place (réutilisés tels quels — - les chemins `/etc/letsencrypt/live//{fullchain,privkey}.pm` - sont référencés par le template). - -## Règle : un seul `reuseport` sur 443/quic - -`listen 443 quic reuseport;` ne peut figurer **qu'une seule fois** par -couple adresse:port, pour tous les vhosts confondus. Le template le -pose sur **Org** seulement (`QUIC_OPTS=reuseport` pour Org, vide pour -Api/Blogs — règle `$(if $(filter Org,$*),reuseport,)` dans le -`Makefile`). Si vous réordonnez les vhosts, gardez `reuseport` sur un -seul. - ## Vérification (sur le host) ```bash @@ -85,46 +96,39 @@ sudo ufw status | grep '443/udp' # 3. En-tête Alt-Svc annoncé curl -sI https://yavsc.pschneider.fr/ | grep -i alt-svc # → h3=":443" -# 4. Requête HTTP/3 effective (curl doit être compilé avec HTTP/3) +# 4. Requête HTTP/3 effective (curl compilé avec HTTP/3) curl -v --http3 https://yavsc.pschneider.fr/ 2>&1 | grep -i 'HTTP/3' # 5. Fallback h2 et h1 toujours fonctionnels curl --http2 https://yavsc.pschneider.fr/ -o /dev/null -w '%{http_code}\n' curl --http1.1 https://yavsc.pschneider.fr/ -o /dev/null -w '%{http_code}\n' ``` - -Répéter 3-5 pour `api.pschneider.fr` et `blogs.pschneider.fr` (prod), -et pour les trois domaines preprod. Sans curl HTTP/3, utiliser un -navigateur : DevTools → Network → colonne « Protocole » affiche `h3` -après la première mise à niveau. +Répéter 3-5 pour `api.pschneider.fr` et `blogs.pschneider.fr` (prod) et +les trois domaines preprod. Sans curl HTTP/3, un navigateur : DevTools → +Network → colonne « Protocole » → `h3` après mise à niveau. ## Comment ça marche (template + envsubst) -`contrib/template.vhost` contient des placeholders `$DOMAIN`, -`$BACKEND_SCHEME`, `$HTTP_HOST`, `$BACKEND_PORT`, `$QUIC_OPTS`. La -règle `generated/nginx/$(APP_INSTALL_PREFIX)yavsc%` du `Makefile` les -substitue via `envsubst` avec une **allowlist** : +`contrib/template.vhost` contient les placeholders `$DOMAIN`, +`$BACKEND_SCHEME`, `$HTTP_HOST`, `$BACKEND_PORT`, `$QUIC_OPTS`. La règle +`generated/nginx/$(APP_INSTALL_PREFIX)yavsc%` du `Makefile` les substitue +via `envsubst` avec une **allowlist** : ```makefile envsubst '$DOMAIN $BACKEND_SCHEME $HTTP_HOST $BACKEND_PORT $QUIC_OPTS' ``` -Cette allowlist est **critique** : elle limite la substitution aux 5 -variables citées, de sorte que les variables nginx intrinsèques -(`$host`, `$scheme`, `$remote_addr`, `$proxy_add_x_forwarded_for`, -`$request_uri`) survivent intactes dans le vhost généré. Sans +Cette allowlist limite la substitution aux 5 variables citées, de sorte +que les variables nginx intrinsèques (`$host`, `$scheme`, `$remote_addr`, +`$proxy_add_x_forwarded_for`, `$request_uri`) survivent intactes. Sans allowlist, `envsubst` détruirait ces variables nginx. ## Hors périmètre -- **Kestrel inchangé** : ni `Protocols: Http1AndHttp2AndHttp3`, ni - `libmsquic`. Si l'on veut plus tard que Kestrel parle aussi h3 - (accès direct sans nginx / topo L4), ajouter - `Kestrel:Endpoints:Https:Protocols = "Http1AndHttp2AndHttp3"` dans - les `appsettings-{app}*.json` + `apt install libmsquic` sur le host - (Kestrel désactive h3 silencieusement si libmsquic absent — fallback - gracieux). -- **docker-compose / Dockerfile** : non touchés (déploiement réel - bare-metal systemd + nginx). -- **Backend HTTP/2** : nginx open-source ne supporte que HTTP/1.1 en - upstream ; on garde `proxy_http_version 1.1`. \ No newline at end of file +- `docker-compose` / `Dockerfile` : non touchés (déploiement bare-metal + systemd + nginx). +- Backend HTTP/2 vers Kestrel : nginx open-source ne supporte que + HTTP/1.1 en upstream ; on garde `proxy_http_version 1.1`. +- `ssl_protocols TLSv1.3` (suivant `contrib/quic.md`) désactive TLS 1.2 + côté TCP aussi. Pour conserver la compat TLS 1.2 sur le fallback TCP, + relâcher en `ssl_protocols TLSv1.2 TLSv1.3;` (QUIC reste en 1.3). \ No newline at end of file diff --git a/contrib/template.appsettings-api.json b/contrib/template.appsettings-api.json index d6412777c..b364eeef1 100644 --- a/contrib/template.appsettings-api.json +++ b/contrib/template.appsettings-api.json @@ -25,7 +25,8 @@ "Certificate": { "Path": "/etc/letsencrypt/live/preprod-api.$DOMAIN/fullchain.pem", "KeyPath": "/etc/letsencrypt/live/preprod-api.$DOMAIN/privkey.pem" - } + }, + "Protocols": "Http1AndHttp2AndHttp3" } } } diff --git a/contrib/template.appsettings-blogs.json b/contrib/template.appsettings-blogs.json index a3efb762a..f522feaca 100644 --- a/contrib/template.appsettings-blogs.json +++ b/contrib/template.appsettings-blogs.json @@ -32,7 +32,8 @@ "Certificate": { "Path": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/fullchain.pem", "KeyPath": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/privkey.pem" - } + }, + "Protocols": "Http1AndHttp2AndHttp3" } } } diff --git a/contrib/template.appsettings-org.json b/contrib/template.appsettings-org.json index dbbfdb525..76203d9e9 100644 --- a/contrib/template.appsettings-org.json +++ b/contrib/template.appsettings-org.json @@ -66,7 +66,8 @@ "Certificate": { "Path": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/fullchain.pem", "KeyPath": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/privkey.pem" - } + }, + "Protocols": "Http1AndHttp2AndHttp3" } } } diff --git a/contrib/template.vhost b/contrib/template.vhost index 75e48c14a..1e9bbdb7e 100644 --- a/contrib/template.vhost +++ b/contrib/template.vhost @@ -1,34 +1,49 @@ # Généré depuis contrib/template.vhost par `make generate-nginx` — NE PAS ÉDITER. # Vhost $DOMAIN — backend $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT -# HTTP/3 (QUIC) termine au edge nginx ; Kestrel reste en h1/h2 backend. +# +# HTTP/3 (QUIC) termine au edge nginx (plan contrib/quic.md). Requiert un +# nginx compilé avec http_v3_module (≥ 1.25 ; `http3 on` ≥ 1.27.4) + UDP 443 +# ouvert. Kestrel reste backend h1/h2 (mais configure aussi h3 côté logiciel, +# cf. template.appsettings-*.json — capacité, non joignable derrière nginx). # # Variables substituées par envsubst (allowlist) : # DOMAIN BACKEND_SCHEME HTTP_HOST BACKEND_PORT QUIC_OPTS +# $QUIC_OPTS = "reuseport" sur un seul vhost (Org), vide sur les autres +# (règle : reuseport une seule fois par addr:port, cf. contrib/quic.md). # Les variables nginx intrinsèques ($host, $scheme, $remote_addr, …) sont # préservées (hors allowlist). -# Port 80 : redirection vers HTTPS. (Certbot/nginx-plugin gère les -# challenges ACME ; ne pas servir l'app en HTTP clair.) +# Port 80 : redirection vers HTTPS. (Certbot/nginx-plugin gère les challenges +# ACME ; ne pas servir l'app en HTTP clair.) server { listen 80; + listen [::]:80; server_name $DOMAIN; return 301 https://$host$request_uri; } server { + # 1. TCP : HTTPS classique + HTTP/2 listen 443 ssl; - listen 443 quic $QUIC_OPTS; # HTTP/3 (UDP 443) — reuseport sur un seul vhost - http2 on; # HTTP/2 sur TCP + listen [::]:443 ssl; + http2 on; + + # 2. UDP : HTTP/3 (QUIC). reuseport sur UN seul vhost (Org via QUIC_OPTS). + listen 443 quic $QUIC_OPTS; + listen [::]:443 quic $QUIC_OPTS; + http3 on; + server_name $DOMAIN; - # Annonce HTTP/3 au client sur le même port 443/udp. `always` pour - # émettre l'en-tête même sur les réponses d'erreur. Le navigateur - # fait d'abord h1/h2 puis upgrade en h3. - add_header Alt-Svc 'h3=":443"; ma=86400' always; - + # 3. Certificats Let's Encrypt (Certbot). ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; # managed by Certbot - ssl_protocols TLSv1.2 TLSv1.3; # QUIC exige TLS 1.3 ; TLS 1.2 gardé pour le fallback TCP + # QUIC exige TLS 1.3. (TLS 1.2 conservable pour le fallback TCP : + # `ssl_protocols TLSv1.2 TLSv1.3;` — QUIC reste en 1.3.) + ssl_protocols TLSv1.3; + + # 4. Annonce h3 au navigateur sur 443/udp. `always` = émis même en erreur. + add_header Alt-Svc 'h3=":443"; ma=86400' always; location / { proxy_pass $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT;