Déploiement WSGI minimal¶
Cette page documente le chemin WSGI minimal pour exposer une application
Forge en production, derrière un serveur WSGI externe (Gunicorn) et un
reverse proxy (Caddy ou Nginx).
python app.py n'est pas pour la production publique
Le serveur ThreadingHTTPServer lancé par python app.py est conçu
pour le développement local, les tests et les démonstrations. Il ne
gère pas correctement la concurrence à grande échelle, le keep-alive,
les timeouts ou la compression. Pour une exposition publique,
utiliser obligatoirement le chemin WSGI documenté ci-dessous, derrière
un reverse proxy.
Voir aussi : Guide de déploiement et
Sécurité en production.
1. Architecture cible¶
flowchart LR
I(["Internet<br/>HTTPS :443"]) -->|"TLS terminé"| P["Reverse proxy<br/>Caddy / Nginx"]
P -->|"HTTP local"| G["Gunicorn<br/>workers Python"]
G -->|"WSGI"| F["Forge<br/>create_configured_wsgi_app()"]
F -->|"SQL"| M[("MariaDB<br/>:3306")]
Trois responsabilités sont séparées :
- Reverse proxy : TLS, fichiers statiques,
X-Real-IP, etStrict-Transport-Security(HSTS), voir §4.1 Headers de sécurité ci-dessous. - Gunicorn : pool de workers Python, gestion du cycle de vie.
- Forge : dispatch des routes via le callable WSGI. Depuis
WSGI-SECURITY-HEADERS-001, Forge pose lui-même le socle des autres headers de sécurité (X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy,Content-Security-Policy) sur toutes les réponses WSGI.
2. Fichier wsgi.py applicatif¶
À placer à la racine du projet applicatif Forge :
# wsgi.py
from core.app.wsgi import create_configured_wsgi_app
application = create_configured_wsgi_app()
La factory create_configured_wsgi_app() :
- charge la même configuration que
python app.py(viacore.app.app_factory.build_application) ; - applique
forge.configure(...)avec toutes les variables d'environnement
(dontAPP_TRUSTED_PROXIES) ; - enregistre le renderer Jinja2 ;
- charge le router applicatif depuis
APP_ROUTES_MODULE; - émet une fois, à la construction, les
avertissements production,
jamais à chaque requête.
3. Lancement Gunicorn¶
Forge n'embarque pas Gunicorn : c'est une dépendance à installer
séparément côté projet applicatif.
Notes :
- Gunicorn écoute uniquement sur la boucle locale (
127.0.0.1), le reverse
proxy s'occupe d'exposer HTTPS publiquement ; - pour un démarrage type production, ajouter
--workers <N>adapté au CPU
disponible. Voir la note multi-worker en §7.
python app.py refuse de démarrer en prod sur une interface publique
Depuis APP-PY-PROD-HOST-GUARD-001, python app.py refuse de démarrer
quand APP_ENV=prod ET APP_HOST cible une interface publique
(0.0.0.0, ::, [::]). Le serveur direct reste un outil de
développement, la production publique doit passer par
WSGI + Gunicorn + reverse proxy (cette page). Les hôtes locaux
(127.0.0.1, localhost, ::1) restent autorisés en prod pour
permettre les tests de validation locale.
4. Reverse proxy¶
Caddy (recommandé pour la simplicité TLS)¶
Nginx (variante équivalente)¶
server {
listen 443 ssl;
server_name forgemvc.example;
# ... ssl_certificate / ssl_certificate_key ...
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Les fichiers statiques (/static/...) et les médias (/media/...) peuvent
être servis directement par le reverse proxy pour soulager Gunicorn, voir
§7.
4.1 Headers de sécurité et HSTS¶
Depuis WSGI-SECURITY-HEADERS-001, le chemin WSGI applique automatiquement
le même socle de headers que python app.py :
| Header | Valeur | Source |
|---|---|---|
X-Frame-Options |
DENY |
Forge (WSGI) |
X-Content-Type-Options |
nosniff |
Forge (WSGI) |
Referrer-Policy |
strict-origin-when-cross-origin |
Forge (WSGI) |
Permissions-Policy |
camera=(), microphone=(), geolocation=(), payment=() |
Forge (WSGI) |
Content-Security-Policy |
default-src 'self'; … |
Forge (WSGI) |
Strict-Transport-Security (HSTS) |
max-age=31536000; includeSubDomains |
Reverse proxy (déploiement standard) ou Forge si wsgi.url_scheme == "https" |
Tous ces headers sont posés en setdefault via
core/security/headers.py :
une route applicative qui définit explicitement un de ces headers (response.headers["Content-Security-Policy"] = "..." par exemple) garde la main.
HSTS, décision conservatrice WSGI. Forge ne pose HSTS que lorsque la
requête a réellement atteint Forge en TLS (wsgi.url_scheme == "https").
Dans le déploiement standard ci-dessus (reverse proxy qui termine TLS,
Forge écoute en HTTP local sur 127.0.0.1:8000), wsgi.url_scheme vaut
http côté Forge, c'est donc au reverse proxy d'ajouter Strict-Transport-Security.
Exemples de configuration :
forgemvc.example {
reverse_proxy 127.0.0.1:8000 {
header_up X-Real-IP {remote_host}
}
# Caddy émet HSTS automatiquement quand TLS est actif (header `Strict-Transport-Security`).
}
server {
listen 443 ssl;
server_name forgemvc.example;
# ... ssl_certificate / ssl_certificate_key ...
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Cette répartition est protégée par
tests/test_wsgi_security_headers_001.py.
5. APP_TRUSTED_PROXIES et X-Real-IP¶
Sans configuration explicite, Forge ignore X-Real-IP et utilise
toujours l'adresse IP observée au niveau du socket TCP. Pour activer la
résolution de l'IP réelle du client derrière un reverse proxy, déclarer la
ou les IPs de confiance :
ou pour plusieurs proxies (espaces tolérés) :
Règles :
- vide par défaut →
X-Real-IPtoujours ignoré ; - liste séparée par virgules, espaces tolérés ;
- comparaison IP exacte, pas de notation CIDR ;
- pas de wildcard ;
0.0.0.0n'a aucune signification spéciale (il ne couvre que0.0.0.0) ;X-Real-IPest ignoré si la requête arrive depuis une IP non listée ;- une valeur invalide dans
X-Real-IPest ignorée, Forge retombe sur
l'IP du socket.
Ticket de référence : HTTP-TRUSTED-PROXY-IP-001.
6. Warnings production au démarrage¶
create_configured_wsgi_app() émet, une seule fois, à la construction
de l'application, jamais par requête, un avertissement si Forge est
configuré en APP_ENV=prod avec un store de session mémoire :
AVERTISSEMENT-PROD - Forge tourne en APP_ENV=prod avec stockage mémoire.
* Sessions : MemorySessionStore est volatile et mono-processus.
* Rate-limit (login, uploads) : compteurs en mémoire non partagés.
Tolérée pour développement/test, cette configuration est fragile en
production. Configurer un session store partagé avant exposition
publique (ex. forge.configure(session_store=FileSessionStore(...))).
Pour silencer le warning dans les tests :
Pour rediriger le warning vers un logger applicatif :
import logging
application = create_configured_wsgi_app(
logger=logging.getLogger("my_app.startup"),
)
Tickets de référence : AUTH-RATE-LIMIT-PROD-WARNING-001,
WSGI-PROD-WARNINGS-001.
7. Limites actuelles en production¶
Ce guide est un socle minimal, pas une recette d'exploitation
complète. Les limites suivantes restent à la charge de l'opérateur :
- Session store mémoire (
MemorySessionStore) : volatile au
redémarrage, mono-processus. UtiliserFileSessionStoreou
MariaDbSessionStore(voir ADR-002)
viaforge.configure(session_store=...). FileSessionStore: utilisable, mais reste fragile en multi-worker
(pas de verrou partagé strict). Pour un déploiement multi-worker fiable,
privilégierMariaDbSessionStore.- Rate-limits login/upload encore en mémoire : compteurs non partagés
entre workers Gunicorn. Chaque worker tient son propre compteur, donc la
limite effective est multipliée par le nombre de workers : avec le défaut
login de 5 tentatives par fenêtre de 60 s (core/auth/rate_limit.py,
LOGIN_MAX_ATTEMPTS/LOGIN_RATE_LIMIT_WINDOW), un déploiement àNworkers
tolère jusqu'àN × 5tentatives par fenêtre, l'attaquant étant réparti sur
les workers par le proxy. La protection reste utile mais n'est pas
distribuée. Pour un rate-limit effectif en multi-worker, brancher un backend
partagé (table MariaDB ou Redis) qui centralise le comptage des tentatives ;
c'est à la charge de l'application (hors périmètre du noyau, principe 8). - Anti-rejeu MFA (TOTP) en mémoire : l'état anti-rejeu de
forge-mvc-mfa
est propre à chaque worker ; un code TOTP intercepté peut être rejoué sur un
autre worker. Non distribué dans la série 1.0.0. - Multi-worker : Forge émet déjà un avertissement supplémentaire au
démarragepython app.pysiWEB_CONCURRENCY > 1ou si
SERVER_SOFTWAREcontientgunicorn/uwsgi. Lire ce warning au
premier démarrage Gunicorn. - Fichiers statiques (
/static/...) : faire servir directement par le
reverse proxy, plus rapide et plus sûr qu'un dispatch Python. - Médias (
/media/...) : à cadrer selon l'application,app.py
fournitserve_media_filemais le chemin WSGI minimal ne le gère pas
automatiquement. - HTTPS : à terminer côté reverse proxy. Le pipeline Gunicorn ↔
Forge reste en HTTP local sur127.0.0.1. - Pas de support
X-Forwarded-For: seulX-Real-IPest honoré, et
uniquement derrière un proxy de confiance. - Pas de notation CIDR pour
APP_TRUSTED_PROXIES: seules les IPs
exactes sont acceptées. - Aucune génération automatique du fichier
wsgi.pyapplicatif :
c'est à l'utilisateur de le créer (voir §2).
Pour une vue d'ensemble des limites de production hors WSGI, voir le
futur ticket DOCS-PRODUCTION-LIMITS-001.