PD-47 — Plan d'implémentation (v2)¶
1. Découpage en composants¶
1.1 Backend — ProbatioVault-backend (src/backup/)¶
| ID | Composant | Responsabilité | Fichier(s) |
|---|---|---|---|
| MOD-01 | BackupModule | Enregistrement NestJS, wiring DI, import TypeORM entities | src/backup/backup.module.ts |
| MOD-02 | BackupOrchestratorService | Orchestration flux F1/F3 (cron), coordination export→hash→encrypt→upload→journal→cleanup, retry avec backoff exponentiel, circuit-breaker replanification | src/backup/services/backup-orchestrator.service.ts |
| MOD-03 | BackupStateService | Machine d'états §5.7 : validation des transitions, rejet des transitions interdites, journalisation des refus | src/backup/services/backup-state.service.ts |
| MOD-04 | BackupEncryptionService | Chiffrement AES-256-GCM par DEK éphémère, wrapping DEK avec K_backup (Vault), support rotation K_backup, destruction DEK en mémoire après usage | src/backup/services/backup-encryption.service.ts |
| MOD-05 | BackupS3Service | Upload S3 avec SSE-KMS, génération clé objet (§5.1 patterns), vérification post-upload (HeadObject + ETag), multipart pour artefacts > 100 MB | src/backup/services/backup-s3.service.ts |
| MOD-06 | BackupHashService | Calcul SHA3-256 sur données en clair (avant chiffrement), vérification hash post-restauration | src/backup/services/backup-hash.service.ts |
| MOD-07 | BackupJournalService | Écriture append-only (INSERT uniquement, ni UPDATE ni DELETE), déduplication par backup_id via UPSERT ON CONFLICT DO NOTHING, validation format §5.1 [CORR M-03] | src/backup/services/backup-journal.service.ts |
| MOD-08 | BackupWatchdogService | Vérification indépendante présence backup du jour en S3 (04:00 Europe/Paris), émission alerte, relance unique via queue BullMQ dédiée backup-relaunch [CORR B-02], escalade critique si relance échoue. Ne dépend pas de l'état orchestrateur. | src/backup/services/backup-watchdog.service.ts |
| MOD-09 | BackupReconciliationService | Détection d'incohérences état↔objet↔journal au redémarrage pour tout état non-terminal (SCHEDULED, RUNNING) [CORR M-07], réconciliation avec trace observable (log + événement audit) | src/backup/services/backup-reconciliation.service.ts |
| MOD-10 | BackupRestorationService | Flux F4 : restauration PITR en staging, vérification hash SHA3-256 avant validation, validation cohérence schéma/données, enregistrement preuve | src/backup/services/backup-restoration.service.ts |
| MOD-11 | BackupValidationService | Validation des formats contractuels §5.1 (regex, types, bornes), rejet avec code FAILED_FORMAT | src/backup/services/backup-validation.service.ts |
| MOD-12 | BackupConfigService | Chargement configuration (env/vault), validation bornes §5.8, rejet (throw) pour paramètres marqués "rejet config" dans la spec, clamp uniquement pour paramètres marqués "clamp" [CORR M-02] | src/backup/services/backup-config.service.ts |
| MOD-13 | Entities + Migration | BackupExecution (machine d'états), BackupJournalEvent (journal append-only), migration TypeORM | src/backup/entities/*.ts, src/migrations/ |
| MOD-14 | Tests | Unitaires, intégration, roundtrip encrypt-decrypt, state machine exhaustive. Test runner : Jest (aligné stack NestJS existante) [CORR M-08] | src/backup/__tests__/ |
1.2 Infra — ProbatioVault-infra¶
| ID | Composant | Responsabilité | Fichier(s) |
|---|---|---|---|
| MOD-15 | Terraform KMS + IAM + S3 | Clé KMS dédiée backup, user IAM backup isolé, upgrade SSE-KMS sur bucket backups, policy HTTPS-only | terraform/kms-backup.tf, terraform/iam-backup.tf, terraform/storage-aws-core.tf (modification) |
| MOD-16 | Ansible backup provisioning | Installation binaires pg (pg_dump, pg_basebackup, pg_receivewal), service systemd pg_receivewal, scripts helper | ansible/roles/postgresql_backup/ |
1.3 Diagramme de dépendances¶
BackupModule
├── BackupOrchestratorService
│ ├── BackupStateService
│ ├── BackupEncryptionService ← Vault (K_backup)
│ ├── BackupS3Service ← AWS SDK (SSE-KMS via KMS key)
│ ├── BackupHashService
│ ├── BackupJournalService
│ ├── BackupValidationService
│ └── BackupConfigService
├── BackupWatchdogService (indépendant — pas de dépendance vers OrchestratorService) [CORR B-02]
│ ├── BackupS3Service
│ ├── AlertService (existant)
│ └── BullMQ Queue 'backup-relaunch' (producteur) ← relance indirecte F1
├── BackupRelaunchConsumer (consomme queue 'backup-relaunch') [CORR B-02]
│ └── BackupOrchestratorService ← déclenche runDailyBackup()
├── BackupReconciliationService
│ ├── BackupStateService
│ ├── BackupS3Service
│ └── BackupJournalService
└── BackupRestorationService
├── BackupS3Service
├── BackupEncryptionService
├── BackupHashService
└── BackupJournalService
[CORR B-02] Le watchdog ne dépend PAS de l'orchestrateur. La relance F1 est déclenchée via une queue BullMQ dédiée backup-relaunch. Le watchdog (producteur) envoie un job dans la queue. Un BackupRelaunchConsumer (consommateur, séparé du watchdog) injecte BackupOrchestratorService et appelle runDailyBackup(). Ainsi : - Le watchdog reste indépendant (CC-07 respecté : pas d'injection de BackupOrchestratorService) - La relance est découplée et observable (job BullMQ tracé) - Si la relance échoue, le consommateur émet l'escalade critique via AlertService
2. Flux techniques¶
2.1 F1 — Sauvegarde logique quotidienne¶
[Cron 03:00 Europe/Paris]
│
▼
OrchestratorService.runDailyBackup()
│
├─ 1. ConfigService.validate() — vérification config runtime
├─ 2. StateService.create(backup_id, SCHEDULED)
├─ 3. StateService.transition(SCHEDULED → RUNNING)
├─ 4. child_process.spawn('pg_dump', ['-Fc', '-Z9', DB_URL])
│ └─ Stream piped vers buffer temporaire (/tmp/bkp-*.dump)
├─ 5. HashService.computeSHA3_256(cleartext_stream) — hash sur dump compressé, AVANT chiffrement
├─ 6. EncryptionService.encrypt(cleartext_stream)
│ ├─ Génération DEK (crypto.randomBytes(32))
│ ├─ AES-256-GCM encrypt dump → ciphertext
│ ├─ Wrap DEK avec K_backup (Vault Transit)
│ └─ DEK cleartext zéroïsé en mémoire
├─ 7. Suppression artefact clair (/tmp/bkp-*.dump)
│ └─ Timer: DOIT être < 300s après fin export (étape 4)
├─ 8. S3Service.upload(ciphertext, s3_key='YYYY/MM/backup_YYYYMMDD.enc', SSE-KMS)
│ └─ Metadata S3: wrapped_dek, iv, auth_tag, k_backup_version (copie redondante) [CORR B-03]
├─ 9. S3Service.verifyPostUpload(s3_key) — HeadObject + vérification SHA3-256 post-download [CORR M-04]
├─ 10. JournalService.writeEvent(backup_id, backup_type='pg_dump', status='SUCCESS', hash, size, duration)
├─ 11. StateService.transition(RUNNING → SUCCESS)
└─ 12. Suppression ciphertext local
[CORR M-04] Étape 9 — Vérification post-upload : au lieu de se fier uniquement à l'ETag S3 (qui est un MD5 ou un hash multipart, non lié à SHA3-256), le service effectue un GetObject partiel (ou complet si < 100 MB), déchiffre applicativement, et recalcule le SHA3-256 pour comparaison avec le hash de l'étape 5. Cela garantit l'intégrité SHA3-256 contractuelle de bout en bout.
[CORR M-06] Gestion des erreurs dans F1 — garantie append-only en cas d'échec précoce : - Toute exécution de F1 est wrappée dans un try/finally. Le bloc finally garantit l'écriture d'au moins un événement journal (status=FAILED avec le code erreur), même si l'échec survient avant l'upload (étapes 4-6). - Échec étapes 4-6 : abort immédiat, purge cleartext, RUNNING → FAILED, alerte critique, + événement journal garanti par finally [CORR M-06] - Échec étape 8 (upload) : retry max 3 avec backoff exponentiel (delay_initial × factor^n), puis RUNNING → FAILED - Échec étape 9 (hash invalide post-download) : objet marqué invalide, RUNNING → FAILED, alerte critique - Timer cleartext (étape 7) : vérifié par assertion Date.now() - export_end_ts < 300_000; violation = arrêt + alerte
2.2 F2 — Archivage WAL continu¶
[pg_receivewal — service systemd permanent]
│
▼
pg_receivewal --slot=pv_backup --directory=/var/wal-archive/
│
├─ Pour chaque segment WAL reçu:
│ ├─ 1. HashService.computeSHA3_256(segment) — hash avant chiffrement
│ ├─ 2. EncryptionService.encrypt(segment)
│ │ ├─ DEK éphémère par segment
│ │ └─ Wrap DEK avec K_backup
│ ├─ 3. S3Service.upload(ciphertext, s3_key='wal/{WAL_SEGMENT_NAME}.enc', SSE-KMS)
│ ├─ 4. JournalService.writeEvent(wal_backup_id, backup_type='wal', hash, size)
│ └─ 5. Suppression segment clair local
│
└─ Monitoring: lag P95 < 5 min (métrique Prometheus/CloudWatch)
Architecture pg_receivewal : processus systemd déployé par Ansible, connecté au PostgreSQL OVH via streaming replication. Le NestJS BackupModule expose un endpoint interne (ou lit un fichier de métriques) pour le monitoring du lag.
Hypothèse critique : pg_receivewal nécessite un slot de réplication sur le PostgreSQL OVH (H-TECH-01, cf. §8).
2.3 F3 — Sauvegarde physique hebdomadaire¶
Identique à F1 sauf : - Cron dimanche 02:00 Europe/Paris - Commande : pg_basebackup -D /tmp/basebackup -Ft -z - backup_type = 'basebackup' - Hash SHA3-256 calculé sur l'archive tar compressée, AVANT chiffrement (décision technique pour R-02)
2.4 F4 — Restauration de validation (staging)¶
RestorationService.runValidation(target_timestamp)
│
├─ 1. Sélection du backup logique le plus récent avant target_timestamp
├─ 2. S3Service.download(backup_s3_key) → ciphertext
├─ 3. EncryptionService.decrypt(ciphertext, wrapped_dek, k_backup_version)
├─ 4. HashService.verifySHA3_256(cleartext, expected_hash) — AVANT toute restauration
│ └─ Si invalide → abort + alerte critique (ERR-47-04)
├─ 5. Restauration PostgreSQL staging (pg_restore)
├─ 6. Download + déchiffrement WAL segments entre backup et target_timestamp
├─ 7. Rejouage WAL (recovery.conf / recovery_target_time)
├─ 8. Validation cohérence schéma + données métier (jeu contractuel minimal)
├─ 9. JournalService.writeEvent(..., 'RESTORE_VALIDATED')
└─ 10. Enregistrement preuve de test (rapport exportable)
[CORR B-01] En cas d'échec de la restauration trimestrielle (ERR-47-08), le service ouvre un incident conformité et interdit la clôture sans plan d'action. Voir TC-ERR-08 ci-dessous pour le mapping corrigé.
2.5 Diagrammes Mermaid¶
2.5.1 Graphe de dépendances des composants¶
graph TD
subgraph BackupModule
MOD01[BackupModule<br/>MOD-01]
MOD02[BackupOrchestratorService<br/>MOD-02]
MOD03[BackupStateService<br/>MOD-03]
MOD04[BackupEncryptionService<br/>MOD-04]
MOD05[BackupS3Service<br/>MOD-05]
MOD06[BackupHashService<br/>MOD-06]
MOD07[BackupJournalService<br/>MOD-07]
MOD08[BackupWatchdogService<br/>MOD-08]
MOD09[BackupReconciliationService<br/>MOD-09]
MOD10[BackupRestorationService<br/>MOD-10]
MOD11[BackupValidationService<br/>MOD-11]
MOD12[BackupConfigService<br/>MOD-12]
MOD13[Entities + Migration<br/>MOD-13]
MOD14[Tests<br/>MOD-14]
CONSUMER[BackupRelaunchConsumer]
end
subgraph Infra
MOD15[Terraform KMS + IAM + S3<br/>MOD-15]
MOD16[Ansible backup provisioning<br/>MOD-16]
end
subgraph External
VAULT[(Vault Transit<br/>K_backup)]
AWS_S3[(AWS S3<br/>SSE-KMS)]
AWS_KMS[(AWS KMS)]
BULLMQ[(BullMQ Queue<br/>backup-relaunch)]
ALERT[AlertService<br/>existant]
PG[(PostgreSQL OVH)]
end
MOD01 --> MOD02
MOD01 --> MOD08
MOD01 --> MOD09
MOD01 --> MOD10
MOD02 --> MOD03
MOD02 --> MOD04
MOD02 --> MOD05
MOD02 --> MOD06
MOD02 --> MOD07
MOD02 --> MOD11
MOD02 --> MOD12
MOD08 --> MOD05
MOD08 --> ALERT
MOD08 --> BULLMQ
CONSUMER --> BULLMQ
CONSUMER --> MOD02
MOD09 --> MOD03
MOD09 --> MOD05
MOD09 --> MOD07
MOD10 --> MOD05
MOD10 --> MOD04
MOD10 --> MOD06
MOD10 --> MOD07
MOD04 --> VAULT
MOD05 --> AWS_S3
AWS_S3 --> AWS_KMS
MOD15 --> AWS_KMS
MOD15 --> AWS_S3
MOD16 --> PG
style MOD08 fill:#f9f,stroke:#333,stroke-width:2px
style CONSUMER fill:#f9f,stroke:#333,stroke-width:2px
style BULLMQ fill:#ff9,stroke:#333,stroke-width:2px Note : Le watchdog (MOD-08, en violet) est volontairement decoupled de l'orchestrateur (MOD-02). La relance passe par la queue BullMQ (en jaune) consommee par BackupRelaunchConsumer, conformement a CC-07 et [CORR B-02].
2.5.2 Sequence F1 — Sauvegarde logique quotidienne¶
sequenceDiagram
participant Cron as Cron 03:00
participant Orch as OrchestratorService<br/>(MOD-02)
participant Cfg as ConfigService<br/>(MOD-12)
participant State as StateService<br/>(MOD-03)
participant PG as pg_dump
participant Hash as HashService<br/>(MOD-06)
participant Enc as EncryptionService<br/>(MOD-04)
participant Vault as Vault Transit
participant S3 as S3Service<br/>(MOD-05)
participant Journal as JournalService<br/>(MOD-07)
Cron->>Orch: runDailyBackup()
Orch->>Cfg: validate()
Cfg-->>Orch: config OK
Orch->>State: create(backup_id, SCHEDULED)
Orch->>State: transition(SCHEDULED, RUNNING)
Orch->>PG: spawn pg_dump -Fc -Z9
PG-->>Orch: dump compresse /tmp/bkp-*.dump
Orch->>Hash: computeSHA3_256(cleartext)
Hash-->>Orch: hash_sha3
Orch->>Enc: encrypt(cleartext)
Enc->>Enc: DEK = randomBytes(32)
Enc->>Enc: AES-256-GCM encrypt
Enc->>Vault: wrap DEK avec K_backup
Vault-->>Enc: wrapped_dek
Enc->>Enc: DEK.fill(0) zeroisation
Enc-->>Orch: ciphertext + wrapped_dek + iv + authTag
Orch->>Orch: fs.unlink(cleartext) < 300s
Orch->>S3: upload(ciphertext, SSE-KMS)
Note over S3: metadata: wrapped_dek, iv,<br/>authTag, k_backup_version
Orch->>S3: verifyPostUpload(s3_key)
S3->>S3: GetObject + decrypt + SHA3-256
S3-->>Orch: hash match OK
Orch->>Journal: writeEvent(backup_id, SUCCESS, hash, size, duration)
Orch->>State: transition(RUNNING, SUCCESS)
Orch->>Orch: fs.unlink(ciphertext) 2.5.3 Sequence F4 — Restauration de validation¶
sequenceDiagram
participant Restore as RestorationService<br/>(MOD-10)
participant S3 as S3Service<br/>(MOD-05)
participant Enc as EncryptionService<br/>(MOD-04)
participant Hash as HashService<br/>(MOD-06)
participant PGR as pg_restore
participant Journal as JournalService<br/>(MOD-07)
Restore->>S3: download(backup_s3_key)
S3-->>Restore: ciphertext
Restore->>Enc: decrypt(ciphertext, wrapped_dek, k_version)
Enc-->>Restore: cleartext
Restore->>Hash: verifySHA3_256(cleartext, expected_hash)
alt Hash invalide
Hash-->>Restore: MISMATCH
Restore->>Restore: abort + alerte critique ERR-47-04
else Hash valide
Hash-->>Restore: OK
Restore->>PGR: pg_restore (staging)
PGR-->>Restore: restore OK
Restore->>S3: download WAL segments
Restore->>PGR: rejouage WAL (recovery_target_time)
Restore->>Restore: validation coherence schema + donnees
Restore->>Journal: writeEvent(RESTORE_VALIDATED)
Restore->>Restore: enregistrement preuve
end 2.5.4 Machine d'etats BackupExecution¶
stateDiagram-v2
[*] --> SCHEDULED
SCHEDULED --> RUNNING : demarrage backup
RUNNING --> SUCCESS : backup + upload + verify OK
RUNNING --> FAILED : erreur (export, chiffrement, upload, hash)
FAILED --> SCHEDULED : replanification<br/>(circuit-breaker max 3 cycles)
SUCCESS --> EXPIRED : lifecycle S3 30 jours
EXPIRED --> DELETED : suppression confirmee
DELETED --> [*]
note right of FAILED
Retry upload (max 3) reste<br/>dans RUNNING.<br/>FAILED→SCHEDULED = nouvelle tentative.
end note 3. Mapping invariants → mécanismes¶
| Invariant ID | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-47-01 | Aucun artefact clair > 300s après export | Timer strict dans OrchestratorService : cleartext_ttl = Date.now() - export_end_ts. Si > 300_000ms → arrêt + alerte. Suppression immédiate via fs.unlink + fs.access de vérification. | MOD-02 | Timestamp export_end dans journal, absence de fichier clair confirmée par check fs | pg_dump lent sur grosse base → timer déclenché prématurément si export > 300s. Mitigation : timer commence APRÈS fin d'export, pas pendant. |
| INV-47-02 | Double chiffrement AES-256-GCM + SSE-KMS | Couche 1 : crypto.createCipheriv('aes-256-gcm', dek) côté Node.js. Couche 2 : ServerSideEncryption: 'aws:kms' + SSEKMSKeyId côté AWS SDK. | MOD-04 + MOD-05 + MOD-15 (KMS) | Métadonnée S3 x-amz-server-side-encryption: aws:kms + contenu indéchiffrable sans DEK | Si KMS key indisponible → upload échoue (fail-closed, correct). |
| INV-47-03 | Clé backup séparée de clé maître | K_backup dérivée dans Vault Transit (chemin dédié transit/keys/pv-backup-*), séparée de pv-master-*. Version taggée pour rotation. | MOD-04 + Vault | Chemin Vault distinct observable. TC-INV-03 vérifie que les key IDs sont différents. | Rotation K_backup : les anciennes versions doivent rester accessibles pendant la fenêtre de rétention (30 jours min). |
| INV-47-04 | Bucket sans accès public, IAM dédiée | aws_s3_bucket_public_access_block (existant), IAM user backup dédié (MOD-15), bucket policy HTTPS-only (existant). | MOD-15 | Terraform plan montre block_public=true. aws s3api get-public-access-block retourne tout bloqué. | Déjà en place (cf. storage-aws-core.tf). IAM backup user à créer séparément du backend user. |
| INV-47-05 | >= 1 événement append-only par tentative, dédup par backup_id | BackupJournalEvent table INSERT-only (pas de UPDATE/DELETE via RLS policy). UPSERT avec ON CONFLICT (backup_id) DO NOTHING. Garantie try/finally dans tous les flux [CORR M-06]. [CORR M-03] Déduplication par backup_id seul (aligné spec INV-47-05 : "dédupliqués par backup_id"). | MOD-07 + MOD-13 | SELECT count(*) FROM backup_journal_events WHERE backup_id = $1 >= 1 pour chaque exécution | RLS mal configurée → UPDATE possible. Mitigation : TC-INV-05 tente un UPDATE et vérifie rejet. |
| INV-47-06 | Watchdog indépendant de l'orchestrateur | BackupWatchdogService : cron NestJS séparé (04:00), ne partage aucune instance avec OrchestratorService, lit directement S3 (pas la DB d'état). Relance via queue BullMQ backup-relaunch [CORR B-02]. | MOD-08 | Si orchestrateur crashé (process kill), watchdog détecte absence backup. TC-INV-06 injecte panne orchestrateur. | Si tout le process NestJS est down → watchdog aussi down. Mitigation : watchdog Ansible cron comme backup du watchdog NestJS (défense en profondeur). |
| INV-47-07 | Machine d'états §5.7 stricte | BackupStateService.transition(from, to) : table de transitions autorisées en code. Toute transition hors table → BackupStateTransitionError + journal refus. | MOD-03 | Log de refus avec from, to, backup_id, timestamp. TC-INV-07 teste chaque transition interdite. | État corrompu en DB → machine d'états contournée. Mitigation : contrainte CHECK en DB sur status enum. |
| INV-47-08 | Hash SHA3-256 vérifié avant validation restauration | RestorationService : appel HashService.verify() AVANT pg_restore. Si hash invalide → abort immédiat, pas de restauration. | MOD-10 + MOD-06 | Log d'abort avec hash attendu vs calculé. TC-INV-08 corrompt un artefact et vérifie le rejet. | Hash calculé sur mauvais objet (chiffré au lieu de clair). Mitigation : hash toujours calculé sur le clair APRÈS déchiffrement. |
| INV-47-09 | Envelope encryption, DEK jamais en clair au repos | DEK générée en mémoire (crypto.randomBytes(32)), utilisée pour chiffrement, wrappée par K_backup (Vault Transit encrypt), DEK cleartext zéroïsée (buffer.fill(0)). Source de vérité du wrapped DEK : DB (champ wrappedDek + iv + authTag dans entity BackupExecution). Copie redondante en métadonnée S3 pour récupération d'urgence [CORR B-03]. | MOD-04 | TC-INV-09 : scan base + disque post-backup, vérification absence de 32 bytes DEK pattern. Wrapped DEK présente en DB et en métadonnée S3 (copie). |
4. Mapping critères d'acceptation → mécanismes¶
| Critère ID | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-47-01 | Cron 03:00 Europe/Paris + flux F1 complet | MOD-02 + MOD-05 | aws s3 ls s3://bucket/YYYY/MM/backup_YYYYMMDD.enc avant 04:00. Journal event backup_type=pg_dump, status=SUCCESS. | pg_dump > 60 min → backup absent à 04:00. |
| CA-47-02 | pg_receivewal continu + monitoring lag | MOD-16 (Ansible) + MOD-02 (monitoring) | Métrique wal_receiver_lag_p95 < 5min sur 24h. | Slot de réplication non disponible sur OVH (H-TECH-01). |
| CA-47-03 | AES-256-GCM (MOD-04) + SSE-KMS (MOD-05/MOD-15) | MOD-04 + MOD-05 + MOD-15 | Preuve 1 : contenu illisible sans DEK. Preuve 2 : HeadObject retourne ServerSideEncryption: aws:kms. | — |
| CA-47-04 | Timer 300s + fs.unlink + vérification | MOD-02 | Trace audit : cleartext_deleted_at - export_completed_at < 300s. fs.access() confirme absence. | — |
| CA-47-05 | Lifecycle S3 30 jours (existant) + transitions EXPIRED→DELETED | MOD-15 (existant) + MOD-03 | Objet J+31 absent (vérification S3 ListObjects). Transition EXPIRED→DELETED journalisée. | — |
| CA-47-06 | JournalService INSERT-only + dédup par backup_id [CORR M-03] | MOD-07 | count(*) >= 1 par backup_id. Tentative doublon → DO NOTHING (pas d'erreur, pas de duplication). | — |
| CA-47-07 | RestorationService flux F4 + hash verify | MOD-10 + MOD-06 | Rapport restauration : hash OK, schéma OK, données OK. Preuve exportable. | Jeu de cohérence données non défini (Q-47-03). |
| CA-47-08 | WatchdogService indépendant + relance via BullMQ [CORR B-02] + fallback Ansible cron | MOD-08 + MOD-16 | Watchdog détecte absence malgré orchestrateur down. Alerte émise. Relance via queue. Escalade si relance échoue. | — |
| CA-47-09 | StateService.transition() avec table autorisée | MOD-03 | 0 transition hors §5.7 dans logs sur 30 jours. | — |
5. Mapping tests (TC-*) → mécanismes + observables¶
5.1 Tests nominaux¶
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
|---|---|---|---|---|
| TC-NOM-01 | F1, INV-01/02/05, CA-01/03/04/06 | OrchestratorService.runDailyBackup() en environnement de test avec pg_dump mock, S3 localstack, clock override Europe/Paris | Objet S3 présent, chiffrement double couche vérifié, cleartext absent après 300s, journal >= 1 event | Intégration |
| TC-NOM-02 | F2, INV-02/05, CA-02/06 | pg_receivewal en test avec PostgreSQL local + WAL generation, monitoring lag mock. Prérequis : H-TECH-01 levée (slot de réplication OVH confirmé). Si H-TECH-01 non levée, ce test est SKIP avec justification tracée. [CORR m-04] | Lag P95 < 5min, objets WAL chiffrés, hash SHA3-256 par segment | Intégration |
| TC-NOM-03 | F3, INV-02/05, CA-03/06 | OrchestratorService.runWeeklyBasebackup() similaire à TC-NOM-01 | Objet basebackup.enc présent, journal event backup_type=basebackup | Intégration |
| TC-NOM-04 | F4, INV-08, CA-07 | RestorationService.runValidation() avec artefacts pré-chargés en localstack | Hash validé, pg_restore réussi, cohérence vérifiée, preuve enregistrée | Intégration |
| TC-NOM-05 | F1, INV-06, CA-08 | WatchdogService avec orchestrateur mock down, S3 sans backup du jour, queue BullMQ mockée [CORR B-02] | Alerte émise, job de relance envoyé dans queue, escalade si échec | Unitaire + Intégration |
| TC-NOM-06 | F3/§5.7/§5.8, INV-07, CA-05/09 | StateService : simulation rétention 30j, transitions SUCCESS→EXPIRED→DELETED | Transitions conformes, objet J+31 absent (mock S3 lifecycle) | Unitaire |
| TC-NOM-07 | §5.6 crash pré-commit | OrchestratorService : interruption forcée avant StateService.transition(→SUCCESS) | Pas de SUCCESS en DB pour cette tentative | Unitaire |
| TC-NOM-08 | §5.6 crash post-commit | ReconciliationService.reconcile() après insertion état incohérent (SUCCESS en DB, pas d'objet S3) | Réconciliation exécutée, trace observable dans journal, état corrigé | Intégration |
5.2 Tests d'erreur¶
| Test ID | Référence spec | Mécanisme(s) | Point(s) d'observation | Niveau |
|---|---|---|---|---|
| TC-ERR-01 | ERR-47-01 | OrchestratorService avec pg_dump mock qui échoue | Status FAILED, retry_count <= 3, alerte critique après épuisement. Événement journal garanti même en échec précoce [CORR M-06] | Unitaire |
| TC-ERR-02 | ERR-47-02 | EncryptionService.encrypt() avec erreur simulée (ex: Vault indisponible) | Pas d'upload, status FAILED, cleartext purgé, alerte critique. Événement journal garanti [CORR M-06] | Unitaire |
| TC-ERR-03 | ERR-47-03 | S3Service.upload() avec erreur réseau simulée | Retry <= 3 (même tentative, pas de FAILED→SCHEDULED), puis FAILED | Unitaire |
| TC-ERR-04 | ERR-47-04 | HashService.verifySHA3_256() retourne false après download post-upload [CORR M-04] | Objet marqué invalide, status FAILED, alerte critique | Unitaire |
| TC-ERR-05 | ERR-47-05 | WatchdogService sans backup du jour, relance via BullMQ, relance échoue [CORR B-02] | Alerte + job relance dans queue + escalade critique | Unitaire |
| TC-ERR-06 | ERR-47-06 | pg_receivewal avec interruption réseau simulée | Alerte immédiate, reprise automatique, contrôle rattrapage | Intégration |
| TC-ERR-07 | ERR-47-07 | ValidationService avec données hors format (backup_id uppercase, hash non-hex, etc.) | Rejet FAILED_FORMAT, journal d'erreur | Unitaire |
| TC-ERR-08 | ERR-47-08 | RestorationService.runValidation() avec restauration trimestrielle qui échoue (pg_restore fail, cohérence données invalide, OU hash invalide pré-restore) [CORR B-01] | Incident conformité ouvert, clôture bloquée sans plan d'action. Le test vérifie : (1) un incident est créé dans le système, (2) le flag restorationIncidentOpen bloque la clôture, (3) seule la soumission d'un plan d'action débloque. | Unitaire |
[CORR B-01] TC-ERR-08 est désormais aligné sur ERR-47-08 (échec restauration trimestrielle → incident conformité + interdiction clôture sans plan d'action). L'ancien mapping sur ERR-47-04 (hash invalide post-upload) est couvert par TC-ERR-04.
5.3 Tests d'invariants (TC-INV-*)¶
Décision technique (réponse à R-03) : chaque TC-INV reçoit un scénario GIVEN/WHEN/THEN explicite pour guider l'implémentation.
| Test ID | GIVEN | WHEN | THEN | Niveau |
|---|---|---|---|---|
| TC-INV-01 | Un backup F1 complet en cours (export terminé, clair sur disque) | L'orchestrateur atteint l'étape de suppression | Le fichier clair est absent en < 300s (fs.access → ENOENT). Le delta timestamp est < 300_000ms. | Intégration |
| TC-INV-02 | Un artefact backup uploadé en S3 | HeadObject sur l'artefact | ServerSideEncryption == aws:kms. Le contenu téléchargé est déchiffrable uniquement avec la DEK. | Intégration |
| TC-INV-03 | K_backup dérivée dans Vault, K_master existante | Lecture des deux clés dans Vault Transit | Les key IDs sont distincts, les chemins Vault sont différents. Rotation K_backup : un artefact chiffré avec K_backup_v1 est déchiffrable après rotation vers K_backup_v2. | Intégration |
| TC-INV-04 | Bucket backups provisionné en AWS | Tentative d'accès public (HTTP sans credentials) + get-public-access-block | Accès refusé. Block public = true sur les 4 flags. IAM user backup != IAM user backend (ARN distincts). | Unitaire (Terraform plan) |
| TC-INV-05 | Un backup terminé (SUCCESS) | Lecture journal + tentative UPDATE sur backup_journal_events | >= 1 événement pour ce backup_id. UPDATE rejeté (RLS deny). Insertion doublon (même backup_id) → DO NOTHING, count inchangé. [CORR M-03] | Intégration |
| TC-INV-06 | Orchestrateur NestJS arrêté (process.kill simulé) | Watchdog cron exécuté à 04:00, pas de backup du jour en S3 | Watchdog émet alerte malgré orchestrateur down. Job de relance envoyé dans queue BullMQ. Watchdog ne dépend d'aucune donnée produite par l'orchestrateur. [CORR B-02] | Intégration |
| TC-INV-07 | Un backup en état DELETED | Appel StateService.transition(DELETED, SCHEDULED) | Transition refusée (BackupStateTransitionError). Log de refus avec from=DELETED, to=SCHEDULED. Même test pour SUCCESS→RUNNING, EXPIRED→SUCCESS, FAILED→SUCCESS. | Unitaire |
| TC-INV-08 | Un artefact restauré en staging avec hash SHA3-256 connu | Hash corrompu (1 bit flip) avant appel verify | Vérification échoue. Restauration pg_restore JAMAIS exécutée. Alerte critique. | Unitaire |
| TC-INV-09 | Un backup complet exécuté (F1) | Inspection post-backup : scan table backup_executions, scan /tmp/, scan process memory map | Aucune DEK en clair trouvée. Wrapped DEK présente en DB (source de vérité) et en métadonnée S3 (copie). Table backup_executions ne contient pas de champ DEK cleartext. [CORR B-03] | Intégration |
5.4 Tests négatifs et adversariaux¶
| Test ID | Entrée invalide | Mécanisme | Observable | Niveau |
|---|---|---|---|---|
| TC-NEG-01 | backup_id avec uppercase ou hors regex | ValidationService.validateBackupId() | Rejet FAILED_FORMAT, journal erreur | Unitaire |
| TC-NEG-02 | hash_sha3_256 non hex/non lowercase/pas 64 chars | ValidationService.validateHash() | Rejet + alerte critique | Unitaire |
| TC-NEG-03 | s3_object_key_backup hors pattern | ValidationService.validateS3Key() | Upload rejeté, pas d'objet stocké | Unitaire |
| TC-NEG-04 | Transition SUCCESS → RUNNING | StateService.transition() | Transition refusée + log | Unitaire |
| TC-NEG-05 | Transition DELETED → SCHEDULED | StateService.transition() | Transition refusée + log | Unitaire |
| TC-NEG-06 | Accès public au bucket | Terraform plan validation | Accès refusé | Unitaire (Terraform) |
| TC-NEG-07 | Insertion doublon même backup_id | JournalService.writeEvent() x2 | 1 seul enregistrement (DO NOTHING) [CORR M-03] | Unitaire |
| TC-NEG-08 | Config backupFrequencyHours = 0 (hors bornes, marqué "rejet config" dans spec) | ConfigService.getConfig() | Throw ConfigValidationError (pas de clamp) [CORR M-02] | Unitaire |
5.5 Tests de non-régression¶
| Test ID | Mécanisme | Observable | Niveau |
|---|---|---|---|
| TC-NR-01 | ValidationService : jeu de données valide/invalide exhaustif pour chaque champ §5.1 | 0 régression regex/type/tailles | Unitaire |
| TC-NR-02 | StateService : test exhaustif de la matrice de transitions (6×6 = 36 combinaisons) | 0 transition illégale acceptée | Unitaire |
| TC-NR-03 | Métriques SLA : RPO, RTO, lag, TTL | Conformité §5.8 | Intégration (monitoring) |
| TC-NR-04 | Lifecycle S3 : objet J+31 absent | Vérification lifecycle réel en localstack | Intégration |
| TC-NR-05 | Journal : >= 1 événement par backup_id pour chaque exécution (succès + échec) | count(*) >= 1 | Intégration |
6. Gestion des erreurs¶
6.1 Codes et traitements¶
| Cas | Code erreur | Retry | Transition | Alerte | Action |
|---|---|---|---|---|---|
| ERR-47-01 (échec pg_dump) | BACKUP_EXPORT_FAILED | max 3, backoff exponentiel | RUNNING → FAILED | Critique si épuisé | Purge cleartext, journal erreur. try/finally garantit événement journal [CORR M-06] |
| ERR-47-02 (échec chiffrement) | BACKUP_ENCRYPTION_FAILED | 0 (abort immédiat) | RUNNING → FAILED | Critique immédiat | Purge cleartext obligatoire, pas d'upload. try/finally garantit événement journal [CORR M-06] |
| ERR-47-03 (échec upload) | BACKUP_UPLOAD_FAILED | max 3, backoff exponentiel | RUNNING → FAILED après épuisement | Après épuisement | Retry dans même tentative (pas de FAILED→SCHEDULED) |
| ERR-47-04 (hash invalide post-upload) | BACKUP_INTEGRITY_FAILED | 0 | RUNNING → FAILED | Critique immédiat | Objet marqué invalide dans metadata S3. Vérification SHA3-256 post-download [CORR M-04] |
| ERR-47-05 (watchdog absence backup) | WATCHDOG_MISSING_BACKUP | relance unique via BullMQ queue backup-relaunch [CORR B-02] | — | Critique si relance échoue | Job de relance dans queue, consommateur déclenche F1, escalade si échec |
| ERR-47-06 (interruption WAL) | WAL_ARCHIVE_INTERRUPTED | auto (pg_receivewal se reconnecte) | — | Immédiate | Contrôle rattrapage lag |
| ERR-47-07 (format invalide) | FAILED_FORMAT | 0 | — (pas de transition, hors machine d'états) | Journal erreur | Rejet événement |
| ERR-47-08 (échec restauration trimestrielle) | RESTORE_VALIDATION_FAILED | 0 | — | Incident conformité | Ouverture incident, clôture bloquée sans plan d'action [CORR B-01] |
6.2 Circuit-breaker replanification¶
La spec autorise FAILED → SCHEDULED pour replanification après épuisement des retries. Le plan borne cette boucle :
max_replanification_cycles: 3 (configurable, min 1, max 10)- [CORR M-01] Ce paramètre est une décision d'implémentation (protection anti-boucle infinie). La spec n'interdit pas la boucle FAILED→SCHEDULED mais ne la borne pas explicitement. Le circuit-breaker est ajouté pour éviter un cycle infini de replanifications, conformément au principe de résilience opérationnelle.
- Compteur
replanification_countdansBackupExecutionentity - À chaque transition
FAILED → SCHEDULED:replanification_count++ - Si
replanification_count >= max_replanification_cycles: transition refusée, status resteFAILED, escalade critique - Observable : log
circuit_breaker_triggeredavec backup_id, replanification_count
6.3 Backoff exponentiel¶
Paramètres §5.8 : - delay_initial : 1000ms (min 0, max 60000) — clamp hors bornes (spec §5.8 colonne "Hors bornes" = clamp) - factor : 2 (min 1, max 5) — clamp hors bornes (spec §5.8 colonne "Hors bornes" = clamp) - retry_max : 3 (min 0, max 5) — rejet config hors bornes [CORR M-02]
[CORR M-02] Distinction clamp vs rejet selon la spec §5.8 colonne "Hors bornes" : - Paramètres marqués "rejet config" (fréquence backup, heure backup, retry, rétention) : throw ConfigValidationError si la valeur est hors bornes. L'application refuse de démarrer. - Paramètres marqués "clamp" (backoff factor, delay initial) : clamp silencieux aux bornes min/max avec warning en log. - Paramètres marqués "arrêt + alerte" (cleartext TTL) : non configurable (fixé à 300s). - Paramètres marqués "non-conformité" (RPO, RTO, périodicité restauration) : non configurables, écart = incident.
Validation croisée (décision technique pour R-08) : si delay_initial < 100 && factor <= 1, warning en log au démarrage (configuration potentiellement nocive).
7. Impacts sécurité¶
7.1 Surface d'attaque¶
| Vecteur | Mitigation | Composant |
|---|---|---|
| Artefact clair sur disque | Timer 300s strict, suppression immédiate, vérification fs.access | MOD-02 |
| DEK en mémoire | Durée minimisée (stream processing), zéroïsation explicite (buffer.fill(0)) | MOD-04 |
| K_backup dans Vault | Vault Transit (HSM-backed en prod), AppRole auth, chemin dédié transit/keys/pv-backup-* | MOD-04 + Vault |
| Accès bucket S3 | IAM user dédié (pas le backend user), bucket policy HTTPS-only, block public | MOD-15 |
| Credentials pg_dump | Connection string depuis Vault (kv/data/db/postgresql), pas en env vars | MOD-12 |
| Log injection dans journal | ValidationService : validation stricte de tous les champs avant écriture | MOD-11 + MOD-07 |
7.2 Conformité NF Z42-013¶
| Exigence | Mécanisme |
|---|---|
| Intégrité des sauvegardes | Hash SHA3-256 pré-chiffrement + vérification SHA3-256 post-download [CORR M-04] + vérification post-restauration |
| Traçabilité des opérations | Journal append-only (INSERT-only, RLS bloque UPDATE/DELETE). try/finally garantit événement même en échec précoce [CORR M-06] |
| Confidentialité | Double chiffrement (AES-256-GCM + SSE-KMS) |
| Disponibilité | RPO 24h (pg_dump) + RPO < 5min (WAL) + watchdog indépendant |
7.3 Rotation de clé K_backup¶
- Vault Transit supporte la rotation native (versions de clé)
- Nouveaux backups : chiffrés avec la version courante
- Anciens backups : déchiffrables avec la version d'origine (Vault conserve les versions)
- Fenêtre de rétention des versions : >= 30 jours (alignée sur lifecycle S3)
- Décision technique (réponse à R-06) : rétention des versions de clé = durée de rétention S3 (30 jours par défaut, configurable avec lifecycle)
8. Hypothèses techniques¶
| ID | Hypothèse | Impact si faux |
|---|---|---|
| H-TECH-01 | Le PostgreSQL OVH Cloud Database (plan business) expose un slot de réplication accessible par pg_receivewal depuis le VPS. | WAL archiving continu impossible → RPO < 5 min non tenu. Fallback : WAL archiving uniquement via les sauvegardes intégrées OVH (non auditable par ProbatioVault). Action de levée : tester pg_receivewal --create-slot avant implémentation. |
| H-TECH-02 | pg_dump et pg_basebackup sont installables sur le VPS via package manager (ou Ansible) et compatibles avec la version PostgreSQL 15 d'OVH. | Export impossible → tout le flux F1/F3 bloqué. |
| H-TECH-03 | La latence réseau VPS → OVH DB est < 5ms (même datacenter GRA7). | pg_dump pourrait être lent, dépassant la fenêtre SLA de 60 min pour bases < 50 GB. |
| H-TECH-04 | La latence réseau VPS → AWS S3 eu-west-3 est suffisante pour upload d'artefacts < 5 GB en < 30 min. | Timeout upload dépassé, RPO non tenu. |
| H-TECH-05 | Vault Transit est disponible avec clé de type aes256-gcm96 pour le wrapping DEK. | Envelope encryption non réalisable → INV-47-09 non tenu. Pas de fallback : si Vault est indisponible, le backup échoue (fail-closed). [CORR M-05] |
| H-TECH-06 | Le NestJS BackupModule peut coexister avec les modules métier existants sans conflit de scheduling (BullMQ, cron). | Conflits de cron → backup décalé hors fenêtre. Mitigation : BullMQ scheduler dédié pour backups. |
| H-47-01 | Base de référence <= 50 GB (spec) | Dépassement SLA durée backup. |
| H-47-02 | S3 cible supporte SSE-KMS (spec) | Non-conformité INV-47-02. |
| H-47-03 | Journal append-only disponible en mode dégradé (spec) | Perte traçabilité. |
| H-47-04 | Secrets fournis par mécanisme sécurisé séparé (spec) | Blocage backups. |
| H-47-05 | Connectivité réseau suffisante (spec) | RPO/RTO non tenus. |
| H-47-06 | Hôtes synchronisés NTP (spec) | Seuils temporels incohérents. |
[CORR M-05] L'hypothèse H-TECH-05 n'a plus de fallback HKDF local. Si Vault Transit est indisponible, le backup échoue avec code BACKUP_ENCRYPTION_FAILED (fail-closed). Justification : un fallback local non contractualisé dans la spec violerait INV-47-03 (séparation des clés) et INV-47-09 (envelope encryption). L'indisponibilité de Vault est un incident opérationnel à traiter par l'infra, pas par un contournement applicatif.
9. Points de vigilance (risques, dette, pièges)¶
9.1 Risques critiques¶
-
H-TECH-01 (pg_receivewal sur OVH) : C'est le risque le plus élevé du plan. Si les slots de réplication ne sont pas exposés, le RPO < 5 min WAL est impossible à atteindre avec notre architecture. Action : lever cette hypothèse en priorité, AVANT l'implémentation.
-
Timing 300s cleartext (INV-47-01) : Le timer est strict et non configurable. Si le chiffrement est lent (base > 10 GB), le cleartext pourrait persister plus de 300s. Mitigation : chiffrement en streaming (pipe pg_dump → gzip → encrypt → upload) au lieu de matérialiser l'intégralité sur disque.
-
Watchdog NestJS vs process crash : Si le process NestJS crash, le watchdog crash aussi. Mitigation : watchdog cron Ansible en fallback (vérifie S3 directement, indépendant du process NestJS).
9.2 Pièges d'implémentation¶
-
Hash sur le bon objet (R-02) : Le hash SHA3-256 DOIT être calculé sur le clair compressé AVANT chiffrement. Pas sur le ciphertext. Pas sur le clair non compressé. Cohérence F1/F2/F3 : toujours hash(cleartext_compressed).
-
DEK zéroïsation :
Buffer.alloc(32)Node.js n'est pas garanti zéroïsé par le GC. Utiliserbuffer.fill(0)explicitement immédiatement après usage. -
Timezone Europe/Paris avec DST (Q-47-01) : Le cron 03:00 Europe/Paris décale de 1h en été/hiver (CET→CEST). NestJS cron doit utiliser le timezone
Europe/Parisnatif, pas un offset fixe UTC+1. -
FAILED_FORMAT n'est PAS un état : C'est un code de rejet. Il ne doit pas apparaître dans l'enum
statusde BackupExecution. Les rejets de format ne créent pas de BackupExecution. -
Retry upload vs replanification (clarification E-11) : Les retries upload (max 3) sont dans la MÊME tentative (état reste RUNNING). La transition FAILED→SCHEDULED est une NOUVELLE tentative, comptée par le circuit-breaker.
9.3 Dette technique identifiée¶
-
Cohérence données post-restauration (Q-47-03) : Le jeu contractuel de tables/compteurs pour valider la cohérence n'est pas défini. Le plan implémentera une vérification minimale (count tables, dernière transaction) avec extensibilité.
-
Granularité journal WAL (Q-47-02) : Le plan implémente 1 événement par segment WAL (granularité maximale), avec possibilité de lotissement ultérieur si performance insuffisante.
10. Contraintes techniques [CORR m-02]¶
| Contrainte | Valeur | Source |
|---|---|---|
| Langage backend | TypeScript (strict mode) | Stack ProbatioVault-backend |
| Framework backend | NestJS 10+ | Stack existante |
| ORM | TypeORM 0.3+ | Stack existante |
| Test runner | Jest (aligné stack NestJS) [CORR M-08] | Convention projet |
| Module system | CJS (CommonJS) — NestJS ne supporte pas nativement ESM. Si une dépendance exige ESM, utiliser import() dynamique. [CORR M-09] | Convention projet |
| Node.js | >= 18.x (LTS) | Pré-requis crypto.subtle + SHA3 |
| PostgreSQL | 15.x (OVH Cloud Database) | Infra existante |
| S3 SDK | AWS SDK v3 (@aws-sdk/client-s3) | Stack existante |
| Terraform | >= 1.5 | Stack infra existante |
| Ansible | >= 2.14 | Stack infra existante |
| Vault | HashiCorp Vault (Transit engine) | Stack sécurité existante |
| BullMQ | v5+ (queue backup-relaunch pour relance watchdog) [CORR B-02] | Stack existante |
11. Variables CI/CD [CORR m-03]¶
| Variable | Source | Usage |
|---|---|---|
VAULT_ADDR | Vault (kv/data/ci/vault) | URL du serveur Vault |
VAULT_TOKEN | Vault (AppRole auth) | Authentification Vault Transit |
AWS_ACCESS_KEY_ID | Vault (kv/data/ci/aws-backup) | Credentials IAM backup user |
AWS_SECRET_ACCESS_KEY | Vault (kv/data/ci/aws-backup) | Credentials IAM backup user |
AWS_KMS_KEY_ID | Vault (kv/data/ci/aws-backup) | ID de la clé KMS backup |
S3_BACKUP_BUCKET | Vault (kv/data/ci/aws-backup) | Nom du bucket de backup |
DATABASE_URL | Vault (kv/data/db/postgresql) | Connection string PostgreSQL |
PG_REPLICATION_SLOT | Ansible vars | Nom du slot de réplication WAL |
BACKUP_CRON_DAILY | Env / config | Expression cron (défaut: 0 3 * * *) |
BACKUP_CRON_WEEKLY | Env / config | Expression cron (défaut: 0 2 * * 0) |
WATCHDOG_CRON | Env / config | Expression cron (défaut: 0 4 * * *) |
Toutes les variables sensibles sont récupérées depuis Vault au runtime. Aucun secret en variable d'environnement GitLab CI/CD (conformément à la politique sécurité ProbatioVault).
12. Hors périmètre¶
- Sauvegarde des objets documentaires métier (déjà redondés multi-cloud).
- Snapshots d'infrastructure Terraform/Ansible.
- Sauvegarde Redis.
- Ancrage Merkle/blockchain des événements de backup.
- Canal et délai d'escalade d'alerte opérationnelle (Q-47-05 — SLO non contractualisé).
- Politique lifecycle sur versions S3 non courantes (Q-47-04 — clarification à lever hors périmètre code).
Mécanismes cross-module¶
Aucune modification d'autres modules. Le BackupModule est entièrement autonome et n'ajoute aucun guard, middleware ou intercepteur sur des routes d'autres modules (confirmé par §10.2 spec : "Aucune contrainte inter-module applicable : vrai au sens applicatif métier").
Périmètre de test¶
| Niveau de test | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | Tous les composants MOD-02 à MOD-12 (services), MOD-13 (entities), MOD-11 (validation exhaustive §5.1) | — |
| Intégration | Interactions entre services backup (state + journal + encryption + S3), roundtrip encrypt-decrypt, S3 avec localstack, pg_dump mock, reconciliation post-crash | — |
| E2E | Flux complet F1 (pg_dump → encrypt → upload → verify) en staging avec PostgreSQL de test et S3 localstack | Flux F2 WAL continu (dépend de H-TECH-01, testable uniquement si slot de réplication confirmé) [CORR m-04] |
| Infrastructure | Terraform plan validation (KMS, IAM, S3), Ansible role lint | Déploiement réel en prod (hors périmètre story) |
Règles : - Tests d'intégration entre composants MOD-02 à MOD-12 sont toujours in scope - Couverture minimale 80% sur le périmètre in scope - Le flux F2 (WAL) E2E est conditionné à la levée de H-TECH-01 (slot de réplication OVH) [CORR m-04] - Aucun stub inter-PD : cette story est autonome - Test runner : Jest [CORR M-08]
Décisions d'implémentation¶
| ID | Décision | Justification | Base spec |
|---|---|---|---|
| DI-01 | Circuit-breaker max_replanification_cycles = 3 | Protection anti-boucle infinie FAILED→SCHEDULED [CORR M-01] | Pas de base spec directe. Décision d'implémentation pour résilience opérationnelle. La spec autorise FAILED→SCHEDULED sans borne ; le circuit-breaker est ajouté comme garde-fou. |
| DI-02 | Wrapped DEK : DB comme source de vérité [CORR B-03] | Cohérence avec entity BackupExecution qui a le champ wrappedDek. S3 metadata en copie redondante. | Spec §10.4 : "si elle est persistée, uniquement sous forme chiffrée conformément à INV-47-09". |
| DI-03 | Relance watchdog via BullMQ queue [CORR B-02] | Respect CC-07 (pas d'injection orchestrateur dans watchdog) tout en permettant la relance spec ERR-47-05. | Spec ERR-47-05 : "relance contrôlée unique". |
| DI-04 | Pas de fallback HKDF si Vault indisponible [CORR M-05] | Fail-closed. Un contournement local violerait INV-47-03/09. | Spec INV-47-03 + INV-47-09. |
| DI-05 | Vérification SHA3-256 post-download (pas ETag) [CORR M-04] | ETag S3 est MD5 ou hash multipart, non mappable sur SHA3-256. | Spec INV-47-08 : "intégrité hash SHA3-256". |
Registre des corrections Gate 5 v1¶
| ID | Type | Résumé | Section(s) modifiée(s) |
|---|---|---|---|
| B-01 | BLOQUANT | TC-ERR-08 remappé sur ERR-47-08 (échec restauration trimestrielle → incident conformité) | §5.2 TC-ERR-08, §6.1 |
| B-02 | BLOQUANT | Mécanisme de relance watchdog via BullMQ queue backup-relaunch (découplage CC-07) | §1.1 MOD-08, §1.3 diagramme, §3 INV-47-06, §4 CA-47-08, §5 TC-NOM-05/TC-ERR-05/TC-INV-06, §6.1 ERR-47-05 |
| B-03 | BLOQUANT | Wrapped DEK : DB source de vérité, S3 metadata copie redondante | §2.1 étape 8, §3 INV-47-09, §5.3 TC-INV-09, Décisions d'implémentation |
| M-01 | MAJEUR | max_replanification_cycles documenté comme décision d'implémentation | §6.2, Décisions d'implémentation |
| M-02 | MAJEUR | Clamp remplacé par rejet (throw) pour paramètres "rejet config" | §1.1 MOD-12, §5.4 TC-NEG-08, §6.3 |
| M-03 | MAJEUR | Dédup journal aligné sur backup_id seul (spec INV-47-05) | §1.1 MOD-07, §3 INV-47-05, §4 CA-47-06, §5.3 TC-INV-05, §5.4 TC-NEG-07 |
| M-04 | MAJEUR | Vérification SHA3-256 post-download au lieu d'ETag | §2.1 étape 9, §5.2 TC-ERR-04, §7.2, Décisions d'implémentation |
| M-05 | MAJEUR | Fallback HKDF supprimé — fail-closed si Vault indisponible | §8 H-TECH-05, Décisions d'implémentation |
| M-06 | MAJEUR | try/finally pour garantir événement journal en échec précoce | §2.1 gestion erreurs, §3 INV-47-05, §5.2 TC-ERR-01/02, §6.1, §7.2 |
| M-07 | MAJEUR | Réconciliation étendue à tout état non-terminal (SCHEDULED, RUNNING) | §1.1 MOD-09 |
| M-08 | MAJEUR | Test runner spécifié : Jest | §1.1 MOD-14, §10, Périmètre de test |
| M-09 | MAJEUR | Module system documenté : CJS (NestJS), import() dynamique si ESM requis | §10 |
| m-01 | MINEUR | CC-14 invariants alignés avec INV spec | Code contracts (document séparé) |
| m-02 | MINEUR | Section "Contraintes techniques" ajoutée | §10 |
| m-03 | MINEUR | Variables CI/CD documentées | §11 |
| m-04 | MINEUR | TC-NOM-02 conditionné à H-TECH-01 avec prérequis explicite | §5.1 TC-NOM-02, Périmètre de test |