# ADR-025 : Sécurité - Secrets Management et Encryption **Statut** : Accepté **Date** : 2026-02-01 ## Contexte RoadWave manipule des données sensibles nécessitant une protection renforcée : - **Secrets applicatifs** : JWT signing key, DB credentials, Mangopay API keys - **PII utilisateurs** : Positions GPS précises, emails, données bancaires (via Mangopay) - **Conformité** : RGPD (minimisation données, encryption at rest), PCI-DSS (paiements) - **Souveraineté** : Self-hosted requis (ADR-015) Contrainte : **OWASP Top 10 mitigation** obligatoire pour sécurité applicative. ## Décision Stratégie **secrets management + encryption at rest + HTTPS** avec stack self-hosted. ### Stack Sécurité | Composant | Technologie | Licence | Justification | |-----------|-------------|---------|---------------| | **Secrets management** | HashiCorp Vault (open source) | MPL-2.0 | Standard industrie, rotation auto, audit logs | | **Encryption PII** | AES-256-GCM (crypto/aes Go) | BSD-3 | NIST approuvé, AEAD (authenticated) | | **HTTPS/TLS** | Let's Encrypt (Certbot) | ISC | Gratuit, renouvellement auto, wildcard support | | **CORS/CSRF** | Fiber middleware | MIT | Protection XSS/CSRF intégrée | | **Rate limiting** | Redis + Token Bucket (Fiber) | MIT/Apache | Protection brute-force, DDoS | | **SQL injection** | sqlc (prepared statements) | MIT | Parameterized queries (ADR-011) | ### Architecture Secrets ```mermaid graph TB subgraph Dev["Environnement Dev"] EnvFile[".env file
(local uniquement)"] end subgraph Prod["Production"] Vault["HashiCorp Vault
(secrets storage)"] API["Backend Go API"] DB["PostgreSQL
(encrypted at rest)"] Redis["Redis
(TLS enabled)"] end subgraph Encryption["Encryption Layer"] AES["AES-256-GCM
(PII encryption)"] TLS["TLS 1.3
(transport)"] end subgraph Secrets["Secrets Stockés"] JWT["JWT Signing Key
(RS256 private key)"] DBCreds["DB Credentials
(user/pass)"] Mangopay["Mangopay API Key
(sandbox + prod)"] EncKey["Encryption Master Key
(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 **Initialisation Vault** (one-time setup) : ```bash # 1. Init Vault (génère unseal keys + root token) vault operator init -key-shares=5 -key-threshold=3 # 2. Unseal (3 clés requises parmi 5) vault operator unseal vault operator unseal vault operator unseal # 3. Login root + création secrets vault login vault secrets enable -path=roadwave kv-v2 ``` **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= # Mangopay API vault kv put roadwave/mangopay \ client_id= \ api_key= \ webhook_secret= ``` **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) **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)) - **Email** : chiffré en base, déchiffré à l'envoi - **Numéro téléphone** : si ajouté (Phase 2) **Architecture encryption** : ```go type Encryptor struct { masterKey []byte // 256 bits (32 bytes) depuis Vault } func (e *Encryptor) Encrypt(plaintext string) (string, error) { block, _ := aes.NewCipher(e.masterKey) gcm, _ := cipher.NewGCM(block) 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 **Let's Encrypt wildcard certificate** : ```bash # Certbot avec DNS challenge (OVH API) certbot certonly \ --dns-ovh \ --dns-ovh-credentials ~/.secrets/ovh.ini \ -d roadwave.fr \ -d *.roadwave.fr # Renouvellement auto (cron) 0 0 * * * certbot renew --post-hook "systemctl reload nginx" ``` **Nginx TLS config** : ```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 ### Secrets Management | Option | Coût | Hébergement | Rotation auto | Audit | Verdict | |--------|------|-------------|---------------|-------|---------| | **Vault (OSS)** | **0€** | Self-hosted | ✅ Oui | ✅ Oui | ✅ Choisi | | Vault Enterprise | 150$/mois | Self-hosted | ✅ Oui | ✅ Oui | ❌ Overkill MVP | | Kubernetes Secrets | 0€ | K8s only | ❌ Non | ⚠️ Limité | ⚠️ Phase 2 (K8s) | | Variables env (.env) | 0€ | VM/container | ❌ Non | ❌ Non | ❌ Insécure prod | | AWS Secrets Manager | 0.40$/secret/mois | Cloud AWS | ✅ Oui | ✅ Oui | ❌ Souveraineté | ### Encryption Library | Option | Performance | AEAD | FIPS 140-2 | Verdict | |--------|-------------|------|------------|---------| | **crypto/aes (Go std)** | ⭐⭐⭐ Rapide | ✅ GCM | ✅ Approuvé | ✅ Choisi | | age (filippo.io/age) | ⭐⭐ Moyen | ✅ ChaCha20 | ❌ Non | ⚠️ Moins standard | | NaCl/libsodium | ⭐⭐⭐ Rapide | ✅ Poly1305 | ❌ Non | ⚠️ CGO dependency | ### TLS Certificate | Option | Coût | Renouvellement | Wildcard | Verdict | |--------|------|----------------|----------|---------| | **Let's Encrypt** | **0€** | Auto (90j) | ✅ Oui (DNS-01) | ✅ Choisi | | OVH SSL | 5€/an | Manuel | ✅ Oui | ❌ Coût inutile | | Cloudflare SSL | 0€ | Auto | ✅ Oui | ⚠️ Proxy Cloudflare | ## Justification ### HashiCorp Vault - **Standard industrie** : utilisé par 80% Fortune 500 - **Rotation automatique** : credentials DB renouvelés toutes les 90j - **Audit logs** : qui a accédé à quel secret, quand - **Unseal ceremony** : sécurité maximale (3/5 clés requises) - **Coût 0€** : version open source MPL-2.0 ### AES-256-GCM - **NIST approuvé** : standard gouvernement US (FIPS 140-2) - **AEAD** : Authenticated Encryption with Associated Data (pas de tampering) - **Performance** : hardware acceleration (AES-NI CPU) - **Bibliothèque std Go** : pas de dépendance externe ### Let's Encrypt - **Gratuit** : économie 50-200€/an vs certificat commercial - **Automatique** : Certbot renouvelle 30j avant expiration - **Wildcard** : 1 certificat pour *.roadwave.fr (tous sous-domaines) - **Adopté massivement** : 300M+ sites web ## Conséquences ### Positives - ✅ **Conformité RGPD** : encryption at rest PII, minimisation données - ✅ **PCI-DSS** : secrets paiement isolés (Mangopay API key dans Vault) - ✅ **OWASP Top 10** : SQL injection (sqlc), XSS/CSRF (Fiber), rate limiting - ✅ **Coût 0€** : stack complète open source - ✅ **Audit trail** : logs Vault tracent tous accès secrets ### Négatives - ⚠️ **Vault unseal** : nécessite 3/5 clés au redémarrage serveur (procédure manuelle) - ⚠️ **Performance encryption** : +0.5-2ms latency par champ chiffré (acceptable) - ❌ **Complexité opérationnelle** : Vault à maintenir (backups, upgrades) - ❌ **Recherche email impossible** : chiffrement empêche `WHERE email = 'x'` (utiliser hash) ### OWASP Top 10 Mitigation | Vulnérabilité | Mitigation RoadWave | Implémentation | |---------------|---------------------|----------------| | **A01: Broken Access Control** | JWT scopes + RBAC | Zitadel roles (ADR-008) | | **A02: Cryptographic Failures** | AES-256-GCM + TLS 1.3 | crypto/aes + Let's Encrypt | | **A03: Injection** | Prepared statements (sqlc) | ADR-011 | | **A04: Insecure Design** | Threat modeling + ADR reviews | Process architecture | | **A05: Security Misconfiguration** | Vault secrets + hardened config | ADR-025 | | **A06: Vulnerable Components** | Dependabot + go mod tidy | GitHub Actions | | **A07: Auth Failures** | Zitadel + rate limiting | ADR-008 + Fiber middleware | | **A08: Software Integrity** | Code signing + SBOM | Phase 2 | | **A09: Logging Failures** | Loki centralisé + audit Vault | ADR-024 | | **A10: SSRF** | Whitelist URLs + network policies | Fiber middleware | ### Rate Limiting (Protection DDoS/Brute-force) **Configuration Fiber** : ```go import "github.com/gofiber/fiber/v3/middleware/limiter" app.Use(limiter.New(limiter.Config{ 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** : - `/auth/login` : 5 req/min/IP (protection brute-force) - `/moderation/report` : 10 req/24h/user (anti-spam) - API générale : 100 req/min/IP ## Rotation des Secrets **Politique de rotation** : | Secret | Rotation | Justification | |--------|----------|---------------| | **JWT signing key** | 1 an | Compromission = invalidation tous tokens | | **DB credentials** | 90 jours | Best practice NIST | | **Mangopay API key** | À la demande | Rotation manuelle si compromission | | **Encryption master key** | Jamais (re-encryption massive) | Backup sécurisé uniquement | **Process rotation DB credentials (Vault)** : ```bash # Vault génère nouveau password + update PostgreSQL vault write database/rotate-root/roadwave # Application récupère nouveau password automatiquement # Ancien password invalide après 1h grace period ``` ## Métriques de Succès - 0 fuite secrets en production (audit logs Vault) - 100% traffic HTTPS (HTTP → HTTPS redirect) - Rate limiting < 0.1% false positives - Encryption overhead < 2ms p95 ## Migration et Rollout ### Phase 1 (MVP - Sprint 2-3) 1. Deploy Vault (Docker single-node) 2. Migrer secrets .env → Vault 3. Encryption emails (AES-256-GCM) 4. HTTPS Let's Encrypt (api.roadwave.fr) 5. Rate limiting Fiber (100 req/min global) ### Phase 2 (Post-MVP - Sprint 6-8) 1. Vault HA (3 nodes Raft) 2. Rotation automatique credentials 3. Field-level encryption GPS (après 24h) 4. WAF (Web Application Firewall) : ModSecurity 5. Penetration testing externe (Bug Bounty) ## Références - [ADR-008 : Authentification](008-authentification.md) (Zitadel, JWT) - [ADR-011 : Accès données](011-orm-acces-donnees.md) (sqlc, prepared statements) - [ADR-015 : Hébergement](015-hebergement.md) (OVH France, souveraineté) - [ADR-024 : Monitoring](024-monitoring-observabilite.md) (Audit logs) - [Règle 02 : Conformité RGPD](../regles-metier/02-conformite-rgpd.md) - [HashiCorp Vault Documentation](https://www.vaultproject.io/docs) - [OWASP Top 10 2021](https://owasp.org/Top10/) - [NIST SP 800-175B (Cryptography)](https://csrc.nist.gov/publications/detail/sp/800-175b/final)