Files
benevoles/deploy
AdamThomasandClaude Sonnet 5 89e74e5ec7 Revue de sécurité avant ouverture au public
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
2026-09-16 08:14:00 +02:00
..

Déploiement

Le pipeline .gitlab-ci.yml se déclenche sur chaque push :

  1. tests (backend : ruff + pytest + alembic check ; frontend : typecheck + lint + build + smoke) ;
  2. sur main seulement : build des images backend et frontend, poussées sur le registre de conteneurs du projet GitLab ;
  3. copie de deploy/docker-compose.prod.yml sur le VPS par SSH ;
  4. 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 en ENVIRONMENT=production sinon (app/config.py:check_production_secrets).
  • EMAIL_ENABLED=true avec 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 via python -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/login ni /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.

  1. Sur GitLab, lancer le pipeline sur main normalement (Push → CI/CD → Pipelines, ou un simple commit). Le job build-frontend échouera ou produira une image sans édition tant que VITE_EDITION_ID n'existe pas : c'est attendu, on l'ignore pour l'instant.
  2. Une fois build-backend et deploy passés, db et backend tournent 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.