From 3fe7b8b46c3d74a5c5baf29a47a832d9d78cfff5 Mon Sep 17 00:00:00 2001 From: Paul Schneider Date: Fri, 25 Sep 2026 13:27:00 +0100 Subject: [PATCH] templates for appsettings --- contrib/Makefile | 40 +++++++- contrib/README.nginx.md | 130 ++++++++++++++++++++++++ contrib/template.appsettings-api.json | 32 ++++++ contrib/template.appsettings-blogs.json | 39 +++++++ contrib/template.appsettings-org.json | 73 +++++++++++++ contrib/template.vhost | 44 ++++++++ 6 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 contrib/README.nginx.md create mode 100644 contrib/template.appsettings-api.json create mode 100644 contrib/template.appsettings-blogs.json create mode 100644 contrib/template.appsettings-org.json create mode 100644 contrib/template.vhost diff --git a/contrib/Makefile b/contrib/Makefile index 4ba6059df..88d090ce7 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -24,7 +24,7 @@ generated/$(APP_INSTALL_PREFIX)yavsc%.service: generated/ template.service $(SLN @cat template.service | APP_NAME="$*" \ DESTDIR="$(DESTDIR)" \ HTTP_HOST="$(HTTP_HOST)" \ - HTTP_PORT="$*_$(HTTP_PORT)" \ + HTTP_PORT="$($*_PORT)" \ APPDIR="$(APPDIR)" \ APP_SYSLOG_ID="$(APP_INSTALL_PREFIX)yavsc$*" \ POSTGRES_HOST="$(POSTGRES_HOST)" \ @@ -45,6 +45,44 @@ generated/$(APP_INSTALL_PREFIX)yavsc%.service: generated/ template.service $(SLN generate-service-units: generated/$(APP_INSTALL_PREFIX)yavscOrg.service generated/$(APP_INSTALL_PREFIX)yavscBlogs.service generated/$(APP_INSTALL_PREFIX)yavscApi.service +# --- Vhosts nginx (générés par envsubst, à l'image des unités systemd) --- +# 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). +NGINX_TEMPLATE := template.vhost +NGINX_OUT_DIR := generated/nginx +NGINX_HOSTS := Org Blogs Api +DOMAIN_ROOT ?= pschneider.fr +# Schéma backend Kestrel : https en prod (Kestrel écoute HTTPS, cert Let's +# Encrypt), http en preprod (Kestrel écoute HTTP simple, cf. .preprod.env). +BACKEND_SCHEME ?= $(if $(filter production,$(APP_INSTALL_ENV)),https,http) +# sous-domaine par hôte : Org -> yavsc, Api -> api, Blogs -> blogs +nginx_subdomain = $(if $(filter Org,$1),yavsc,$(if $(filter Api,$1),api,$(if $(filter Blogs,$1),blogs,$1))) +# domaine complet : . +# prod Org -> yavsc.pschneider.fr +# preprod Org -> preprod-yavsc.pschneider.fr +nginx_domain = $(APP_INSTALL_PREFIX)$(call nginx_subdomain,$1).$(DOMAIN_ROOT) + +$(NGINX_OUT_DIR)/$(APP_INSTALL_PREFIX)yavsc%: $(NGINX_OUT_DIR) $(NGINX_TEMPLATE) .$(APP_INSTALL_ENV).env + @cat $(NGINX_TEMPLATE) \ + | DOMAIN="$(call nginx_domain,$*)" \ + BACKEND_SCHEME="$(BACKEND_SCHEME)" \ + HTTP_HOST="$(HTTP_HOST)" \ + BACKEND_PORT="$($*_PORT)" \ + QUIC_OPTS="$(if $(filter Org,$*),reuseport,)" \ + envsubst '$$DOMAIN $$BACKEND_SCHEME $$HTTP_HOST $$BACKEND_PORT $$QUIC_OPTS' > $@ + @echo Created nginx vhost: $@ + +generate-nginx: $(addprefix $(NGINX_OUT_DIR)/$(APP_INSTALL_PREFIX)yavsc,$(NGINX_HOSTS)) + +generate-nginx-prod: + make generate-nginx APP_INSTALL_ENV=production APP_INSTALL_PREFIX= + +$(NGINX_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 new file mode 100644 index 000000000..f2ea5a156 --- /dev/null +++ b/contrib/README.nginx.md @@ -0,0 +1,130 @@ +# Vhosts nginx — génération par `make generate-nginx` + +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/`). + +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`). + +## Génération + +```bash +cd contrib + +# Preprod (défaut) -> generated/nginx/preprod-yavsc{Org,Blogs,Api} +make generate-nginx + +# Production -> generated/nginx/yavsc{Org,Blogs,Api} +make generate-nginx-prod +# ou manuellement : +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). + +| 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`). + +## 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) +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 +# 1. Module h3 présent +nginx -V 2>&1 | grep -o 'http_v3_module' + +# 2. UDP 443 ouvert +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) +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. + +## 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** : + +```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 +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 diff --git a/contrib/template.appsettings-api.json b/contrib/template.appsettings-api.json new file mode 100644 index 000000000..d6412777c --- /dev/null +++ b/contrib/template.appsettings-api.json @@ -0,0 +1,32 @@ +{ + "Site": { + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": ["api"], + "CorsAllowedOrigins": [ + "https://preprod-blogs.$DOMAIN" + ], + "Bills": "bills", + "TempDir": "temp" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:92" + }, + "Https": { + "Url": "https://localhost:93", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-api.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-api.$DOMAIN/privkey.pem" + } + } + } + } +} diff --git a/contrib/template.appsettings-blogs.json b/contrib/template.appsettings-blogs.json new file mode 100644 index 000000000..a3efb762a --- /dev/null +++ b/contrib/template.appsettings-blogs.json @@ -0,0 +1,39 @@ +{ + "Site": { + "Title": "Yavsc Blogs", + "Description": "A collection of blogs about software development, technology, and programming.", + "Keywords": "software development, technology, programming, coding, blogs", + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": ["blogs"], + "CorsAllowedOrigins": [ + "https://preprod-api.$DOMAIN" + ], + "Blog": "blog" + }, + "ConnectionStrings": { + "YavscConnection": "Server=localhost;Port=5432;Database=lame-db-name;Username=lame-user-name;Password=lame-password;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Authentication": "Debug" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:90" + }, + "Https": { + "Url": "https://localhost:91", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-blogs.$DOMAIN/privkey.pem" + } + } + } + } +} diff --git a/contrib/template.appsettings-org.json b/contrib/template.appsettings-org.json new file mode 100644 index 000000000..dbbfdb525 --- /dev/null +++ b/contrib/template.appsettings-org.json @@ -0,0 +1,73 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "AllowedHosts": "*", + "Site": { + "Title": "yavsc", + "Slogan": "Yavsc!", + "StyleSheet": "/css/default.css", + "Authority": "https://preprod-yavsc.$DOMAIN", + "Audience": [ + "blogs", + "api" + ], + "ExternalUrl": "https://preprod-yavsc.$DOMAIN", + "ApiUrl": "https://preprod-api.$DOMAIN", + "CorsAllowedOrigins": [ + "https://preprod-blogs.$DOMAIN", + "https://preprod-api.$DOMAIN" + ], + "Banner": "/images/banner-dewang.jpg", + "Owner": { + "Name": "Paul Schneider", + "EMail": "paul@pschneider.fr" + }, + "Admin": { + "Name": "Paul Schneider", + "EMail": "paul@pschneider.fr" + }, + "Quota": 200000000, + "Blog": "blog", + "Avatars": "avatars", + "Bills": "bills", + "TempDir": "temp", + "DataDir": "data", + "GitRepository": "git" + }, + "Smtp": { + "Host": "mercure.pschneider.fr", + "Port": 465, + "SenderName": "Paul Schneider", + "SenderEmail": "paul@pschneider.fr", + "UserName": "paul", + "Password": "$SMTP_PASSWORD" + }, + "DataProtection": { + "Keys": { + "Dir": "DataProtection" + }, + "RSAParamFile": "RSA-Params.json", + "ExpiresInHours": 168 + }, + "Moderation": { + "AutoApproveThreshold": 0.7, + "AutoRejectThreshold": 0.9 + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://localhost:88" + }, + "Https": { + "Url": "https://localhost:89", + "Certificate": { + "Path": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/fullchain.pem", + "KeyPath": "/etc/letsencrypt/live/preprod-yavsc.$DOMAIN/privkey.pem" + } + } + } + } +} diff --git a/contrib/template.vhost b/contrib/template.vhost new file mode 100644 index 000000000..75e48c14a --- /dev/null +++ b/contrib/template.vhost @@ -0,0 +1,44 @@ +# 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. +# +# Variables substituées par envsubst (allowlist) : +# DOMAIN BACKEND_SCHEME HTTP_HOST BACKEND_PORT QUIC_OPTS +# 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.) +server { + listen 80; + server_name $DOMAIN; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen 443 quic $QUIC_OPTS; # HTTP/3 (UDP 443) — reuseport sur un seul vhost + http2 on; # HTTP/2 sur TCP + 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; + + 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 + + location / { + proxy_pass $BACKEND_SCHEME://$HTTP_HOST:$BACKEND_PORT; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 100s; + proxy_buffering off; + } + include server-status.conf; +} \ No newline at end of file