Files
benevoles/deploy
AdamThomasandClaude Sonnet 5 0ede409841 Limiteur de tentatives sur /auth/login et /auth/magic-link
Suite de la revue de sécurité (commit précédent) : ni /auth/login (mot
de passe COORDINATION) ni /auth/magic-link n'avaient de limite de
tentatives — aucun verrou de compte, aucun throttling, avant ouverture
au public.

`app/auth/rate_limit.py` : limiteur en mémoire (un seul processus
uvicorn, cf. Dockerfile), à créneau glissant, par clé (édition, email)
plutôt que par IP — le VPS ne transmet pas encore l'IP réelle du
client à travers Caddy, et le risque réel ici est de deviner le mot de
passe d'un compte connu (peu de comptes protégés par mot de passe),
pas une attaque distribuée sur des emails au hasard.

- `/auth/login` : 5 tentatives/minute → 429 `error.too_many_requests`.
- `/auth/magic-link` : 3 demandes/5 min (évite aussi le spam d'emails
  vers une victime ou vers le SMTP).

Fixture `_reset_rate_limiters` (autouse) pour ne pas faire fuiter l'état
d'un test à l'autre. Testé : dépassement, indépendance des clés,
expiration de la fenêtre, scénarios bout en bout sur les deux
endpoints.

Non inclus dans ce commit (édités en parallèle par l'utilisateur, laissés
en l'état) : backend/app/seed.py, frontend/tailwind.config.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
2026-09-16 08:19:27 +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.
  • /auth/login et /auth/magic-link sont limités en tentatives par email (5/min et 3/5 min) — voir app/auth/rate_limit.py. Limité par email et non par IP (le VPS ne transmet pas encore l'IP réelle du client à travers Caddy) : suffisant contre la force brute sur un compte connu, pas contre une attaque distribuée sur de nombreux emails différents.

É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.