Audit de l'application avant sa première publication sur internet. Correctifs immédiats, peu coûteux et sans changement de comportement en dev : - Le backend refuse de démarrer en `ENVIRONMENT=production` si `JWT_SECRET` est vide, trop court ou égal à la valeur par défaut versionnée dans `.env.example` — sinon n'importe qui pourrait forger un jeton de session valide (y compris pour un compte COORDINATION) sans jamais s'authentifier. Testé (`test_config.py`). - Avertissement au démarrage si `EMAIL_ENABLED=false` en production : dans ce cas le jeton de lien magique est journalisé en clair (confort de dev, cf. `api/auth.py`) — un identifiant de connexion en clair dans les logs du conteneur en public serait un vrai problème. - `frontend/nginx.conf` : plus de journal d'accès (la route `/connexion?token=…` y ferait apparaître le jeton de lien magique en clair), en-têtes `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy` (empêche aussi la fuite de ce même jeton vers Google Fonts via l'en-tête Referer), `server_tokens off`. - `deploy/Caddyfile.benevoles` : mêmes en-têtes côté reverse proxy public, plus `Strict-Transport-Security`. - `deploy/README.md` : check-list sécurité avant ouverture (secret, SMTP, ne jamais mettre en production la base de démo `app.seed` — le mot de passe du compte coordination y est versionné en clair —, limites connues comme l'absence de rate-limiting sur `/auth/*`). Points vérifiés sans modification nécessaire : autorisation par rôle et cloisonnement par édition cohérents sur tous les endpoints (`owned_or_404` + vérification d'édition partout), mots de passe bcryptés, jetons de lien magique à usage unique haché en base (256 bits d'entropie), pas de PII/jeton dans les logs applicatifs (RGPD déjà en place), pas d'injection de gabarit dans les emails (substitution sur liste blanche, pas de `.format()`), pas d'injection d'en-tête SMTP (`EmailMessage` du socle Python), pas de `dangerouslySetInnerHTML` côté front, jeton JWT en `Authorization: Bearer` (pas de cookie ⇒ pas de CSRF), `.env` jamais versionné, dépendances à jour. Restent des choix à trancher avec l'utilisateur plutôt qu'à décider seul (compromis effort/risque) : rate-limiting sur les endpoints d'authentification, désactivation de `/docs` en production, conteneurs non-root. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
Déploiement
Le pipeline .gitlab-ci.yml se déclenche sur chaque
push :
- tests (backend :
ruff+pytest+alembic check; frontend :typecheck+lint+build+smoke) ; - sur
mainseulement : build des imagesbackendetfrontend, poussées sur le registre de conteneurs du projet GitLab ; - copie de
deploy/docker-compose.prod.ymlsur le VPS par SSH ; docker compose pull, migration Alembic,up -d.
Le VPS n'expose rien directement : un Caddy déjà installé sur le
serveur (service système, hors de ce dépôt) reçoit le trafic public sur
443 et reverse-proxy vers les deux conteneurs, qui n'écoutent que sur
127.0.0.1.
Avant d'ouvrir au public — check-list sécurité
JWT_SECRET: une vraie valeur générée (openssl rand -hex 32), jamais la valeur par défaut de.env.example. Le backend refuse de démarrer enENVIRONMENT=productionsinon (app/config.py:check_production_secrets).EMAIL_ENABLED=trueavec un SMTP qui fonctionne (docker compose run --rm backend uv run python -m app.email.check <destinataire>pour vérifier) — sinon les liens magiques sont journalisés en clair au lieu d'être envoyés (le backend avertit au démarrage tant que c'est le cas, voir plus bas).- Édition créée via
python -m app.bootstrap(étape 3), jamais viapython -m app.seed: ce dernier crée un compte de démonstration (coordination@lucioles.test) avec un mot de passe versionné dans le dépôt — un vrai identifiant de connexion COORDINATION public si la base de démo devient la base de production. CORS_ORIGINS: sans objet ici tant que le frontend appelle l'API en même origine via/api/*(voir le vhost Caddy, étape 5) — à ne renseigner que si un autre client (domaine séparé, appli mobile) doit appeler l'API directement.- Aucun rate-limiting sur
/auth/loginni/auth/magic-linkà ce stade (pas de verrouillage de compte, pas de throttling) — acceptable pour une petite équipe connue, mais à surveiller si l'URL circule plus largement.
Étape 0 — pousser le dépôt sur GitLab
# Créer d'abord un projet vide sur GitLab (sans README ni .gitignore), puis :
git remote add origin git@gitlab.com:<namespace>/<projet>.git
git push -u origin main
Variables CI/CD à définir (Paramètres → CI/CD → Variables)
| Variable | Description |
|---|---|
SSH_HOST |
Hôte du VPS |
SSH_USER |
Utilisateur SSH |
SSH_KEY |
Clé privée SSH (format OpenSSH) — masquée, sur main seulement |
DEPLOY_PATH |
Dossier du projet sur le VPS (ex. /opt/benevoles) |
VITE_EDITION_ID |
UUID de l'édition de production — à ajouter après l'étape 3 |
CI_REGISTRY, CI_REGISTRY_USER, CI_REGISTRY_PASSWORD et
CI_REGISTRY_IMAGE sont fournies automatiquement par GitLab, rien à
configurer.
La clé publique correspondant à SSH_KEY doit être dans le
~/.ssh/authorized_keys de SSH_USER sur le VPS.
Étape 1 — préparation du VPS (une fois)
mkdir -p /opt/benevoles
cd /opt/benevoles
cat > .env <<'EOF'
POSTGRES_USER=benevoles
POSTGRES_PASSWORD=<mot de passe fort>
POSTGRES_DB=benevoles
JWT_SECRET=<secret fort ; openssl rand -hex 32>
PUBLIC_BASE_URL=https://benevoles.mon-domaine.fr
# Emails (récapitulatifs de planning + liens de connexion). Laisser
# EMAIL_ENABLED=false tant que le SMTP n'est pas prêt : les emails sont alors
# seulement journalisés — un jeton de lien magique en clair dans les logs du
# conteneur permet de se connecter à la place de n'importe qui, donc à
# repasser à true avant l'ouverture au public (le backend affiche un
# avertissement au démarrage tant que c'est le cas).
EMAIL_ENABLED=true
SMTP_HOST=smtp.mon-domaine.fr
SMTP_PORT=587
SMTP_USER=no-reply@mon-domaine.fr
SMTP_PASSWORD=<mot de passe SMTP>
SMTP_STARTTLS=true
SMTP_SSL=false
SMTP_FROM=Festival <no-reply@mon-domaine.fr>
EOF
Le pipeline dépose docker-compose.prod.yml dans ce dossier et l'utilise tel
quel — il n'y a rien d'autre à y copier.
Étape 2 — premier déploiement (backend + base seuls)
Le frontend a besoin de l'UUID d'une édition pour être construit (voir plus bas) : le tout premier déploiement se fait donc en deux temps.
- Sur GitLab, lancer le pipeline sur
mainnormalement (Push → CI/CD → Pipelines, ou un simple commit). Le jobbuild-frontendéchouera ou produira une image sans édition tant queVITE_EDITION_IDn'existe pas : c'est attendu, on l'ignore pour l'instant. - Une fois
build-backendetdeploypassés,dbetbackendtournent sur le VPS.
Étape 3 — créer l'édition de production
ssh SSH_USER@SSH_HOST
cd /opt/benevoles
docker compose -f docker-compose.prod.yml run --rm backend \
uv run python -m app.bootstrap \
--name "Festival 2026" --start 2026-07-01 --end 2026-07-04 \
--email coordination@mon-domaine.fr --password <mot-de-passe-solide>
La commande affiche l'UUID de l'édition créée. Récupérer aussi celui-ci à tout moment avec :
docker compose -f docker-compose.prod.yml exec -T db \
psql -U benevoles -tAc "select id from edition order by created_at desc limit 1"
Étape 4 — brancher le frontend sur cette édition
Dans GitLab, ajouter la variable CI/CD VITE_EDITION_ID avec cet UUID, puis
relancer le pipeline sur main (bouton Retry sur le dernier pipeline, ou un
nouveau commit). Cette fois build-frontend construit une image qui pointe
sur la bonne édition, et deploy démarre le service frontend.
VITE_EDITION_ID ne change plus ensuite : elle ne se touche à nouveau que
pour une nouvelle édition (festival de l'année suivante) qu'on voudrait
publier sur ce même domaine.
Étape 5 — le vhost Caddy
Caddy tourne déjà sur ce serveur (service système) : ajouter le bloc
deploy/Caddyfile.benevoles à son Caddyfile (ou au
fichier qu'il importe pour les sites), avec le vrai nom de domaine, puis :
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
Le DNS du domaine doit déjà pointer (enregistrement A/AAAA) vers ce VPS —
Caddy obtient et renouvelle seul le certificat HTTPS (Let's Encrypt) au
premier accès.
Vérifier ensuite que PUBLIC_BASE_URL dans le .env du VPS (étape 1)
correspond bien à https://<ce domaine> — c'est cette valeur qui est utilisée
dans les liens des emails envoyés aux bénévoles.
Déploiements suivants
Un simple push sur main suffit : tests → build → déploiement → migration →
redémarrage, sans aucune intervention sur le VPS ni sur Caddy.