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_ROLEetVAULT_DYNAMIC_DB_ENABLEDsont 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.<APP_ENV>"]
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/<APP_ENV>"]
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¶
- Principe du moindre privilège
- ConfigService expose uniquement
get<T>(key), pas l'objet complet -
Pas de méthode
getAll()oudump() -
Defense in depth
- Validation namespace AVANT chargement Vault
- Validation source secrets AVANT connexion Vault
-
Validation Joi APRÈS fusion complète
-
Audit trail
- Journalisation de chaque valeur par défaut appliquée
- Journalisation (masquée) de la configuration finale
-
Horodatage du chargement
-
Fail-secure
- Tout échec = arrêt immédiat
- 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/configdisponible 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.logavec données config non masquées -
SECRET_KEYSsynchronisé avec spec section 10.2 -
ALLOWED_NAMESPACESsynchronisé avec spec section 10.1 - Tests couvrent tous les codes d'erreur
- Pas de
anydans 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¶
- Ordre de déploiement
- Vault configuré et secrets présents AVANT déploiement backend
-
Variables VAULT_* dans CI/CD secrets
-
Rollback
- Configuration statique = rollback simple
-
Pas d'état à gérer
-
Monitoring
- Alerter sur erreurs CONFIG_* au démarrage
-
Dashboard temps de démarrage (inclut latence Vault)
-
Documentation ops
- Procédure ajout nouveau secret dans Vault
- 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