PD-19 — Plan d'implémentation¶
1. Résumé exécutif¶
Ce plan décrit l'implémentation des mécanismes CORS, security headers et rate limiting conformément à la spécification PD-19 v2 (Gate 3 RESERVE).
Architecture existante utilisée : - Helmet v7.1.0 (à configurer finement) - CORS via app.enableCors() (à remplacer par middleware configurable) - Rate limiting Redis custom (à étendre en middleware global) - ConfigModule multi-source avec validation Joi (réutilisé) - AuditService avec signature HSM (réutilisé pour journalisation)
Stratégie : Extension des mécanismes existants plutôt que remplacement.
2. Composants à créer/modifier¶
2.1 Configuration (priorité haute)¶
| Fichier | Action | Justification |
|---|---|---|
src/config/security-headers.config.ts | Créer | Configuration security headers par env (INV-01) |
src/config/cors.config.ts | Créer | Configuration CORS contractuelle par env (INV-03, INV-04) |
src/config/rate-limit.config.ts | Modifier | Ajouter seuil global 100/60s/IP (INV-06) |
src/config/config.schema.ts | Modifier | Validation Joi des nouvelles configs |
2.2 Middleware (priorité haute)¶
| Fichier | Action | Justification |
|---|---|---|
src/common/middleware/security-headers.middleware.ts | Créer | Application socle headers (INV-01, INV-02, INV-11) |
src/common/middleware/cors.middleware.ts | Créer | CORS contractuel par env (INV-03, INV-04, INV-05) |
src/common/middleware/global-rate-limit.middleware.ts | Créer | Limitation globale 100/60s/IP (INV-06, INV-07) |
2.3 Services (priorité moyenne)¶
| Fichier | Action | Justification |
|---|---|---|
src/modules/auth/services/rate-limit.service.ts | Modifier | Méthode checkGlobalLimit() avec Retry-After |
src/modules/audit/services/audit-log.service.ts | Modifier | Nouveaux événements CORS/RATE_LIMIT |
2.4 Main bootstrap (priorité haute)¶
| Fichier | Action | Justification |
|---|---|---|
src/main.ts | Modifier | Fail-fast config, remplacer CORS/Helmet basiques |
2.5 Tests (priorité haute)¶
| Fichier | Action | Justification |
|---|---|---|
test/e2e/security-headers.e2e-spec.ts | Créer | TC-NOM-01, TC-NOM-02, TC-NOM-05, TC-ERR-02 |
test/e2e/cors.e2e-spec.ts | Créer | TC-NOM-03, TC-NOM-04, TC-ERR-01 |
test/e2e/rate-limit-global.e2e-spec.ts | Créer | TC-NOM-07, TC-NOM-08, TC-ERR-03, TC-ERR-04 |
test/unit/middleware/security-headers.middleware.spec.ts | Créer | Tests unitaires middleware |
test/unit/middleware/cors.middleware.spec.ts | Créer | Tests unitaires middleware |
test/unit/middleware/global-rate-limit.middleware.spec.ts | Créer | Tests unitaires middleware |
3. Séquence d'implémentation¶
Phase 1 — Configuration fail-fast (1 jour)¶
Objectif : Valider la configuration au démarrage (ERR-05).
-
Modifier
src/config/config.schema.ts:// Ajouter validation obligatoire pour security/CORS/rate-limit CORS_ORIGINS: Joi.array().items(Joi.string().uri()).min(1).required(), SECURITY_HEADERS_ENABLED: Joi.boolean().default(true), RATE_LIMIT_GLOBAL_MAX: Joi.number().integer().min(1).default(100), RATE_LIMIT_GLOBAL_WINDOW_S: Joi.number().integer().min(1).default(60), -
Modifier
src/main.ts: -
Conditions fail-fast (ECT-10) :
CORS_ORIGINSvide ou invalide →"Security config invalid: CORS_ORIGINS must contain at least one valid URI"RATE_LIMIT_GLOBAL_MAX < 1→"Security config invalid: RATE_LIMIT_GLOBAL_MAX must be >= 1"RATE_LIMIT_GLOBAL_WINDOW_S < 1→"Security config invalid: RATE_LIMIT_GLOBAL_WINDOW_S must be >= 1"- Code de sortie :
process.exit(1)après log error
Livrables : TC-ERR-05 passant.
Phase 2 — Security Headers Middleware (1 jour)¶
Objectif : Socle contractuel de headers sur toute réponse (INV-01, INV-02, INV-11).
-
Créer
src/config/security-headers.config.ts:export const securityHeadersConfig = registerAs('securityHeaders', () => ({ csp: "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'", hsts: { maxAge: process.env.NODE_ENV === 'development' ? 300 : 31536000, includeSubDomains: true, enabled: process.env.NODE_ENV !== 'development' || process.env.HTTPS === 'true', }, // ... autres headers contractuels })); -
Créer
src/common/middleware/security-headers.middleware.ts: - Appliquer tous les headers contractuels (INV-01)
- Supprimer
Server,X-Powered-By(INV-02) - S'exécuter sur TOUTE réponse y compris erreurs (INV-11)
-
Cache-Control conditionnel (ECT-01) :
- Appliquer
Cache-Control: no-storesi : - Path match
/auth/*ou/users/me - OU status code
401ou403 - Implémentation via
res.on('finish')pour capturer le status code final
- Appliquer
-
Modifier
src/main.ts: - Remplacer
app.use(helmet())par middleware configurable - Appliquer AVANT tout autre middleware
Livrables : TC-NOM-01, TC-NOM-02, TC-NOM-05, TC-ERR-02 passants.
Phase 3 — CORS Middleware (1 jour)¶
Objectif : CORS contractuel par environnement (INV-03, INV-04, INV-05).
-
Créer
src/config/cors.config.ts:export const corsConfig = registerAs('cors', () => ({ origins: { development: ['http://localhost:3000', 'http://localhost:8080'], test: ['https://app-test.probatiovault.com', 'https://site-test.probatiovault.com'], production: ['https://app.probatiovault.com', 'https://www.probatiovault.com'], }, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Authorization', 'Content-Type', 'Accept', 'Origin', 'X-Requested-With', 'X-Correlation-Id'], exposedHeaders: ['X-Correlation-Id', 'Retry-After'], credentials: true, maxAge: { development: 60, test: 300, production: 600 }, })); -
Créer
src/common/middleware/cors.middleware.ts: - Validation stricte origine (INV-04)
- Preflight déterministe (INV-05)
- Aucun indicateur CORS si origine non autorisée
-
Journalisation des refus (INV-09)
-
Modifier
src/main.ts: - Remplacer
app.enableCors()par middleware custom
Livrables : TC-NOM-03, TC-NOM-04, TC-ERR-01 passants.
Phase 4 — Rate Limiting Global (1 jour)¶
Objectif : Limitation 100/60s/IP sur tous endpoints (INV-06, INV-07).
-
Modifier
src/config/rate-limit.config.ts: -
Créer
src/common/middleware/global-rate-limit.middleware.ts: - Utiliser
RateLimitServiceexistant - Clé = IP client (résolution X-Forwarded-For si proxy de confiance)
- Réponse 429 avec
Retry-Afteret corps JSON contractuel (INV-07) -
Ne pas bloquer les guards spécifiques (registration, login, MFA)
-
Modifier
src/modules/auth/services/rate-limit.service.ts: - Ajouter méthode
checkGlobalLimit(ip: string): Promise<RateLimitResult> - Retourner
{ allowed: boolean, retryAfter?: number }
Livrables : TC-NOM-07, TC-NOM-08, TC-ERR-03, TC-ERR-04 passants.
Phase 5 — Journalisation structurée (0.5 jour)¶
Objectif : Logs JSON UTC corrélables (INV-09).
- Modifier
src/modules/audit/services/audit-log.service.ts: - Ajouter événements :
CORS_REJECTED,RATE_LIMIT_EXCEEDED -
Format : JSON structuré avec timestamp UTC ISO-8601, correlation ID
-
Modifier les middlewares Phase 2-4 :
- Injecter
X-Correlation-Id(existant via RlsMiddleware) - Appeler
AuditServicepour événements de refus
Livrables : TC-NOM-06 passant.
Phase 6 — Tests et corrections (1.5 jours)¶
Objectif : Couverture complète des scénarios contractuels.
- Créer les fichiers de tests E2E et unitaires (§2.5)
- Exécuter
npm run test:e2eetnpm run test:cov - Corriger les régressions
- Résoudre DIV-01-v2 (ECT-05) : Étendre TC-ERR-02 explicitement :
- Ajouter assertions :
expect(response.headers['x-content-type-options']).toBe('nosniff')sur réponses 429 - Ajouter assertions : vérifier security headers sur refus CORS (origine non autorisée)
- Fichier :
test/e2e/security-headers.e2e-spec.ts - Résoudre DIV-02-v2 (ECT-06) : Créer test d'intégration TC-INT-01 :
- Fichier :
test/integration/rate-limit-proxy.integration-spec.ts - Scénarios :
- X-Forwarded-For avec proxy de confiance → IP extraite correctement
- X-Forwarded-For forgé (hors trust chain) → IP source directe utilisée
- Absence de X-Forwarded-For → IP source directe utilisée
- Configuration :
TRUSTED_PROXIES=loopbackpour le test
Livrables : Coverage ≥ 85%, tous TC-* passants, TC-INT-01 passant.
4. Ordre d'exécution des middlewares¶
HTTP Request
↓
[1. SecurityHeadersMiddleware] → Applique socle headers (INV-01, INV-02)
↓
[2. CorsMiddleware] → Validation origine, preflight (INV-03-05)
↓
[3. GlobalRateLimitMiddleware] → Check 100/60s/IP (INV-06, INV-07)
↓
[4. RlsMiddleware] → Context userId, requestId (existant)
↓
[5. JwtAuthGuard] → Authentication (existant)
↓
[6. Specific Rate Limit Guards] → Registration, Login, MFA (existant)
↓
[7. Controller Handler]
↓
[8. HttpExceptionFilter] → Erreurs avec headers (INV-11)
↓
HTTP Response (headers appliqués sur TOUTE réponse)
Point clé : SecurityHeadersMiddleware applique les headers via res.on('finish') pour garantir leur présence même sur les erreurs.
4b. Diagrammes Mermaid¶
4b.1 Graphe de dépendances des composants¶
graph TD
subgraph Configuration
A[config.schema.ts] --> B[security-headers.config.ts]
A --> C[cors.config.ts]
A --> D[rate-limit.config.ts]
end
subgraph Middleware
B --> E[SecurityHeadersMiddleware]
C --> F[CorsMiddleware]
D --> G[GlobalRateLimitMiddleware]
end
subgraph Services existants
H[RateLimitService]
I[AuditService]
J[ConfigService]
end
subgraph Bootstrap
K[main.ts]
end
G --> H
F --> I
G --> I
E --> J
F --> J
G --> J
K --> E
K --> F
K --> G
style A fill:#f9f,stroke:#333
style K fill:#ff9,stroke:#333
style H fill:#9cf,stroke:#333
style I fill:#9cf,stroke:#333
style J fill:#9cf,stroke:#333 4b.2 Diagramme de séquence — Requête avec CORS valide et rate limit OK¶
sequenceDiagram
participant Client
participant SecurityHeadersMW as SecurityHeadersMiddleware
participant CorsMW as CorsMiddleware
participant RateLimitMW as GlobalRateLimitMiddleware
participant Redis
participant RlsMW as RlsMiddleware
participant Guard as JwtAuthGuard
participant Controller
participant AuditSvc as AuditService
Client->>SecurityHeadersMW: HTTP Request
SecurityHeadersMW->>SecurityHeadersMW: Enregistre res.on('finish') pour headers
SecurityHeadersMW->>CorsMW: next()
CorsMW->>CorsMW: Vérifie Origin vs CORS_ORIGINS
alt Origine autorisée
CorsMW->>CorsMW: Ajoute Access-Control-Allow-Origin
CorsMW->>RateLimitMW: next()
else Origine non autorisée
CorsMW->>AuditSvc: CORS_REJECTED event
CorsMW-->>Client: 200 sans headers CORS
end
RateLimitMW->>Redis: INCR rl:global:{ip}
Redis-->>RateLimitMW: count, TTL
alt count <= 100
RateLimitMW->>RlsMW: next()
RlsMW->>Guard: next()
Guard->>Controller: next()
Controller-->>Client: 200 + security headers (via finish)
else count > 100
RateLimitMW->>AuditSvc: RATE_LIMIT_EXCEEDED event
RateLimitMW-->>Client: 429 + Retry-After + security headers (via finish)
end 4b.3 Diagramme de séquence — Preflight OPTIONS¶
sequenceDiagram
participant Browser
participant SecurityHeadersMW as SecurityHeadersMiddleware
participant CorsMW as CorsMiddleware
Browser->>SecurityHeadersMW: OPTIONS /api/resource
SecurityHeadersMW->>SecurityHeadersMW: Enregistre res.on('finish')
SecurityHeadersMW->>CorsMW: next()
CorsMW->>CorsMW: Vérifie Origin + Access-Control-Request-Method
alt Preflight autorisé
CorsMW-->>Browser: 204 + CORS headers + max-age (INV-05)
else Preflight refusé
CorsMW-->>Browser: 200 sans headers CORS (ECT-04)
end
Note over Browser: Headers de sécurité appliqués via res.on('finish') 5. Gestion des environnements¶
| Paramètre | dev | test | prod |
|---|---|---|---|
| Origines CORS | localhost:3000, localhost:8080 | app-test, site-test | app, www |
| HSTS | 300s si HTTPS | 31536000s | 31536000s |
| Cache-Control | routes sensibles | routes sensibles | routes sensibles |
| Preflight max-age | 60s | 300s | 600s |
| Rate limit global | 100/60s | 100/60s | 100/60s |
6. Points d'attention¶
6.1 Rétrocompatibilité¶
- Les guards spécifiques (
RateLimitGuard,LoginRateLimitGuard,MfaRateLimitGuard) restent actifs - Le middleware global s'ajoute en amont, ne les remplace pas
- Les seuils spécifiques (5/min login, 10/min registration) sont plus restrictifs que le global
6.2 Performance¶
- Redis atomique (INCR + EXPIRE en une commande) pour rate limiting
- Cache preflight (max-age) réduit les requêtes OPTIONS
- Headers ajoutés en fin de réponse (pas de double écriture)
6.3 Sécurité¶
- Validation stricte des origines (pas de wildcard en prod)
- IP client via proxy de confiance uniquement (configurable)
- Pas de divulgation d'info dans les réponses d'erreur
6.4 Réserves Gate 3 à résoudre¶
| ID | Résolution | Où |
|---|---|---|
| DIV-01-v2 | Assertions headers sur 429/refus CORS | Tests E2E Phase 6 |
| DIV-02-v2 | Documentation test intégration IP proxy | Plan §4 + tests |
7. Dépendances externes¶
| Dépendance | Version | Action |
|---|---|---|
| helmet | ^7.1.0 | Existant, config à ajuster |
| ioredis | ^5.3.2 | Existant, pas de changement |
| @nestjs/config | ^3.1.1 | Existant, pas de changement |
| joi | 18.0.2 | Existant, schema à étendre |
Aucune nouvelle dépendance requise.
8. Estimation effort¶
| Phase | Effort | Agents |
|---|---|---|
| 1. Config fail-fast | 1j | agent-developer |
| 2. Security Headers | 1j | agent-developer |
| 3. CORS Middleware | 1j | agent-developer |
| 4. Rate Limiting Global | 1j | agent-developer |
| 5. Journalisation | 0.5j | agent-developer |
| 6. Tests et corrections | 1.5j | agent-qa-unit-integration |
| Total | 6j |
9. Critères de succès¶
- TC-NOM-01 à TC-NOM-08 passants
- TC-ERR-01 à TC-ERR-05 passants
- TC-NR-01 à TC-NR-06 passants
- TC-NEG-01 à TC-NEG-06 passants
- Coverage ≥ 85%
- DIV-01-v2 et DIV-02-v2 résolus
- Pipeline GitLab vert (lint, types, tests)
- Logs JSON UTC corrélables observables
10. Clarifications Gate 5¶
10.1 Refus preflight (ECT-04)¶
Le refus d'un preflight non autorisé retourne un 200 OK sans headers CORS. Le navigateur interprète l'absence de Access-Control-Allow-Origin comme un refus. Aucun statut 403/401 n'est renvoyé pour éviter de divulguer des informations.
10.2 Stratégie indisponibilité Redis (ECT-07)¶
En cas d'indisponibilité Redis : - Stratégie recommandée : fail-open (permettre les requêtes) - Justification : Préserver la disponibilité du service - Mitigation : Alerte monitoring sur erreurs Redis, fallback vers rate limiting mémoire local (dégradé) - À valider avec l'équipe SRE avant implémentation
10.3 Rétention logs 90 jours (ECT-08)¶
La rétention des logs de sécurité (90 jours minimum, INV-09) est une configuration infrastructure gérée par : - ElasticSearch/Loki : retention policy - S3/Glacier : lifecycle rules - Non codée dans l'application
10.4 Légende diagramme §4 (ECT-09)¶
Le diagramme §4 montre le flux logique complet de traitement d'une requête HTTP, incluant : - Middlewares NestJS (implémentent NestMiddleware) : positions 1-4 - Guards NestJS (implémentent CanActivate) : positions 5-6 - Interceptors/Filters : position 8
Les guards s'exécutent après les middlewares mais avant les handlers.
Document produit par le workflow de gouvernance IA — Étape 4 (Claude mode équilibré) Mis à jour après Gate 5 v1 (corrections ECT-01, ECT-05, ECT-06, ECT-10) Date : 2026-02-09