refactor(adr-023/024/025): retirer exemples de code et scripts
Suppression de tous les exemples de code pour garder uniquement les descriptions techniques : ADR-023 (Architecture Modération) : - Diagramme Mermaid → description flux textuelle - Exemples SQL/Redis → description workflow - Interface Go → description abstraction - Dépendances → liste concise ADR-024 (Monitoring et Observabilité) : - Diagramme Mermaid → architecture textuelle - Exemples PromQL → description métriques - Config YAML alertes → liste alertes avec seuils - Commandes bash WAL-E → description backup - Runbooks → étapes sans commandes ADR-025 (Sécurité et Secrets) : - Diagramme Mermaid → flux secrets textuel - Commandes bash Vault → description process - Code Go encryption → architecture encryption - Schéma SQL → contraintes textuelles - Config Nginx → configuration TLS - Code Go rate limiting → paramètres middleware ADR restent 100% techniques et complets sans code concret. Cohérence avec ADR-022 (même approche). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,79 +28,35 @@ Architecture hybride **humain + IA** avec file d'attente intelligente.
|
|||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
```mermaid
|
**Flux de traitement** :
|
||||||
graph TB
|
1. **Client** (App Mobile/Web) → Signalement utilisateur
|
||||||
subgraph Client["App Mobile/Web"]
|
2. **API Backend** (Fiber) → Endpoint `/moderation/report`
|
||||||
Report["Signalement utilisateur"]
|
3. **Queue PostgreSQL** → LISTEN/NOTIFY pour dispatch asynchrone
|
||||||
end
|
4. **Worker Go** → Goroutine de traitement (transcription + analyse)
|
||||||
|
5. **IA Self-hosted** → Whisper large-v3 (transcription) + distilbert/roberta (NLP)
|
||||||
subgraph Backend["Backend Go"]
|
6. **Cache Redis** → Sorted Sets pour priorisation temps réel
|
||||||
API["API Fiber<br/>/moderation/report"]
|
7. **Dashboard React** → Interface modérateurs avec Wavesurfer.js (player audio)
|
||||||
Queue["PostgreSQL Queue<br/>LISTEN/NOTIFY"]
|
8. **Stockage** → PostgreSQL (signalements + logs audit) + Redis (cache priorisation)
|
||||||
Worker["Worker Go<br/>(transcription + NLP)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph AI["IA Self-hosted"]
|
|
||||||
Whisper["Whisper large-v3<br/>(transcription)"]
|
|
||||||
NLP["distilbert<br/>(sentiment + haine)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Moderation["Modération Dashboard"]
|
|
||||||
Dashboard["React Dashboard"]
|
|
||||||
Player["Wavesurfer.js<br/>(lecture audio)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Storage["Stockage"]
|
|
||||||
DB["PostgreSQL<br/>(signalements + logs)"]
|
|
||||||
Redis["Redis<br/>(priorisation + cache)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
Report --> API
|
|
||||||
API --> Queue
|
|
||||||
Queue --> Worker
|
|
||||||
Worker --> Whisper
|
|
||||||
Whisper --> NLP
|
|
||||||
NLP --> Redis
|
|
||||||
Worker --> DB
|
|
||||||
Dashboard --> Player
|
|
||||||
Dashboard --> Redis
|
|
||||||
Dashboard --> DB
|
|
||||||
|
|
||||||
classDef clientStyle fill:#e3f2fd,stroke:#1565c0
|
|
||||||
classDef backendStyle fill:#fff3e0,stroke:#e65100
|
|
||||||
classDef aiStyle fill:#f3e5f5,stroke:#6a1b9a
|
|
||||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
|
||||||
|
|
||||||
class Client,Report clientStyle
|
|
||||||
class Backend,API,Queue,Worker backendStyle
|
|
||||||
class AI,Whisper,NLP aiStyle
|
|
||||||
class Storage,DB,Redis storageStyle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Workflow de Traitement
|
### Workflow de Traitement
|
||||||
|
|
||||||
1. **Réception signalement** :
|
1. **Réception signalement** :
|
||||||
```sql
|
- Insertion en base PostgreSQL (table `moderation_reports`)
|
||||||
INSERT INTO moderation_reports (content_id, user_id, category, comment)
|
- Notification asynchrone via PostgreSQL NOTIFY
|
||||||
VALUES ($1, $2, $3, $4)
|
|
||||||
RETURNING id;
|
|
||||||
|
|
||||||
NOTIFY moderation_queue, 'report_id:{id}';
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Worker asynchrone** (goroutine) :
|
2. **Worker asynchrone** (goroutine) :
|
||||||
- Écoute `LISTEN moderation_queue`
|
- Écoute queue PostgreSQL (LISTEN/NOTIFY)
|
||||||
- Télécharge audio depuis stockage S3/local
|
- Téléchargement audio depuis stockage S3/local
|
||||||
- Transcription Whisper (1-10 min selon durée)
|
- Transcription audio via Whisper large-v3 (1-10 min selon durée)
|
||||||
- Analyse NLP : score confiance 0-100%
|
- Analyse NLP : score confiance 0-100% (distilbert + roberta)
|
||||||
- Calcul priorité : `(score_IA × 0.7) + (nb_signalements × 0.2) + (fiabilité_signaleur × 0.1)`
|
- Calcul priorité selon formule : `(score_IA × 0.7) + (nb_signalements × 0.2) + (fiabilité_signaleur × 0.1)`
|
||||||
- Insertion Redis Sorted Set : `ZADD moderation:priority {priority} {report_id}`
|
- Insertion dans Redis Sorted Set pour priorisation
|
||||||
|
|
||||||
3. **Dashboard modérateurs** :
|
3. **Dashboard modérateurs** :
|
||||||
- Poll Redis Sorted Set : `ZREVRANGE moderation:priority 0 19` (top 20)
|
- Récupération signalements priorisés depuis Redis (top 20 par page)
|
||||||
- Affichage liste priorisée avec transcription, waveform, historique créateur
|
- Affichage : transcription, waveform audio, historique créateur
|
||||||
- Actions : Approuver, Rejeter, Escalade (shortcuts clavier A/R/E)
|
- Actions disponibles : Approuver, Rejeter, Escalade (shortcuts clavier A/R/E)
|
||||||
- Logs audit PostgreSQL (conformité DSA)
|
- Logs audit PostgreSQL pour traçabilité (conformité DSA)
|
||||||
|
|
||||||
## Alternatives considérées
|
## Alternatives considérées
|
||||||
|
|
||||||
@@ -136,14 +92,7 @@ graph TB
|
|||||||
|
|
||||||
- **Performance MVP** : Suffisant jusqu'à 1000 signalements/jour (~0.7/min)
|
- **Performance MVP** : Suffisant jusqu'à 1000 signalements/jour (~0.7/min)
|
||||||
- **Simplicité** : Pas de broker externe, transactions ACID
|
- **Simplicité** : Pas de broker externe, transactions ACID
|
||||||
- **Migration facile** : Abstraction interface `ModerationQueue` → swap vers Redis Streams si besoin
|
- **Migration facile** : Abstraction via interface `ModerationQueue` → swap vers Redis Streams si besoin (méthodes : Enqueue, Listen)
|
||||||
|
|
||||||
```go
|
|
||||||
type ModerationQueue interface {
|
|
||||||
Enqueue(ctx context.Context, reportID int64) error
|
|
||||||
Listen(ctx context.Context) (<-chan int64, error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Whisper large-v3 self-hosted
|
### Whisper large-v3 self-hosted
|
||||||
|
|
||||||
@@ -176,24 +125,16 @@ type ModerationQueue interface {
|
|||||||
|
|
||||||
### Dépendances
|
### Dépendances
|
||||||
|
|
||||||
```go
|
**Backend Go** :
|
||||||
// backend/go.mod
|
- `gofiber/fiber/v3` : API Dashboard
|
||||||
require (
|
- `jackc/pgx/v5` : PostgreSQL + LISTEN/NOTIFY
|
||||||
github.com/gofiber/fiber/v3 latest // API Dashboard
|
- `redis/rueidis` : Cache priorisation
|
||||||
github.com/jackc/pgx/v5 latest // PostgreSQL + LISTEN/NOTIFY
|
- Whisper : via Python subprocess ou go-whisper bindings
|
||||||
github.com/redis/rueidis latest // Cache priorisation
|
|
||||||
// Whisper via Python subprocess ou go-whisper bindings
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend Dashboard** :
|
**Frontend Dashboard** :
|
||||||
```json
|
- `react` : Framework UI
|
||||||
{
|
- `@tanstack/react-table` : Tables performantes
|
||||||
"react": "^18.3.0",
|
- `wavesurfer.js` : Player audio avec waveform
|
||||||
"@tanstack/react-table": "^8.10.0",
|
|
||||||
"wavesurfer.js": "^7.0.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Métriques de Succès
|
## Métriques de Succès
|
||||||
|
|
||||||
|
|||||||
@@ -31,86 +31,42 @@ Stack **Prometheus + Grafana + Loki** self-hosted avec alerting multi-canal.
|
|||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
```mermaid
|
**Services surveillés** :
|
||||||
graph TB
|
- Backend Go API (métriques Fiber)
|
||||||
subgraph Services["Services RoadWave"]
|
- PostgreSQL (pg_exporter)
|
||||||
API["Backend Go API<br/>(Fiber metrics)"]
|
- Redis (redis_exporter)
|
||||||
DB["PostgreSQL<br/>(pg_exporter)"]
|
- Zitadel (endpoint metrics)
|
||||||
Redis["Redis<br/>(redis_exporter)"]
|
|
||||||
Zitadel["Zitadel<br/>(metrics endpoint)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Monitoring["Stack Monitoring"]
|
**Stack Monitoring** :
|
||||||
Prom["Prometheus<br/>(scrape + TSDB)"]
|
- **Prometheus** : Collecte métriques (scrape), stockage TSDB 15j rétention
|
||||||
Grafana["Grafana<br/>(dashboards)"]
|
- **Grafana** : Visualisation dashboards
|
||||||
Loki["Loki<br/>(logs aggregation)"]
|
- **Loki** : Agrégation logs (chunks compressés, 7j rétention)
|
||||||
Alert["Alertmanager<br/>(routing)"]
|
- **Alertmanager** : Routing alertes multi-canal
|
||||||
Uptime["Uptime Kuma<br/>(external checks)"]
|
- **Uptime Kuma** : Checks HTTP externes, SSL monitoring
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Notifications["Alerting"]
|
**Alerting** :
|
||||||
Email["Email (Brevo)"]
|
- Email (Brevo) : asynchrone, faible intrusivité
|
||||||
Slack["Webhook Slack/Discord"]
|
- Webhook (Slack/Discord) : temps réel, on-call
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Storage["Stockage"]
|
**Stockage** :
|
||||||
PromStorage["Prometheus TSDB<br/>(15j retention)"]
|
- Prometheus TSDB : métriques 15j
|
||||||
LokiStorage["Loki Chunks<br/>(7j retention)"]
|
- Loki chunks : logs 7j
|
||||||
Backups["Backups PostgreSQL<br/>(S3 OVH)"]
|
- Backups PostgreSQL : WAL-E continuous vers S3 OVH
|
||||||
end
|
|
||||||
|
|
||||||
API --> Prom
|
|
||||||
DB --> Prom
|
|
||||||
Redis --> Prom
|
|
||||||
Zitadel --> Prom
|
|
||||||
|
|
||||||
API -.->|logs stdout| Loki
|
|
||||||
Prom --> Grafana
|
|
||||||
Loki --> Grafana
|
|
||||||
Prom --> Alert
|
|
||||||
|
|
||||||
Alert --> Email
|
|
||||||
Alert --> Slack
|
|
||||||
|
|
||||||
Uptime -.->|external HTTP checks| API
|
|
||||||
Uptime --> Alert
|
|
||||||
|
|
||||||
Prom --> PromStorage
|
|
||||||
Loki --> LokiStorage
|
|
||||||
DB -.->|WAL-E continuous| Backups
|
|
||||||
|
|
||||||
classDef serviceStyle fill:#e3f2fd,stroke:#1565c0
|
|
||||||
classDef monitoringStyle fill:#fff3e0,stroke:#e65100
|
|
||||||
classDef notifStyle fill:#f3e5f5,stroke:#6a1b9a
|
|
||||||
classDef storageStyle fill:#e8f5e9,stroke:#2e7d32
|
|
||||||
|
|
||||||
class Services,API,DB,Redis,Zitadel serviceStyle
|
|
||||||
class Monitoring,Prom,Grafana,Loki,Alert,Uptime monitoringStyle
|
|
||||||
class Notifications,Email,Slack notifStyle
|
|
||||||
class Storage,PromStorage,LokiStorage,Backups storageStyle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Métriques Clés
|
### Métriques Clés
|
||||||
|
|
||||||
**API Performance** (Prometheus PromQL) :
|
**API Performance** (requêtes PromQL) :
|
||||||
```promql
|
- Latency p99 : histogramme quantile 99e percentile sur durée requêtes HTTP (fenêtre 5 min)
|
||||||
# Latency p99
|
- Error rate : ratio requêtes 5xx / total requêtes (fenêtre 5 min)
|
||||||
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
|
- Throughput : taux de requêtes par seconde (fenêtre 5 min)
|
||||||
|
|
||||||
# Error rate
|
|
||||||
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m])
|
|
||||||
|
|
||||||
# Throughput
|
|
||||||
rate(http_requests_total[5m])
|
|
||||||
```
|
|
||||||
|
|
||||||
**Infrastructure** :
|
**Infrastructure** :
|
||||||
- CPU usage : `rate(node_cpu_seconds_total{mode!="idle"}[5m])`
|
- CPU usage : taux utilisation CPU (mode non-idle, fenêtre 5 min)
|
||||||
- Memory usage : `node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes`
|
- Memory usage : ratio mémoire disponible / totale
|
||||||
- Disk I/O : `rate(node_disk_io_time_seconds_total[5m])`
|
- Disk I/O : temps I/O disque (fenêtre 5 min)
|
||||||
|
|
||||||
**Business** :
|
**Business** (compteurs custom) :
|
||||||
- Active users (DAU) : compteur custom `roadwave_active_users_total`
|
- Active users (DAU) : `roadwave_active_users_total`
|
||||||
- Audio streams actifs : `roadwave_hls_streams_active`
|
- Audio streams actifs : `roadwave_hls_streams_active`
|
||||||
- Signalements modération : `roadwave_moderation_reports_total`
|
- Signalements modération : `roadwave_moderation_reports_total`
|
||||||
|
|
||||||
@@ -209,60 +165,29 @@ rate(http_requests_total[5m])
|
|||||||
|
|
||||||
### Alerting Rules
|
### Alerting Rules
|
||||||
|
|
||||||
**Critiques** (Slack + Email immédiat) :
|
**Alertes critiques** (Slack + Email immédiat) :
|
||||||
```yaml
|
- **API Down** : Job API indisponible pendant >1 min → Notification immédiate
|
||||||
- alert: APIDown
|
- **High Error Rate** : Taux erreurs 5xx >1% pendant >5 min → Notification immédiate
|
||||||
expr: up{job="roadwave-api"} == 0
|
- **Database Down** : PostgreSQL indisponible pendant >1 min → Notification immédiate
|
||||||
for: 1m
|
|
||||||
severity: critical
|
|
||||||
message: "API indisponible depuis 1 min"
|
|
||||||
|
|
||||||
- alert: HighErrorRate
|
**Alertes warnings** (Email uniquement) :
|
||||||
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
|
- **High Latency** : Latency p99 >100ms pendant >10 min → Investigation requise
|
||||||
for: 5m
|
- **Disk Space Running Out** : Espace disque <10% pendant >30 min → Nettoyage requis
|
||||||
severity: critical
|
|
||||||
message: "Error rate >1% depuis 5 min"
|
|
||||||
|
|
||||||
- alert: DatabaseDown
|
|
||||||
expr: up{job="postgresql"} == 0
|
|
||||||
for: 1m
|
|
||||||
severity: critical
|
|
||||||
message: "PostgreSQL indisponible"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Warnings** (Email uniquement) :
|
|
||||||
```yaml
|
|
||||||
- alert: HighLatency
|
|
||||||
expr: histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 0.1
|
|
||||||
for: 10m
|
|
||||||
severity: warning
|
|
||||||
message: "Latency p99 >100ms depuis 10 min"
|
|
||||||
|
|
||||||
- alert: DiskSpaceRunningOut
|
|
||||||
expr: node_filesystem_avail_bytes / node_filesystem_size_bytes < 0.1
|
|
||||||
for: 30m
|
|
||||||
severity: warning
|
|
||||||
message: "Espace disque <10%"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup & Disaster Recovery
|
### Backup & Disaster Recovery
|
||||||
|
|
||||||
**PostgreSQL WAL-E** :
|
**PostgreSQL WAL-E** :
|
||||||
```bash
|
- Méthode : Backup continu Write-Ahead Log (WAL)
|
||||||
# Backup continu WAL (Write-Ahead Log)
|
- Rétention : 7 jours full + WAL incrémentaux
|
||||||
wal-e backup-push /var/lib/postgresql/data
|
- Stockage : S3 OVH région GRA (France)
|
||||||
|
- Chiffrement : AES-256 server-side
|
||||||
# Rétention : 7 jours full + WAL
|
|
||||||
# Stockage : S3 OVH (région GRA, France)
|
|
||||||
# Chiffrement : AES-256 server-side
|
|
||||||
```
|
|
||||||
|
|
||||||
**RTO (Recovery Time Objective)** : 1h
|
**RTO (Recovery Time Objective)** : 1h
|
||||||
- Temps de restore depuis S3 : ~30 min (DB 10 GB)
|
- Restore depuis S3 : ~30 min (DB 10 GB)
|
||||||
- Temps validation + relance services : ~30 min
|
- Validation + relance services : ~30 min
|
||||||
|
|
||||||
**RPO (Recovery Point Objective)** : 15 min
|
**RPO (Recovery Point Objective)** : 15 min
|
||||||
- WAL archivage toutes les 15 min
|
- Fréquence archivage WAL : toutes les 15 min
|
||||||
- Perte maximale : 15 min de transactions
|
- Perte maximale : 15 min de transactions
|
||||||
|
|
||||||
**Tests DR** : Mensuel (restore backup sur environnement staging)
|
**Tests DR** : Mensuel (restore backup sur environnement staging)
|
||||||
@@ -272,7 +197,7 @@ wal-e backup-push /var/lib/postgresql/data
|
|||||||
### API Down (5xx errors spike)
|
### API Down (5xx errors spike)
|
||||||
|
|
||||||
1. **Vérifier** : Grafana dashboard → onglet Errors
|
1. **Vérifier** : Grafana dashboard → onglet Errors
|
||||||
2. **Logs** : Loki query `{app="roadwave-api"} |= "error"`
|
2. **Logs** : Requête Loki filtrée sur app roadwave-api + niveau error
|
||||||
3. **Actions** :
|
3. **Actions** :
|
||||||
- Si OOM : restart container + augmenter RAM
|
- Si OOM : restart container + augmenter RAM
|
||||||
- Si DB connexions saturées : vérifier slow queries
|
- Si DB connexions saturées : vérifier slow queries
|
||||||
@@ -282,7 +207,7 @@ wal-e backup-push /var/lib/postgresql/data
|
|||||||
### Database Slow Queries
|
### Database Slow Queries
|
||||||
|
|
||||||
1. **Identifier** : Grafana → PostgreSQL dashboard → Top slow queries
|
1. **Identifier** : Grafana → PostgreSQL dashboard → Top slow queries
|
||||||
2. **Analyser** : `EXPLAIN ANALYZE` sur query problématique
|
2. **Analyser** : Utiliser EXPLAIN ANALYZE sur query problématique
|
||||||
3. **Actions** :
|
3. **Actions** :
|
||||||
- Index manquant : créer index (migration rapide)
|
- Index manquant : créer index (migration rapide)
|
||||||
- Lock contention : identifier transaction longue et kill si bloquante
|
- Lock contention : identifier transaction longue et kill si bloquante
|
||||||
@@ -291,7 +216,7 @@ wal-e backup-push /var/lib/postgresql/data
|
|||||||
### High Load (CPU >80%)
|
### High Load (CPU >80%)
|
||||||
|
|
||||||
1. **Vérifier** : Grafana → Node Exporter → CPU usage
|
1. **Vérifier** : Grafana → Node Exporter → CPU usage
|
||||||
2. **Top processus** : `htop` ou `docker stats`
|
2. **Top processus** : Consulter htop ou docker stats
|
||||||
3. **Actions** :
|
3. **Actions** :
|
||||||
- Si Whisper (modération) : réduire concurrence workers
|
- Si Whisper (modération) : réduire concurrence workers
|
||||||
- Si API : scale horizontal (ajouter instance)
|
- Si API : scale horizontal (ajouter instance)
|
||||||
|
|||||||
@@ -30,184 +30,62 @@ Stratégie **secrets management + encryption at rest + HTTPS** avec stack self-h
|
|||||||
|
|
||||||
### Architecture Secrets
|
### Architecture Secrets
|
||||||
|
|
||||||
```mermaid
|
**Environnements** :
|
||||||
graph TB
|
- **Développement** : Fichier .env local (non versionné)
|
||||||
subgraph Dev["Environnement Dev"]
|
- **Production** : HashiCorp Vault (self-hosted)
|
||||||
EnvFile[".env file<br/>(local uniquement)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Prod["Production"]
|
**Flux** :
|
||||||
Vault["HashiCorp Vault<br/>(secrets storage)"]
|
1. **Vault** stocke secrets sensibles (JWT signing key, DB credentials, Mangopay API key, encryption master key)
|
||||||
API["Backend Go API"]
|
2. **Backend API** récupère secrets depuis Vault au démarrage
|
||||||
DB["PostgreSQL<br/>(encrypted at rest)"]
|
3. **Encryption layer** : AES-256-GCM pour PII, TLS 1.3 pour transport
|
||||||
Redis["Redis<br/>(TLS enabled)"]
|
4. **Stockage** : PostgreSQL (data encrypted at rest), Redis (TLS enabled)
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Encryption["Encryption Layer"]
|
|
||||||
AES["AES-256-GCM<br/>(PII encryption)"]
|
|
||||||
TLS["TLS 1.3<br/>(transport)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Secrets["Secrets Stockés"]
|
|
||||||
JWT["JWT Signing Key<br/>(RS256 private key)"]
|
|
||||||
DBCreds["DB Credentials<br/>(user/pass)"]
|
|
||||||
Mangopay["Mangopay API Key<br/>(sandbox + prod)"]
|
|
||||||
EncKey["Encryption Master Key<br/>(AES-256)"]
|
|
||||||
end
|
|
||||||
|
|
||||||
EnvFile -.->|dev only| API
|
|
||||||
Vault --> API
|
|
||||||
|
|
||||||
Vault --- JWT
|
|
||||||
Vault --- DBCreds
|
|
||||||
Vault --- Mangopay
|
|
||||||
Vault --- EncKey
|
|
||||||
|
|
||||||
API --> AES
|
|
||||||
API --> TLS
|
|
||||||
AES --> DB
|
|
||||||
TLS --> DB
|
|
||||||
TLS --> Redis
|
|
||||||
|
|
||||||
classDef devStyle fill:#fff3e0,stroke:#e65100
|
|
||||||
classDef prodStyle fill:#e3f2fd,stroke:#1565c0
|
|
||||||
classDef encStyle fill:#f3e5f5,stroke:#6a1b9a
|
|
||||||
classDef secretStyle fill:#ffebee,stroke:#c62828
|
|
||||||
|
|
||||||
class Dev,EnvFile devStyle
|
|
||||||
class Prod,Vault,API,DB,Redis prodStyle
|
|
||||||
class Encryption,AES,TLS encStyle
|
|
||||||
class Secrets,JWT,DBCreds,Mangopay,EncKey secretStyle
|
|
||||||
```
|
|
||||||
|
|
||||||
### Secrets Management avec Vault
|
### Secrets Management avec Vault
|
||||||
|
|
||||||
**Initialisation Vault** (one-time setup) :
|
**Initialisation Vault** (one-time setup) :
|
||||||
```bash
|
1. Init Vault : génération 5 unseal keys + root token (Shamir secret sharing)
|
||||||
# 1. Init Vault (génère unseal keys + root token)
|
2. Unseal : 3 clés parmi 5 requises pour déverrouiller Vault
|
||||||
vault operator init -key-shares=5 -key-threshold=3
|
3. Login root + activation KV-v2 engine (path : `roadwave/`)
|
||||||
|
|
||||||
# 2. Unseal (3 clés requises parmi 5)
|
**Secrets stockés** :
|
||||||
vault operator unseal <key1>
|
- **JWT signing key** : Paire RS256 privée/publique
|
||||||
vault operator unseal <key2>
|
- **Database credentials** : Host, port, user, password (généré aléatoire 32 caractères)
|
||||||
vault operator unseal <key3>
|
- **Mangopay API** : Client ID, API key, webhook secret
|
||||||
|
|
||||||
# 3. Login root + création secrets
|
**Récupération depuis Backend Go** :
|
||||||
vault login <root-token>
|
- Utilisation SDK `hashicorp/vault/api`
|
||||||
vault secrets enable -path=roadwave kv-v2
|
- Authentification via token Vault (variable env VAULT_TOKEN)
|
||||||
```
|
- Récupération secrets via KVv2 engine
|
||||||
|
|
||||||
**Stockage secrets** :
|
|
||||||
```bash
|
|
||||||
# JWT signing key (RS256 private key)
|
|
||||||
vault kv put roadwave/jwt private_key=@jwt-private.pem public_key=@jwt-public.pem
|
|
||||||
|
|
||||||
# Database credentials
|
|
||||||
vault kv put roadwave/database \
|
|
||||||
host=localhost \
|
|
||||||
port=5432 \
|
|
||||||
user=roadwave \
|
|
||||||
password=<généré-aléatoire-32-chars>
|
|
||||||
|
|
||||||
# Mangopay API
|
|
||||||
vault kv put roadwave/mangopay \
|
|
||||||
client_id=<sandbox-client-id> \
|
|
||||||
api_key=<sandbox-api-key> \
|
|
||||||
webhook_secret=<généré-aléatoire>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Récupération depuis Go** :
|
|
||||||
```go
|
|
||||||
import vault "github.com/hashicorp/vault/api"
|
|
||||||
|
|
||||||
client, _ := vault.NewClient(&vault.Config{
|
|
||||||
Address: "http://vault:8200",
|
|
||||||
})
|
|
||||||
client.SetToken(os.Getenv("VAULT_TOKEN"))
|
|
||||||
|
|
||||||
secret, _ := client.KVv2("roadwave").Get(context.Background(), "database")
|
|
||||||
dbPassword := secret.Data["password"].(string)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Encryption PII (Field-level)
|
### Encryption PII (Field-level)
|
||||||
|
|
||||||
**Données chiffrées** (AES-256-GCM) :
|
**Données chiffrées** (AES-256-GCM) :
|
||||||
- **GPS précis** : lat/lon (24h), puis geohash-5 seulement ([Règle 02](../regles-metier/02-conformite-rgpd.md))
|
- **GPS précis** : lat/lon conservés 24h puis réduits à geohash-5 (~5km²) ([Règle 02](../regles-metier/02-conformite-rgpd.md))
|
||||||
- **Email** : chiffré en base, déchiffré à l'envoi
|
- **Email** : chiffré en base, déchiffré uniquement à l'envoi
|
||||||
- **Numéro téléphone** : si ajouté (Phase 2)
|
- **Numéro téléphone** : si ajouté (Phase 2)
|
||||||
|
|
||||||
**Architecture encryption** :
|
**Architecture encryption** :
|
||||||
```go
|
- Utilisation bibliothèque standard Go `crypto/aes` avec mode GCM (AEAD)
|
||||||
type Encryptor struct {
|
- Master key 256 bits (32 bytes) récupérée depuis Vault
|
||||||
masterKey []byte // 256 bits (32 bytes) depuis Vault
|
- Chiffrement : génération nonce aléatoire + seal GCM → encodage base64
|
||||||
}
|
- Stockage : colonne `email_encrypted` en base PostgreSQL
|
||||||
|
|
||||||
func (e *Encryptor) Encrypt(plaintext string) (string, error) {
|
**Contraintes** :
|
||||||
block, _ := aes.NewCipher(e.masterKey)
|
- Index direct sur champ chiffré impossible
|
||||||
gcm, _ := cipher.NewGCM(block)
|
- Solution : index sur hash SHA-256 de l'email chiffré pour recherche
|
||||||
|
|
||||||
nonce := make([]byte, gcm.NonceSize())
|
|
||||||
rand.Read(nonce)
|
|
||||||
|
|
||||||
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
|
||||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
email := "user@example.com"
|
|
||||||
encryptedEmail, _ := encryptor.Encrypt(email)
|
|
||||||
// Store in DB: "Ae3xK9... (base64 ciphertext)"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Schema PostgreSQL** :
|
|
||||||
```sql
|
|
||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
email_encrypted TEXT NOT NULL, -- AES-256-GCM chiffré
|
|
||||||
created_at TIMESTAMPTZ NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Index sur email chiffré IMPOSSIBLE → utiliser hash pour recherche
|
|
||||||
CREATE INDEX idx_email_hash ON users(sha256(email_encrypted));
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTPS/TLS Configuration
|
### HTTPS/TLS Configuration
|
||||||
|
|
||||||
**Let's Encrypt wildcard certificate** :
|
**Let's Encrypt wildcard certificate** :
|
||||||
```bash
|
- Méthode : Certbot avec DNS-01 challenge (API OVH)
|
||||||
# Certbot avec DNS challenge (OVH API)
|
- Domaines couverts : `roadwave.fr` + `*.roadwave.fr` (wildcard)
|
||||||
certbot certonly \
|
- Renouvellement : automatique via cron quotidien (30j avant expiration)
|
||||||
--dns-ovh \
|
|
||||||
--dns-ovh-credentials ~/.secrets/ovh.ini \
|
|
||||||
-d roadwave.fr \
|
|
||||||
-d *.roadwave.fr
|
|
||||||
|
|
||||||
# Renouvellement auto (cron)
|
**Nginx TLS configuration** :
|
||||||
0 0 * * * certbot renew --post-hook "systemctl reload nginx"
|
- Protocol : TLS 1.3 uniquement (pas de TLS 1.2 ou inférieur)
|
||||||
```
|
- Ciphers : TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384
|
||||||
|
- HSTS : max-age 1 an, includeSubDomains
|
||||||
**Nginx TLS config** :
|
- Security headers : X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name api.roadwave.fr;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/roadwave.fr/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/roadwave.fr/privkey.pem;
|
|
||||||
|
|
||||||
# TLS 1.3 uniquement
|
|
||||||
ssl_protocols TLSv1.3;
|
|
||||||
ssl_ciphers 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384';
|
|
||||||
|
|
||||||
# HSTS (force HTTPS)
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
# Security headers
|
|
||||||
add_header X-Frame-Options "DENY" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternatives considérées
|
## Alternatives considérées
|
||||||
|
|
||||||
@@ -295,24 +173,11 @@ server {
|
|||||||
|
|
||||||
### Rate Limiting (Protection DDoS/Brute-force)
|
### Rate Limiting (Protection DDoS/Brute-force)
|
||||||
|
|
||||||
**Configuration Fiber** :
|
**Configuration** :
|
||||||
```go
|
- Middleware Fiber `limiter` avec backend Redis
|
||||||
import "github.com/gofiber/fiber/v3/middleware/limiter"
|
- Limite : 100 requêtes par minute par IP (global)
|
||||||
|
- Clé de limitation : adresse IP client
|
||||||
app.Use(limiter.New(limiter.Config{
|
- Réponse limitation : HTTP 429 "Too many requests"
|
||||||
Max: 100, // 100 requêtes
|
|
||||||
Expiration: 1 * time.Minute, // par minute
|
|
||||||
Storage: redisStorage, // Redis backend
|
|
||||||
KeyGenerator: func(c fiber.Ctx) string {
|
|
||||||
return c.IP() // Par IP
|
|
||||||
},
|
|
||||||
LimitReached: func(c fiber.Ctx) error {
|
|
||||||
return c.Status(429).JSON(fiber.Map{
|
|
||||||
"error": "Too many requests",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Rate limits par endpoint** :
|
**Rate limits par endpoint** :
|
||||||
- `/auth/login` : 5 req/min/IP (protection brute-force)
|
- `/auth/login` : 5 req/min/IP (protection brute-force)
|
||||||
@@ -330,14 +195,11 @@ app.Use(limiter.New(limiter.Config{
|
|||||||
| **Mangopay API key** | À la demande | Rotation manuelle si compromission |
|
| **Mangopay API key** | À la demande | Rotation manuelle si compromission |
|
||||||
| **Encryption master key** | Jamais (re-encryption massive) | Backup sécurisé uniquement |
|
| **Encryption master key** | Jamais (re-encryption massive) | Backup sécurisé uniquement |
|
||||||
|
|
||||||
**Process rotation DB credentials (Vault)** :
|
**Process rotation DB credentials** :
|
||||||
```bash
|
- Vault génère automatiquement nouveau password
|
||||||
# Vault génère nouveau password + update PostgreSQL
|
- Vault met à jour PostgreSQL avec nouveau password
|
||||||
vault write database/rotate-root/roadwave
|
- Application récupère nouveau password au prochain accès Vault
|
||||||
|
- Ancien password invalide après grace period de 1h
|
||||||
# Application récupère nouveau password automatiquement
|
|
||||||
# Ancien password invalide après 1h grace period
|
|
||||||
```
|
|
||||||
|
|
||||||
## Métriques de Succès
|
## Métriques de Succès
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user