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

162 lines
6.8 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.
- 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
```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.