Files
benevoles/deploy/README.md
T
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

164 lines
7.0 KiB
Markdown

# Déploiement
Le pipeline [`.gitlab-ci.yml`](../.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
```bash
# 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)
```bash
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
```bash
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 :
```bash
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`](./Caddyfile.benevoles) à son `Caddyfile` (ou au
fichier qu'il importe pour les sites), avec le vrai nom de domaine, puis :
```bash
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.