Aller au contenu

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).

  1. 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),
    

  2. Modifier src/main.ts :

    // Fail-fast si config invalide
    const configService = app.get(ConfigService);
    configService.validateSecurityConfig(); // Throws si invalide
    

  3. Conditions fail-fast (ECT-10) :

  4. CORS_ORIGINS vide ou invalide → "Security config invalid: CORS_ORIGINS must contain at least one valid URI"
  5. RATE_LIMIT_GLOBAL_MAX < 1"Security config invalid: RATE_LIMIT_GLOBAL_MAX must be >= 1"
  6. RATE_LIMIT_GLOBAL_WINDOW_S < 1"Security config invalid: RATE_LIMIT_GLOBAL_WINDOW_S must be >= 1"
  7. 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).

  1. 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
    }));
    

  2. Créer src/common/middleware/security-headers.middleware.ts :

  3. Appliquer tous les headers contractuels (INV-01)
  4. Supprimer Server, X-Powered-By (INV-02)
  5. S'exécuter sur TOUTE réponse y compris erreurs (INV-11)
  6. Cache-Control conditionnel (ECT-01) :

    • Appliquer Cache-Control: no-store si :
    • Path match /auth/* ou /users/me
    • OU status code 401 ou 403
    • Implémentation via res.on('finish') pour capturer le status code final
  7. Modifier src/main.ts :

  8. Remplacer app.use(helmet()) par middleware configurable
  9. 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).

  1. 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 },
    }));
    

  2. Créer src/common/middleware/cors.middleware.ts :

  3. Validation stricte origine (INV-04)
  4. Preflight déterministe (INV-05)
  5. Aucun indicateur CORS si origine non autorisée
  6. Journalisation des refus (INV-09)

  7. Modifier src/main.ts :

  8. 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).

  1. Modifier src/config/rate-limit.config.ts :

    global: {
      max: 100,
      windowSeconds: 60,
      keyPrefix: 'rl:global:',
    },
    

  2. Créer src/common/middleware/global-rate-limit.middleware.ts :

  3. Utiliser RateLimitService existant
  4. Clé = IP client (résolution X-Forwarded-For si proxy de confiance)
  5. Réponse 429 avec Retry-After et corps JSON contractuel (INV-07)
  6. Ne pas bloquer les guards spécifiques (registration, login, MFA)

  7. Modifier src/modules/auth/services/rate-limit.service.ts :

  8. Ajouter méthode checkGlobalLimit(ip: string): Promise<RateLimitResult>
  9. 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).

  1. Modifier src/modules/audit/services/audit-log.service.ts :
  2. Ajouter événements : CORS_REJECTED, RATE_LIMIT_EXCEEDED
  3. Format : JSON structuré avec timestamp UTC ISO-8601, correlation ID

  4. Modifier les middlewares Phase 2-4 :

  5. Injecter X-Correlation-Id (existant via RlsMiddleware)
  6. Appeler AuditService pour é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.

  1. Créer les fichiers de tests E2E et unitaires (§2.5)
  2. Exécuter npm run test:e2e et npm run test:cov
  3. Corriger les régressions
  4. Résoudre DIV-01-v2 (ECT-05) : Étendre TC-ERR-02 explicitement :
  5. Ajouter assertions : expect(response.headers['x-content-type-options']).toBe('nosniff') sur réponses 429
  6. Ajouter assertions : vérifier security headers sur refus CORS (origine non autorisée)
  7. Fichier : test/e2e/security-headers.e2e-spec.ts
  8. Résoudre DIV-02-v2 (ECT-06) : Créer test d'intégration TC-INT-01 :
  9. Fichier : test/integration/rate-limit-proxy.integration-spec.ts
  10. 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
  11. Configuration : TRUSTED_PROXIES=loopback pour 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
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