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.
441 lines
13 KiB
Markdown
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
|
|
```
|