Aller au contenu

PD-22 — Plan d'implémentation : Module de configuration

EPIC de référence : PD-186 — BACKEND CORE Spécification source : PD-22-specification.md


0. Migration des variables existantes

0.1 Renommages obligatoires

Les variables suivantes doivent être renommées pour conformité avec la spec :

Variable actuelle Variable PD-22 Fichiers impactés Justification
NODE_ENV APP_ENV app.module.ts, database.config.ts, main.ts APP_ENV seule source de vérité
PORT HTTP_PORT main.ts Namespace HTTP_ obligatoire
CORS_ORIGIN HTTP_CORS_ORIGIN security.config.ts, main.ts Namespace HTTP_ obligatoire
DATABASE_URL DB_DATABASE_URL database.config.ts, parse-database-url.ts Namespace DB_ obligatoire
BCRYPT_SALT_ROUNDS CRYPTO_BCRYPT_SALT_ROUNDS security.config.ts Namespace CRYPTO_ obligatoire

0.2 Variables à supprimer

Les variables suivantes ne sont PAS dans la liste fermée PD-22 (section 10.2) :

Variable actuelle Action requise Justification
VAULT_NAMESPACE Supprimer Non listée dans section 10.2

Note : Les variables VAULT_DB_ROLE et VAULT_DYNAMIC_DB_ENABLED sont autorisées par la spec 10.2 (ajout pour Database Secrets Engine). Elles ne sont PAS des secrets mais des paramètres de configuration.

0.3 Plan de migration

PHASE 1 — Préparation (non breaking)
├─ Créer les nouveaux fichiers de config avec nouveaux noms
├─ Supporter temporairement les deux noms (ancien + nouveau)
└─ Logger un WARNING si ancien nom détecté

PHASE 2 — Migration .env
├─ Mettre à jour .env.example avec nouveaux noms
├─ Mettre à jour .env.development
├─ Mettre à jour .env.test
└─ Mettre à jour secrets Vault avec nouveaux noms

PHASE 3 — Suppression rétrocompatibilité
├─ Supprimer support des anciens noms
├─ Activer rejet strict (UNKNOWN_VARIABLE)
└─ Mettre à jour documentation

0.4 Script de migration suggéré

#!/bin/bash
# migrate-env-vars.sh

# Renommages dans les fichiers .env*
sed -i '' 's/^NODE_ENV=/APP_ENV=/g' .env*
sed -i '' 's/^PORT=/HTTP_PORT=/g' .env*
sed -i '' 's/^CORS_ORIGIN=/HTTP_CORS_ORIGIN=/g' .env*
sed -i '' 's/^DATABASE_URL=/DB_DATABASE_URL=/g' .env*
sed -i '' 's/^BCRYPT_SALT_ROUNDS=/CRYPTO_BCRYPT_SALT_ROUNDS=/g' .env*

echo "Migration terminée. Vérifier manuellement les fichiers."

0.5 Points d'attention migration

Risque Impact Mitigation
CI/CD utilise NODE_ENV Échec pipelines Mettre à jour variables GitLab/GitHub avant déploiement
Frameworks NestJS attendent NODE_ENV Comportement inattendu Conserver NODE_ENV en parallèle pour NestJS internals
Vault contient anciens noms secrets Secrets non trouvés Migrer Vault AVANT déploiement nouveau code

1. Découpage en composants

1.1 Architecture modulaire

src/config/
├── config.module.ts              # Module NestJS racine
├── config.service.ts             # Service d'accès à la configuration validée
├── config.schema.ts              # Schéma Joi complet avec namespaces
├── config.types.ts               # Types TypeScript stricts
├── config.constants.ts           # Constantes (namespaces, codes erreur)
├── loaders/
│   ├── env-file.loader.ts        # Chargeur .env.<APP_ENV>
│   ├── process-env.loader.ts     # Chargeur variables processus
│   └── vault.loader.ts           # Chargeur secrets Vault
├── validators/
│   ├── namespace.validator.ts    # Validation namespace autorisés
│   ├── secret-source.validator.ts # Détection secrets hors Vault
│   └── unknown-variable.validator.ts # Rejet variables inconnues
└── utils/
    ├── masking.util.ts           # Masquage secrets pour logs
    └── config-logger.util.ts     # Journalisation sécurisée

1.2 Responsabilités des composants

Composant Responsabilité Dépendances
ConfigModule Bootstrap du module, orchestration chargement Tous les loaders
ConfigService Exposition immuable de la configuration Schema validé
config.schema.ts Définition Joi exhaustive des 60+ variables Joi
env-file.loader.ts Lecture .env.<APP_ENV> dotenv
process-env.loader.ts Lecture process.env
vault.loader.ts Récupération secrets depuis Vault node-vault
namespace.validator.ts Vérification préfixe autorisé Liste fermée
secret-source.validator.ts Détection secrets interdits hors Vault Liste secrets
masking.util.ts Remplacement secrets par ***SECRET***

2. Flux techniques

2.1 Séquence de chargement (ordre strict)

┌─────────────────────────────────────────────────────────────────────────────┐
│                        DÉMARRAGE APPLICATION                                 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 1: Lecture APP_ENV depuis process.env                                  │
│ ├─ Si absent → throw ConfigError(ENV_INVALID)                               │
│ └─ Si ∉ {dev, test, prod} → throw ConfigError(ENV_INVALID)                  │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 2: Chargement .env.<APP_ENV>                                          │
│ ├─ Fichier absent → continue (pas d'erreur, les valeurs peuvent venir de   │
│ │   process.env ou des defaults Joi)                                        │
│ └─ Fichier présent → parse et stocke dans Map<string, string>               │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 3: Lecture variables processus (process.env)                          │
│ └─ Écrase les valeurs du .env pour les clés communes                        │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 4: Validation pré-Vault (FAIL-FAST)                                   │
│ ├─ Vérification namespace de chaque variable                                │
│ │   └─ Si namespace invalide → throw ConfigError(UNKNOWN_VARIABLE)          │
│ ├─ Détection secrets applicatifs dans .env/process.env                      │
│ │   └─ Si détecté → throw ConfigError(SECRET_SOURCE_FORBIDDEN)              │
│ └─ Vérification VAULT_ROLE_ID et VAULT_SECRET_ID présents                   │
│     └─ Si absents (ni .env ni process.env) et secrets applicatifs requis    │
│         → throw ConfigError(CONFIG_SOURCE_MISSING)                          │
│     NOTE: Les secrets de bootstrap DOIVENT être fournis via .env ou         │
│           process.env. L'absence des DEUX sources = échec immédiat.         │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 5: Connexion Vault et récupération secrets                            │
│ ├─ AppRole login avec VAULT_ROLE_ID + VAULT_SECRET_ID                       │
│ ├─ Lecture kv/backend/<APP_ENV>                                             │
│ └─ Si échec → throw ConfigError(CONFIG_SOURCE_MISSING)                      │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 6: Fusion finale (priorité croissante)                                │
│ 1. Defaults du schéma Joi                                                   │
│ 2. Valeurs .env.<APP_ENV>                                                   │
│ 3. Valeurs process.env                                                      │
│ 4. Secrets Vault (écrasement final)                                         │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 7: Validation Joi complète                                            │
│ ├─ abortEarly: false (toutes les erreurs)                                   │
│ ├─ allowUnknown: false (rejet strict)                                       │
│ ├─ stripUnknown: false (pas de suppression silencieuse)                     │
│ └─ Si erreur → throw ConfigError(MISSING_VARIABLE|INVALID_TYPE|...)         │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ ÉTAPE 8: Gel de la configuration                                            │
│ ├─ Object.freeze() récursif                                                 │
│ ├─ Journalisation des valeurs par défaut appliquées                         │
│ └─ Exposition via ConfigService.get<T>(key)                                 │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 Diagramme de séquence détaillé

┌────────┐   ┌──────────────┐   ┌─────────────┐   ┌───────────────┐   ┌───────┐
│  Main  │   │ ConfigModule │   │   Loaders   │   │  Validators   │   │ Vault │
└───┬────┘   └──────┬───────┘   └──────┬──────┘   └───────┬───────┘   └───┬───┘
    │               │                  │                  │               │
    │ bootstrap()   │                  │                  │               │
    │──────────────>│                  │                  │               │
    │               │                  │                  │               │
    │               │ loadAppEnv()     │                  │               │
    │               │─────────────────>│                  │               │
    │               │                  │                  │               │
    │               │ loadEnvFile()    │                  │               │
    │               │─────────────────>│                  │               │
    │               │                  │                  │               │
    │               │ loadProcessEnv() │                  │               │
    │               │─────────────────>│                  │               │
    │               │                  │                  │               │
    │               │ validatePreVault()                  │               │
    │               │────────────────────────────────────>│               │
    │               │                  │                  │               │
    │               │ loadVaultSecrets()                  │               │
    │               │─────────────────>│                  │               │
    │               │                  │ appRoleLogin()   │               │
    │               │                  │─────────────────────────────────>│
    │               │                  │                  │               │
    │               │                  │ readKvSecrets()  │               │
    │               │                  │─────────────────────────────────>│
    │               │                  │                  │               │
    │               │ validateSchema() │                  │               │
    │               │────────────────────────────────────>│               │
    │               │                  │                  │               │
    │               │ freeze()         │                  │               │
    │               │─────────────────>│                  │               │
    │               │                  │                  │               │
    │  ConfigService│                  │                  │               │
    │<──────────────│                  │                  │               │

2.3 Diagrammes Mermaid

Graphe de dépendances des composants

graph TD
    Main["main.ts<br/>(bootstrap)"] --> CM["ConfigModule"]

    CM --> EFL["env-file.loader.ts<br/>.env.&lt;APP_ENV&gt;"]
    CM --> PEL["process-env.loader.ts<br/>process.env"]
    CM --> VL["vault.loader.ts<br/>secrets Vault"]
    CM --> NV["namespace.validator.ts"]
    CM --> SSV["secret-source.validator.ts"]
    CM --> UVV["unknown-variable.validator.ts"]

    VL -->|AppRole login| Vault["HashiCorp Vault<br/>kv/backend/&lt;APP_ENV&gt;"]

    CM --> CS["config.schema.ts<br/>Joi validation"]
    CM --> CT["config.types.ts"]
    CM --> CC["config.constants.ts"]

    CS --> CSvc["ConfigService<br/>(immutable, frozen)"]
    CSvc --> MU["masking.util.ts"]
    CSvc --> CLU["config-logger.util.ts"]

    style Main fill:#e0e0e0,stroke:#333
    style CM fill:#4a90d9,stroke:#333,color:#fff
    style CSvc fill:#2ecc71,stroke:#333,color:#fff
    style Vault fill:#f39c12,stroke:#333,color:#fff
    style NV fill:#e74c3c,stroke:#333,color:#fff
    style SSV fill:#e74c3c,stroke:#333,color:#fff
    style UVV fill:#e74c3c,stroke:#333,color:#fff

Diagramme de séquence — Chargement et validation

sequenceDiagram
    participant Main as main.ts
    participant CM as ConfigModule
    participant EFL as env-file.loader
    participant PEL as process-env.loader
    participant NV as namespace.validator
    participant SSV as secret-source.validator
    participant VL as vault.loader
    participant Vault as HashiCorp Vault
    participant Schema as config.schema (Joi)
    participant CS as ConfigService

    Main->>CM: bootstrap()

    Note over CM: ÉTAPE 1 — Lecture APP_ENV
    CM->>PEL: loadAppEnv()
    PEL-->>CM: APP_ENV = dev|test|prod
    alt APP_ENV absent ou invalide
        CM--xMain: throw ConfigError(ENV_INVALID)
    end

    Note over CM: ÉTAPE 2 — Fichier .env
    CM->>EFL: loadEnvFile(APP_ENV)
    EFL-->>CM: Map<string, string>

    Note over CM: ÉTAPE 3 — Variables processus
    CM->>PEL: loadProcessEnv()
    PEL-->>CM: Map<string, string> (écrase .env)

    Note over CM: ÉTAPE 4 — Validation pré-Vault
    CM->>NV: validateNamespaces(mergedConfig)
    alt Namespace invalide
        NV--xCM: throw ConfigError(UNKNOWN_VARIABLE)
    end
    CM->>SSV: validateSecretSource(mergedConfig)
    alt Secret hors Vault détecté
        SSV--xCM: throw ConfigError(SECRET_SOURCE_FORBIDDEN)
    end

    Note over CM: ÉTAPE 5 — Secrets Vault
    CM->>VL: loadVaultSecrets(VAULT_ROLE_ID, VAULT_SECRET_ID)
    VL->>Vault: AppRole login
    Vault-->>VL: token
    VL->>Vault: read kv/backend/<APP_ENV>
    Vault-->>VL: secrets
    VL-->>CM: Map<string, string>

    Note over CM: ÉTAPE 6-7 — Fusion + Validation Joi
    CM->>Schema: validate(fusionConfig, {abortEarly: false})
    alt Erreur de validation
        Schema--xCM: throw ConfigError(MISSING_VARIABLE|INVALID_TYPE|...)
    end

    Note over CM: ÉTAPE 8 — Gel et exposition
    CM->>CS: deepFreeze(validatedConfig)
    CS-->>Main: ConfigService (immutable)

3. Mapping invariants → mécanismes

# Invariant (spec) Mécanisme technique
1 Aucune variable sans validation Joi.validate() avec abortEarly: false avant exposition
2 Aucune valeur implicite silencieuse Joi.default() avec callback de journalisation
3 Valeurs par défaut journalisées Hook custom logDefaultApplied(key, value) dans schéma
4 Rejet strict variables inconnues Joi.object().unknown(false) + stripUnknown: false
5 APP_ENV obligatoire liste fermée Joi.string().required().valid('dev', 'test', 'prod')
6 Ordre chargement strict documenté Chaîne de loaders séquentielle dans ConfigModule.forRoot()
7 Secrets uniquement via Vault SecretSourceValidator détecte et rejette secrets hors Vault
8 Secrets jamais en clair dans logs MaskingUtil.mask(value) sur toute sortie log
9 Erreurs déterministes code stable Enum ConfigErrorCode + classe ConfigError
10 Configuration immuable Object.freeze() récursif + readonly TypeScript
11 Namespace obligatoire NamespaceValidator.validate(key) contre liste fermée

3.1 Détail des mécanismes critiques

Mécanisme M1 : Validation Joi stricte

// config.schema.ts
export const configSchema = Joi.object({
  APP_ENV: Joi.string().required().valid('dev', 'test', 'prod'),
  // ... autres variables
}).options({
  abortEarly: false,      // Collecter TOUTES les erreurs
  allowUnknown: false,    // REJETER variables inconnues
  stripUnknown: false,    // NE PAS supprimer silencieusement
  presence: 'required',   // Par défaut tout est requis
});

Mécanisme M2 : Détection secrets hors Vault

// Liste exhaustive des clés secrètes (contractuelle)
const SECRET_KEYS = [
  'DB_DATABASE_URL', 'DB_PASSWORD',
  'REDIS_PASSWORD',
  'JWT_SECRET',
  'TURNSTILE_SECRET_KEY',
  'SMTP_USER', 'SMTP_PASS',
  'S3_ACCESS_KEY_ID', 'S3_SECRET_ACCESS_KEY',
  'INTERNAL_API_KEY',
  'CLOUDHSM_PIN',
] as const;

// Secrets de bootstrap autorisés hors Vault
const VAULT_BOOTSTRAP_KEYS = ['VAULT_ROLE_ID', 'VAULT_SECRET_ID'] as const;

function validateSecretSource(envConfig: Record<string, string>): void {
  for (const key of SECRET_KEYS) {
    if (key in envConfig) {
      throw new ConfigError(
        ConfigErrorCode.SECRET_SOURCE_FORBIDDEN,
        `Secret "${key}" detected outside Vault`
      );
    }
  }
}

Mécanisme M3 : Masquage systématique

// masking.util.ts
const SECRET_PLACEHOLDER = '***SECRET***';

function maskSecrets(config: Record<string, unknown>): Record<string, unknown> {
  const masked = { ...config };
  for (const key of SECRET_KEYS) {
    if (key in masked) {
      masked[key] = SECRET_PLACEHOLDER;
    }
  }
  return masked;
}

Mécanisme M4 : Journalisation valeurs par défaut

// Wrapper Joi pour tracer les defaults
function trackedDefault<T>(key: string, value: T): T {
  defaultsApplied.push({ key, value });
  return value;
}

// Usage dans le schéma
HTTP_PORT: Joi.number().default(trackedDefault('HTTP_PORT', 3000)),

Mécanisme M5 : Gel récursif

function deepFreeze<T extends object>(obj: T): Readonly<T> {
  Object.getOwnPropertyNames(obj).forEach((name) => {
    const value = (obj as Record<string, unknown>)[name];
    if (value && typeof value === 'object') {
      deepFreeze(value as object);
    }
  });
  return Object.freeze(obj);
}

4. Gestion des erreurs

4.1 Codes d'erreur stables

// config.constants.ts
export enum ConfigErrorCode {
  ENV_INVALID = 'CONFIG_ENV_INVALID',
  MISSING_VARIABLE = 'CONFIG_MISSING_VARIABLE',
  INVALID_TYPE = 'CONFIG_INVALID_TYPE',
  INVALID_VALUE = 'CONFIG_INVALID_VALUE',
  UNKNOWN_VARIABLE = 'CONFIG_UNKNOWN_VARIABLE',
  SECRET_LOG_ATTEMPT = 'CONFIG_SECRET_LOG_ATTEMPT',
  SECRET_SOURCE_FORBIDDEN = 'CONFIG_SECRET_SOURCE_FORBIDDEN',
  CONFIG_SOURCE_MISSING = 'CONFIG_SOURCE_MISSING',
}

4.2 Classe d'erreur dédiée

// config.error.ts
export class ConfigError extends Error {
  constructor(
    public readonly code: ConfigErrorCode,
    public readonly details: string,
    public readonly context?: Record<string, unknown>,
  ) {
    super(`[${code}] ${details}`);
    this.name = 'ConfigError';
  }
}

4.3 Matrice de gestion des erreurs

Code erreur Condition déclenchante Action Exit
ENV_INVALID APP_ENV absent ou invalide Log erreur + exit 1
MISSING_VARIABLE Variable required() absente Log erreur + exit 1
INVALID_TYPE Type Joi non respecté Log erreur + exit 1
INVALID_VALUE Contrainte Joi violée Log erreur + exit 1
UNKNOWN_VARIABLE Variable hors schéma ou namespace Log erreur + exit 1
SECRET_SOURCE_FORBIDDEN Secret dans .env/process.env Log erreur (masqué) + exit 1
CONFIG_SOURCE_MISSING Vault indisponible, secrets requis Log erreur + exit 1

4.4 Format de sortie erreur

[FATAL] Configuration validation failed

Code: CONFIG_MISSING_VARIABLE
Details: Variable "JWT_SECRET" is required but was not provided

Variables with errors:
  - JWT_SECRET: "value" is required
  - DB_PASSWORD: "value" is required

Startup aborted.

5. Impacts sécurité

5.1 Analyse des risques

Risque Impact Probabilité Mitigation
Fuite secret dans logs Critique Moyenne Masquage systématique + revue code
Secret dans .env committé Critique Faible Rejet strict SECRET_SOURCE_FORBIDDEN
Vault indisponible Majeur Faible Échec immédiat, pas de fallback
Injection via variable Majeur Faible Validation Joi stricte des formats
Accès config non autorisé Moyen Faible Immutabilité + encapsulation service

5.2 Mesures de sécurité implémentées

  1. Principe du moindre privilège
  2. ConfigService expose uniquement get<T>(key), pas l'objet complet
  3. Pas de méthode getAll() ou dump()

  4. Defense in depth

  5. Validation namespace AVANT chargement Vault
  6. Validation source secrets AVANT connexion Vault
  7. Validation Joi APRÈS fusion complète

  8. Audit trail

  9. Journalisation de chaque valeur par défaut appliquée
  10. Journalisation (masquée) de la configuration finale
  11. Horodatage du chargement

  12. Fail-secure

  13. Tout échec = arrêt immédiat
  14. Aucun fallback, aucune dégradation gracieuse

5.3 Liste des secrets (contractuelle)

Variable Type secret Source unique
DB_DATABASE_URL Connection string Vault
DB_PASSWORD Mot de passe Vault
REDIS_PASSWORD Mot de passe Vault
JWT_SECRET Clé symétrique Vault
TURNSTILE_SECRET_KEY API key Vault
SMTP_USER Identifiant Vault
SMTP_PASS Mot de passe Vault
S3_ACCESS_KEY_ID Access key Vault
S3_SECRET_ACCESS_KEY Secret key Vault
INTERNAL_API_KEY API key Vault
CLOUDHSM_PIN PIN HSM Vault

5.4 Secrets de bootstrap (exception contrôlée)

Variable Usage Source autorisée
VAULT_ROLE_ID AppRole authentication .env, process.env
VAULT_SECRET_ID AppRole authentication .env, process.env

6. Hypothèses techniques

H1 — Environnement d'exécution

  • Runtime Node.js >= 18.x LTS
  • Framework NestJS >= 10.x
  • Module @nestjs/config disponible mais non utilisé (implémentation custom)

H2 — Dépendances externes

Package Version Usage
joi ^17.x Validation de schéma
dotenv ^16.x Parsing fichiers .env
node-vault ^0.10.x Client Vault

H3 — Infrastructure Vault

  • Vault accessible via VAULT_ADDR
  • Méthode d'authentification : AppRole
  • Path secrets : kv/backend/<APP_ENV>
  • KV version 2

H4 — Structure fichiers .env

.env.dev      # Développement local
.env.test     # Tests automatisés
.env.prod     # Production (minimal, pas de secrets)

H5 — Convention de nommage

  • Variables en SCREAMING_SNAKE_CASE
  • Préfixe namespace obligatoire
  • Pas de variable sans namespace

H6 — Disponibilité Vault

  • Vault doit être accessible au démarrage
  • Timeout connexion : 5 secondes
  • Pas de retry automatique (fail-fast)

7. Points de vigilance

7.1 Risques d'implémentation

Point Risque Mitigation
Ordre des validations Secret détecté après log Valider sources AVANT tout logging
Fusion des configs Écrasement accidentel secret Secrets chargés EN DERNIER
Performance démarrage Latence Vault Cache token, connection pooling
Tests unitaires Mock Vault complexe Interface abstraite VaultClient
Type safety Perte types après freeze Generics TypeScript stricts

7.2 Checklist de revue code

  • Aucun console.log avec données config non masquées
  • SECRET_KEYS synchronisé avec spec section 10.2
  • ALLOWED_NAMESPACES synchronisé avec spec section 10.1
  • Tests couvrent tous les codes d'erreur
  • Pas de any dans les types config
  • Object.freeze() appelé sur objet final

7.3 Tests critiques à implémenter

Test Type Priorité
Secret dans .env → SECRET_SOURCE_FORBIDDEN Unit P0
Variable inconnue → UNKNOWN_VARIABLE Unit P0
Namespace invalide → UNKNOWN_VARIABLE Unit P0
APP_ENV invalide → ENV_INVALID Unit P0
Vault indisponible → CONFIG_SOURCE_MISSING Integration P0
Config valide complète → démarrage OK Integration P0
Immutabilité post-validation Unit P1
Masquage secrets dans logs Unit P1
Valeur par défaut journalisée Unit P1

7.4 Points d'attention déploiement

  1. Ordre de déploiement
  2. Vault configuré et secrets présents AVANT déploiement backend
  3. Variables VAULT_* dans CI/CD secrets

  4. Rollback

  5. Configuration statique = rollback simple
  6. Pas d'état à gérer

  7. Monitoring

  8. Alerter sur erreurs CONFIG_* au démarrage
  9. Dashboard temps de démarrage (inclut latence Vault)

  10. Documentation ops

  11. Procédure ajout nouveau secret dans Vault
  12. Procédure ajout nouvelle variable de config

8. Résumé des livrables

Livrable Fichier Description
Module principal config.module.ts Bootstrap et DI
Service d'accès config.service.ts API publique immutable
Schéma Joi config.schema.ts ~200 lignes, toutes variables
Types TypeScript config.types.ts Interfaces strictes
Loader .env env-file.loader.ts Parsing dotenv
Loader Vault vault.loader.ts Client node-vault
Validateurs validators/*.ts Namespace, secrets, unknown
Utilitaires utils/*.ts Masquage, logging
Tests unitaires *.spec.ts Couverture > 90%
Tests intégration *.e2e-spec.ts Scénarios complets

Annexe A — Schéma Joi complet (structure)

export const configSchema = Joi.object({
  // Application
  APP_ENV: Joi.string().required().valid('dev', 'test', 'prod'),
  APP_URL: Joi.string().uri().required(),

  // HTTP
  HTTP_PORT: Joi.number().port().default(3000),
  HTTP_CORS_ORIGIN: Joi.string().default('http://localhost:3000'),

  // Database
  DB_HOST: Joi.string().hostname().required(),
  DB_PORT: Joi.number().port().default(5432),
  DB_NAME: Joi.string().required(),
  DB_USER: Joi.string().required(),
  DB_PASSWORD: Joi.string().required(), // Via Vault
  DB_DATABASE_URL: Joi.string().uri(), // Via Vault (optionnel)
  DB_SSL: Joi.boolean().default(false),
  DB_SSL_REJECT_UNAUTHORIZED: Joi.boolean().default(true),
  DB_POOL_MIN: Joi.number().min(0).default(2),
  DB_POOL_MAX: Joi.number().min(1).default(10),
  DB_POOL_IDLE_TIMEOUT: Joi.number().min(0).default(10000),
  DB_POOL_ACQUIRE_TIMEOUT: Joi.number().min(0).default(60000),
  DB_STATEMENT_TIMEOUT: Joi.number().min(0).default(30000),
  DB_LOGGING: Joi.boolean().default(false),
  DB_SLOW_QUERY_THRESHOLD: Joi.number().min(0).default(1000),

  // Redis
  REDIS_HOST: Joi.string().hostname().required(),
  REDIS_PORT: Joi.number().port().default(6379),
  REDIS_DB: Joi.number().min(0).max(15).default(0),
  REDIS_PASSWORD: Joi.string().required(), // Via Vault
  REDIS_TLS_ENABLED: Joi.boolean().required(),

  // JWT
  JWT_SECRET: Joi.string().min(32).required(), // Via Vault
  JWT_EXPIRATION: Joi.string().default('15m'),
  JWT_REFRESH_EXPIRATION: Joi.string().default('7d'),

  // Crypto
  CRYPTO_BCRYPT_SALT_ROUNDS: Joi.number().min(10).max(14).default(12),

  // Rate limiting
  RATE_LIMIT_REGISTRATION_MAX: Joi.number().min(1).default(5),
  RATE_LIMIT_REGISTRATION_WINDOW_MS: Joi.number().min(1000).default(3600000),
  RATE_LIMIT_REGISTRATION_SOFT: Joi.number().min(1).default(3),
  RATE_LIMIT_REGISTRATION_HARD: Joi.number().min(1).default(10),

  // CAPTCHA
  CAPTCHA_PROVIDER: Joi.string().valid('turnstile', 'none').default('turnstile'),
  TURNSTILE_SECRET_KEY: Joi.string().when('CAPTCHA_PROVIDER', {
    is: 'turnstile',
    then: Joi.required(), // Via Vault
    otherwise: Joi.optional(),
  }),
  TURNSTILE_TIMEOUT: Joi.number().min(1000).default(5000),

  // SMTP
  SMTP_HOST: Joi.string().hostname().required(),
  SMTP_PORT: Joi.number().port().default(587),
  SMTP_SECURE: Joi.boolean().default(false),
  SMTP_USER: Joi.string().required(), // Via Vault
  SMTP_PASS: Joi.string().required(), // Via Vault
  SMTP_FROM: Joi.string().email().required(),

  // Email worker
  EMAIL_WORKER_INTERVAL_MS: Joi.number().min(1000).default(60000),
  EMAIL_MAX_RETRIES: Joi.number().min(0).default(3),

  // S3
  S3_ENDPOINT: Joi.string().uri().required(),
  S3_REGION: Joi.string().required(),
  S3_BUCKET: Joi.string().required(),
  S3_ACCESS_KEY_ID: Joi.string().required(), // Via Vault
  S3_SECRET_ACCESS_KEY: Joi.string().required(), // Via Vault

  // Glacier
  GLACIER_ENABLED: Joi.boolean().default(false),
  GLACIER_REGION: Joi.string().when('GLACIER_ENABLED', {
    is: true,
    then: Joi.required(),
  }),
  GLACIER_VAULT_NAME: Joi.string().when('GLACIER_ENABLED', {
    is: true,
    then: Joi.required(),
  }),

  // Internal
  INTERNAL_API_KEY: Joi.string().min(32).required(), // Via Vault
  INTERNAL_ALLOWED_NETWORKS: Joi.string().default('127.0.0.1/32'),

  // Vault bootstrap
  VAULT_ADDR: Joi.string().uri().required(),
  VAULT_ROLE_ID: Joi.string().required(),
  VAULT_SECRET_ID: Joi.string().required(),
  VAULT_DYNAMIC_DB_ENABLED: Joi.boolean().default(false),
  VAULT_DB_ROLE: Joi.string().when('VAULT_DYNAMIC_DB_ENABLED', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),

  // CloudHSM (PKCS#11)
  CLOUDHSM_LIBRARY_PATH: Joi.string().default('/opt/cloudhsm/lib/libcloudhsm_pkcs11.so'),
  CLOUDHSM_SLOT: Joi.number().min(0).default(0),
  CLOUDHSM_USER: Joi.string().default('crypto_user'),
  CLOUDHSM_PIN: Joi.string().required(), // Via Vault
  CLOUDHSM_SESSION_TIMEOUT: Joi.number().min(0).default(300000),
  CLOUDHSM_MAX_SESSIONS: Joi.number().min(1).default(10),
}).options({
  abortEarly: false,
  allowUnknown: false,
  stripUnknown: false,
});

Annexe B — Interface ConfigService

export interface IConfigService {
  /**
   * Récupère une valeur de configuration typée
   * @throws ConfigError si la clé n'existe pas
   */
  get<T extends keyof AppConfig>(key: T): AppConfig[T];

  /**
   * Vérifie si l'environnement est celui spécifié
   */
  isEnv(env: 'dev' | 'test' | 'prod'): boolean;

  /**
   * Retourne l'environnement courant
   */
  getEnv(): 'dev' | 'test' | 'prod';
}

Document généré conformément à la spécification PD-22-specification.md Version : 1.0 Date : 2025-12-29