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
7.0 KiB
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./auth/loginet/auth/magic-linksont limités en tentatives par email (5/min et 3/5 min) — voirapp/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.
- 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.