Files
la-voix-du-peuple/DEPLOIEMENT.md
T
billisdead defc07648c Security: close the 9 items from the June review + one found along the way
Findings from the 2026-06-24 security review, now mandatory before this
platform leaves the LAN for a public VPS (see budget/externalization note):

- Timing attack on admin password: hmac.compare_digest instead of !=
- CORS wildcard removed, restricted to ALLOWED_ORIGINS (needed for credentials)
- hCaptcha now fails closed on network error instead of open
- HMAC signatures use the full SHA-256 digest instead of a 64-bit truncation
- Consultation-scoped submissions were missing the consent/cooldown checks
  the global submission route already had
- Admin search capped at 200 chars (ILIKE cost)
- Webhook URLs validated against SSRF (private/loopback/link-local/metadata
  IPs rejected, re-checked at trigger time too, redirects not followed)
- Flood tracker backed by Redis when REDIS_URL is set (shared across Gunicorn
  workers, survives restarts), falls back to in-memory otherwise
- Admin session moved from sessionStorage/Bearer token to an httpOnly+Secure+
  SameSite=Strict cookie; new /api/admin/logout and /api/admin/session routes

Also found while wiring ProxyFix for the cookie's Secure flag: the app never
told Flask about the Nginx reverse proxy, so request.remote_addr saw Nginx's
own IP for every request in production — silently breaking rate limiting,
flood detection, and IP blacklist (everyone would collapse onto one shared
key). Added ProxyFix (1 hop).

CSV exports now stream instead of loading everything into memory. AI prompts
delimit citizen-submitted content explicitly and instruct the model to treat
it as inert data, never instructions, to reduce prompt injection surface.

Docs (DEX.md, SECURITE_ANTI_ABUS.md, README.md, DEPLOIEMENT.md) and
dev-local.sh updated to match.
2026-09-10 09:27:50 +02:00

441 lines
13 KiB
Markdown

# Guide d'auto-hébergement — La Voix du Peuple
## Rocky Linux 9 (ou RHEL 9, AlmaLinux 9)
---
## Architecture
```
Internet
│
▼
[Nginx] ─── /api/* ──► [Gunicorn + Flask] ──► [PostgreSQL]
│ │
└─── /* ──► [React SPA] └──► [Mistral AI API]
(fichiers statiques)
```
- **Frontend** : React + Vite, servi comme fichiers statiques par Nginx
- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx
- **Base de données** : PostgreSQL 15+
- **IA** : Mistral AI (`MISTRAL_API_KEY` requis) ou tout fournisseur compatible OpenAI
> **Note production :** En déploiement exposé à Internet, placer HAProxy devant Nginx pour la
> terminaison TLS. Voir `ARCHITECTURE.md` pour le schéma complet. Dans cette configuration,
> Nginx écoute sur le port 8080 (fichier `deploy/nginx.conf`) et HAProxy forward sur ce port.
> Pour un déploiement standalone sans HAProxy, changez `listen 8080;` en `listen 80;` dans
> `deploy/nginx.conf` et activez HTTPS avec certbot (section 12).
---
## Prérequis
```bash
sudo dnf update -y
sudo dnf install -y git curl wget epel-release
```
---
## 1. Installer les dépendances système
### Python 3.11
```bash
sudo dnf install -y python3.11 python3.11-pip python3.11-devel
python3.11 --version # doit afficher Python 3.11.x
```
### Node.js 20 (LTS)
```bash
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs
node --version # doit afficher v20.x.x
```
### pnpm
```bash
npm install -g pnpm
pnpm --version
```
### PostgreSQL 15
```bash
sudo dnf install -y postgresql15-server postgresql15
sudo /usr/pgsql-15/bin/postgresql-15-setup initdb
sudo systemctl enable --now postgresql-15
```
### Nginx
```bash
sudo dnf install -y nginx
sudo systemctl enable nginx
```
---
## 2. Configurer PostgreSQL
```bash
sudo -u postgres psql
```
Dans psql :
```sql
CREATE USER voixdupeuple WITH PASSWORD 'CHANGEME_MOT_DE_PASSE_FORT';
CREATE DATABASE voixdupeuple OWNER voixdupeuple;
GRANT ALL PRIVILEGES ON DATABASE voixdupeuple TO voixdupeuple;
\q
```
Éditez `/var/lib/pgsql/15/data/pg_hba.conf` — remplacez `ident` par `scram-sha-256` sur
la ligne `127.0.0.1/32` :
```
host all all 127.0.0.1/32 scram-sha-256
```
```bash
sudo systemctl restart postgresql-15
# Test de connexion
psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple -c "\dt"
```
---
## 3. Créer l'utilisateur système
```bash
sudo useradd -r -s /bin/bash -d /opt/voix-du-peuple voixdupeuple
sudo mkdir -p /opt/voix-du-peuple
sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple
```
---
## 4. Cloner le dépôt
```bash
sudo -u voixdupeuple git clone \
https://votre-depot.example.com/voix-du-peuple.git \
/opt/voix-du-peuple
```
---
## 5. Configurer les variables d'environnement
```bash
sudo -u voixdupeuple cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env
sudo nano /opt/voix-du-peuple/.env
```
Valeurs obligatoires à renseigner :
```env
# Base de données
DATABASE_URL=postgresql://voixdupeuple:CHANGEME_MOT_DE_PASSE_FORT@127.0.0.1:5432/voixdupeuple
# IA — Mistral AI (recommandé)
MISTRAL_API_KEY=votre-cle-mistral
# Sécurité — générez avec : python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=VALEUR_ALEATOIRE_LONGUE
# Panel d'administration
ADMIN_SECRET=MOT_DE_PASSE_ADMIN_FORT
```
Sécurisez le fichier :
```bash
sudo chmod 600 /opt/voix-du-peuple/.env
sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple/.env
```
---
## 6. Installer les dépendances Python
```bash
sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple
python3.11 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r artifacts/flask-api/requirements.txt
"
```
---
## 7. Initialiser la base de données
La base de données est initialisée **automatiquement** au premier démarrage du service
(création des tables et migrations incrémentales idempotentes). Aucune action manuelle n'est
requise. Passez directement à l'étape suivante.
---
## 8. Construire le frontend React
```bash
sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple
pnpm install --frozen-lockfile
cd artifacts/voix-du-peuple
pnpm build
"
```
Les fichiers statiques sont générés dans `artifacts/voix-du-peuple/dist/public/`.
Si vous avez configuré `VITE_APP_URL` (URL publique du site, pour le QR code du flyer) ou
`VITE_HCAPTCHA_SITE_KEY`, ajoutez ces variables dans le `.env` **avant** le build — elles
sont intégrées statiquement dans le bundle.
---
## 9. Configurer le service systemd (Gunicorn)
```bash
# Créer le répertoire de logs
sudo mkdir -p /var/log/voix-du-peuple
sudo chown voixdupeuple:voixdupeuple /var/log/voix-du-peuple
# Copier le fichier de service
sudo cp /opt/voix-du-peuple/deploy/voix-du-peuple-api.service \
/etc/systemd/system/voix-du-peuple-api.service
# Activer et démarrer
sudo systemctl daemon-reload
sudo systemctl enable --now voix-du-peuple-api
# Vérifier
sudo systemctl status voix-du-peuple-api
```
Test de l'API :
```bash
curl http://127.0.0.1:8000/api/healthz
# {"status": "ok"}
```
---
## 10. Configurer Nginx
Le fichier `deploy/nginx.conf` configure Nginx en écoute sur le port **8080** (mode
derrière HAProxy). Pour un déploiement standalone sans HAProxy, changez `listen 8080;`
en `listen 80;` avant de copier.
```bash
sudo cp /opt/voix-du-peuple/deploy/nginx.conf \
/etc/nginx/conf.d/voix-du-peuple.conf
# Si déploiement standalone : changer le port d'écoute
sudo sed -i 's/listen 8080;/listen 80;/' /etc/nginx/conf.d/voix-du-peuple.conf
# Tester la configuration
sudo nginx -t
# Recharger Nginx
sudo systemctl reload nginx
```
---
## 11. Ouvrir le pare-feu
```bash
# Standalone (Nginx expose directement HTTP/HTTPS)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
```
SELinux — autoriser Nginx à proxyfier vers Gunicorn :
```bash
sudo setsebool -P httpd_can_network_connect 1
```
---
## 12. (Recommandé) HTTPS avec Let's Encrypt — déploiement standalone
> Ne s'applique qu'en déploiement standalone (Nginx exposé directement). Si HAProxy gère
> le TLS, configurez Let's Encrypt sur HAProxy — voir `ARCHITECTURE.md`.
```bash
sudo dnf install -y certbot python3-certbot-nginx
# Remplacez voix-du-peuple.example.com par votre domaine
sudo certbot --nginx -d voix-du-peuple.example.com \
--email contact@example.com --agree-tos --no-eff-email
# Vérifier le renouvellement automatique
sudo systemctl status certbot-renew.timer
```
---
## Structure du projet
```
voix-du-peuple/
├── artifacts/
│ ├── flask-api/ # Backend Python Flask
│ │ ├── app.py # Application principale + routes API
│ │ ├── ai_agent.py # Agent IA (filtrage, synthèse, check-law)
│ │ ├── legal_framework.py # Prompts ancrés dans le droit (16 sources)
│ │ ├── database.py # Accès PostgreSQL (psycopg2, sans ORM)
│ │ ├── requirements.txt # Dépendances Python
│ │ └── start.sh # Démarrage dev direct (python app.py)
│ └── voix-du-peuple/ # Frontend React + Vite 7
│ ├── src/
│ │ ├── pages/ # home, about, transparence, contributions-brutes,
│ │ │ # consultations, legal-notice, privacy-policy, admin…
│ │ ├── components/ # UI (shadcn/ui + Radix), consent-dialog, navbar…
│ │ ├── lib/ # prompts.ts (verbatim IA), hooks, utils
│ │ └── App.tsx # Routing principal (Wouter)
│ └── vite.config.ts # Config Vite (dev + production)
├── lib/
│ ├── api-spec/ # Spécification OpenAPI + config Orval
│ ├── api-client-react/ # Hooks React Query générés
│ └── api-zod/ # Schémas Zod générés
├── deploy/
│ ├── voix-du-peuple-api.service # Service systemd Gunicorn
│ └── nginx.conf # Configuration Nginx (port 8080, derrière HAProxy)
├── docs/ # Documentation technique (RGPD, sécurité, prompts…)
├── .env.example # Variables d'environnement (modèle commenté)
└── DEPLOIEMENT.md # Ce fichier
```
---
## Variables d'environnement complètes
| Variable | Obligatoire | Description |
|----------|-------------|-------------|
| `DATABASE_URL` | Oui | URL PostgreSQL — `postgresql://user:pass@host:5432/db` |
| `MISTRAL_API_KEY` | Oui* | Clé API Mistral AI (fournisseur par défaut) |
| `OPENAI_API_KEY` | Oui* | Alternative si Mistral non utilisé (compatible OpenAI) |
| `OPENAI_BASE_URL` | Non | Base URL pour fournisseur tiers (Ollama, proxy…) |
| `FILTER_MODEL` | Non | Modèle de filtrage (défaut : `mistral-small-latest`) |
| `SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `mistral-large-latest`) |
| `SECRET_KEY` | Recommandé | Active le cooldown cookie et le consentement RGPD signé HMAC-SHA256 |
| `ADMIN_SECRET` | Recommandé | Mot de passe du panel `/admin` (session via cookie httpOnly) |
| `ALLOWED_ORIGINS` | Recommandé | Domaine(s) public(s) autorisés en CORS — requis pour le cookie de session |
| `REDIS_URL` | Non | Rate limiting persistant entre workers — `redis://localhost:6379/0` |
| `RATE_LIMIT_CONTRIBUTIONS` | Non | Seuil de soumissions (défaut : `5 per minute;3 per hour`) |
| `CONTRIBUTION_COOLDOWN_SECONDS` | Non | Délai inter-soumissions par session (défaut : `240`) |
| `FLOOD_THRESHOLD` | Non | Seuil d'alerte flood en 5 min (défaut : `10`) |
| `IP_BLACKLIST_DAYS` | Non | Durée du blacklist IP (défaut : `30`) |
| `HCAPTCHA_SECRET_KEY` | Non | Active la vérification hCaptcha côté serveur |
| `VITE_HCAPTCHA_SITE_KEY` | Non | Clé publique hCaptcha côté frontend (rebuild requis) |
| `VITE_APP_URL` | Non | URL publique du site — utilisée par le QR code du flyer (rebuild requis) |
*`MISTRAL_API_KEY` est prioritaire. Si absent, `OPENAI_API_KEY` est utilisé. L'un des deux est obligatoire.
---
## Modèles IA
| Rôle | Modèle par défaut | Variable de contrôle |
|------|-------------------|----------------------|
| Filtrage des contributions | `mistral-small-latest` | `FILTER_MODEL` |
| Synthèse collective | `mistral-large-latest` | `SYNTHESIS_MODEL` |
| Vérification cadre légal | même que `FILTER_MODEL` | `FILTER_MODEL` |
Pour changer de modèle, définissez la variable dans `.env` — aucune modification du code n'est nécessaire.
---
## Mise à jour
```bash
sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple
git pull origin main
# Dépendances Python
.venv/bin/pip install -r artifacts/flask-api/requirements.txt
# Rebuild frontend
pnpm install --frozen-lockfile
cd artifacts/voix-du-peuple && pnpm build
"
# Redémarrer le service (les migrations DB s'appliquent automatiquement)
sudo systemctl restart voix-du-peuple-api
sudo systemctl reload nginx
```
---
## Logs et supervision
```bash
# Logs API Flask (journald + fichiers)
sudo journalctl -u voix-du-peuple-api -f
sudo tail -f /var/log/voix-du-peuple/api-error.log
sudo tail -f /var/log/voix-du-peuple/api-access.log
# Logs Nginx
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
# Statut des services
sudo systemctl status voix-du-peuple-api
sudo systemctl status nginx
sudo systemctl status postgresql-15
```
---
## Dépannage fréquent
### L'API répond 502 Bad Gateway
```bash
# Vérifier que Gunicorn tourne
sudo systemctl status voix-du-peuple-api
# Vérifier SELinux
sudo setsebool -P httpd_can_network_connect 1
# Tester Gunicorn directement
curl http://127.0.0.1:8000/api/healthz
```
### Erreur "could not connect to server" (PostgreSQL)
```bash
sudo systemctl status postgresql-15
psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple -c "\dt"
# Vérifier DATABASE_URL dans .env
```
### Erreur Mistral AI API
```bash
# Vérifier la clé dans .env
grep MISTRAL_API_KEY /opt/voix-du-peuple/.env
# Tester l'accès à l'API Mistral
curl https://api.mistral.ai/v1/models \
-H "Authorization: Bearer $(grep MISTRAL_API_KEY /opt/voix-du-peuple/.env | cut -d= -f2)"
```
### Le frontend affiche une page blanche
```bash
# Vérifier que le build existe
ls /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public/
# Vérifier les droits
sudo chown -R voixdupeuple:voixdupeuple \
/opt/voix-du-peuple/artifacts/voix-du-peuple/dist/
```
### Les variables VITE_* ne sont pas prises en compte
Les variables préfixées `VITE_` sont intégrées au moment du build, pas à l'exécution.
Après toute modification de `VITE_APP_URL` ou `VITE_HCAPTCHA_SITE_KEY`, relancez le build :
```bash
cd /opt/voix-du-peuple/artifacts/voix-du-peuple && pnpm build
```