PD-17 — Plan d'implémentation¶
Navigation User Story
| Document | | | ---------- | -- | | [Spécification](PD-17-specification.md) | | | **Plan d'implémentation** | *(ce document)* | | Critères d'acceptation | *(à venir)* | | Retour d'expérience | *(à venir)* | [Retour à backend-core](../PD-186-epic.md) · [Index User Story](index.md)Objectif¶
Implémenter l'extension PD-17 du journal probatoire PD-37 pour tracer toutes les tentatives d'accès (ALLOW/DENY) avec leur contexte, sans créer de schéma concurrent ni altérer les garanties cryptographiques existantes.
1. Découpage en composants¶
1.1 Composants existants (PD-37) — Non modifiés¶
| Composant | Rôle | Fichier |
|---|---|---|
AuditLog | Entité append-only | entities/audit-log.entity.ts |
AuditSignatureService | Signature HSM | services/audit-signature.service.ts |
AuditLogService | Facade de logging | services/audit-log.service.ts |
JsonCanonicalizeService | RFC 8785 | services/json-canonicalize.service.ts |
1.2 Composants à créer (PD-17)¶
| Composant | Rôle | Fichier |
|---|---|---|
AccessAuditService | Facade spécialisée accès | services/access-audit.service.ts |
AccessResult | Enum ALLOW/DENY | types/access-audit.types.ts |
DenyCode | Taxonomie codes refus | types/access-audit.types.ts |
AccessAuditEntry | Structure PD-17 | types/access-audit.types.ts |
AccessAuditGuard | Guard NestJS | guards/access-audit.guard.ts |
AccessAuditInterceptor | Interceptor NestJS | interceptors/access-audit.interceptor.ts |
1.3 Structure fichiers cible¶
src/modules/audit/
├── services/
│ ├── audit-log.service.ts # Existant (PD-37)
│ ├── audit-signature.service.ts # Existant (PD-37)
│ └── access-audit.service.ts # Nouveau (PD-17)
├── types/
│ ├── audit-action.types.ts # Existant (PD-37)
│ └── access-audit.types.ts # Nouveau (PD-17)
├── guards/
│ └── access-audit.guard.ts # Nouveau (PD-17)
├── interceptors/
│ └── access-audit.interceptor.ts # Nouveau (PD-17)
└── audit.module.ts # Mis à jour
2. Flux techniques¶
2.1 Flux ALLOW¶
[Requête HTTP]
│
▼
[AuthGuard] ─── Authentification ───► OK
│
▼
[AccessAuditGuard] ─── Vérification autorisation ───► ALLOW
│
├──► AccessAuditService.logAllow(context)
│ │
│ ▼
│ AuditLogService.log({
│ actionType: 'access.allow',
│ metadata: { pd: 'PD-17', access_result: 'ALLOW', context: {...} }
│ })
│ │
│ ▼
│ AuditSignatureService.signAuditEntry()
│ │
│ ▼
│ [HSM Signature] ───► [audit_log INSERT]
│
▼
[Controller] ─── Exécution métier
│
▼
[Response]
2.2 Flux DENY¶
[Requête HTTP]
│
▼
[AuthGuard] ─── Authentification ───► OK
│
▼
[AccessAuditGuard] ─── Vérification autorisation ───► DENY
│
├──► AccessAuditService.logDeny(context, denyCode)
│ │
│ ▼
│ AuditLogService.log({
│ actionType: 'access.deny',
│ metadata: { pd: 'PD-17', access_result: 'DENY', deny_code: 'ACL_DENY', context: {...} }
│ })
│ │
│ ▼
│ AuditSignatureService.signAuditEntry()
│ │
│ ▼
│ [HSM Signature] ───► [audit_log INSERT]
│
▼
[ForbiddenException] ───► HTTP 403
2.3 Flux HSM indisponible¶
[AccessAuditService.logAllow/logDeny()]
│
▼
[AuditLogService.log()] ───► [HSM Error]
│
▼
[AuditSignatureDelayedError] ◄─── Queue BullMQ
│
▼
[AccessAuditService] ───► throw ServiceUnavailableException
│
▼
[Guard] ───► DENY access (mode strict)
│
▼
[HTTP 503]
2bis. Diagrammes Mermaid¶
2bis.1 Graphe de dépendances des composants¶
graph TD
subgraph "PD-17 — Nouveaux composants"
AAG[AccessAuditGuard]
AAS[AccessAuditService]
AAI[AccessAuditInterceptor]
AAT[AccessAuditEntry / AccessResult / DenyCode]
end
subgraph "PD-37 — Composants existants (non modifiés)"
ALS[AuditLogService]
ASS[AuditSignatureService]
ALE[AuditLog Entity]
JCS[JsonCanonicalizeService]
end
subgraph "Externes"
HSM[HSM PKCS#11]
DB[(PostgreSQL audit_log)]
BQ[BullMQ DLQ]
end
AAG -->|appelle| AAS
AAI -.->|alternative| AAS
AAS -->|utilise types| AAT
AAS -->|délègue logging| ALS
ALS -->|sérialise| JCS
ALS -->|signe| ASS
ASS -->|signature ECDSA| HSM
ALS -->|INSERT append-only| ALE
ALE -->|persiste| DB
ASS -.->|fallback si HSM down| BQ 2bis.2 Diagramme de séquence — Flux ALLOW¶
sequenceDiagram
participant C as Client HTTP
participant AG as AuthGuard
participant RG as RolesGuard
participant AAG as AccessAuditGuard
participant AAS as AccessAuditService
participant ALS as AuditLogService
participant JCS as JsonCanonicalizeService
participant ASS as AuditSignatureService
participant HSM as HSM PKCS#11
participant DB as PostgreSQL
C->>AG: Requête HTTP
AG->>AG: Vérification authentification
AG->>RG: OK
RG->>RG: Décision autorisation → ALLOW
RG->>AAG: Contexte + décision ALLOW
AAG->>AAS: logAllow(context)
AAS->>AAS: buildPd17Payload('ALLOW', null, context)
AAS->>ALS: log({ actionType: 'access.allow', metadata: Pd17Payload })
ALS->>JCS: canonicalize(entry)
JCS-->>ALS: entry_canonical (RFC 8785)
ALS->>ASS: signAuditEntry(entry_canonical)
ASS->>HSM: ECDSA P-256 sign(SHA3-256(entry_canonical))
HSM-->>ASS: signature
ASS-->>ALS: signedEntry
ALS->>DB: INSERT audit_log (append-only)
DB-->>ALS: OK
ALS-->>AAS: void
AAS-->>AAG: void
AAG->>C: Passe au Controller → Response 200 2bis.3 Diagramme de séquence — Flux DENY¶
sequenceDiagram
participant C as Client HTTP
participant AG as AuthGuard
participant RG as RolesGuard
participant AAG as AccessAuditGuard
participant AAS as AccessAuditService
participant ALS as AuditLogService
participant ASS as AuditSignatureService
participant HSM as HSM PKCS#11
participant DB as PostgreSQL
C->>AG: Requête HTTP
AG->>AG: Vérification authentification
AG->>RG: OK
RG->>RG: Décision autorisation → DENY (ACL_DENY)
RG->>AAG: Contexte + décision DENY
AAG->>AAS: logDeny(context, 'ACL_DENY')
AAS->>AAS: buildPd17Payload('DENY', 'ACL_DENY', context)
AAS->>ALS: log({ actionType: 'access.deny', metadata: Pd17Payload })
ALS->>ASS: signAuditEntry(entry_canonical)
ASS->>HSM: ECDSA P-256 sign
HSM-->>ASS: signature
ALS->>DB: INSERT audit_log (append-only)
DB-->>ALS: OK
ALS-->>AAS: void
AAS-->>AAG: void
AAG->>C: ForbiddenException → HTTP 403 2bis.4 Diagramme de séquence — HSM indisponible (mode strict)¶
sequenceDiagram
participant C as Client HTTP
participant AAG as AccessAuditGuard
participant AAS as AccessAuditService
participant ALS as AuditLogService
participant ASS as AuditSignatureService
participant HSM as HSM PKCS#11
participant BQ as BullMQ DLQ
C->>AAG: Requête (ALLOW ou DENY)
AAG->>AAS: logAllow(context)
AAS->>ALS: log(...)
ALS->>ASS: signAuditEntry(entry_canonical)
ASS->>HSM: ECDSA sign
HSM--xASS: Connection timeout / Error
ASS->>BQ: enqueue(unsignedEntry)
ASS-->>ALS: AuditSignatureDelayedError
ALS-->>AAS: Error propagée
AAS->>AAS: Mode strict → throw ServiceUnavailableException
AAS-->>AAG: ServiceUnavailableException
AAG->>C: HTTP 503 — Accès refusé 3. Mapping invariants → mécanismes¶
| Invariant (Spec §4) | Mécanisme technique | Vérification |
|---|---|---|
| Unicité du journal | AccessAuditService appelle uniquement AuditLogService.log() | Pas de nouvel accès direct à audit_log |
| Primauté PD-37 | Aucune modification de AuditLog entity ni migration | Review code : 0 changement sur PD-37 |
| Encodage unique dans entry_canonical | Données PD-17 dans metadata (sérialisé en entry_canonical) | Pas de nouvelle colonne dans migration |
| Append-only strict | Trigger audit_log_immutable existant (PD-37) | Test UPDATE/DELETE échoue |
| Signature obligatoire | AuditLogService.log() signe via HSM | Aucun bypass possible dans AccessAuditService |
| Horodatage certifié | timestamp géré par PD-37 (new Date() serveur) | Horloge NTP synchronisée |
| Neutralité décisionnelle | AccessAuditService reçoit décision, ne la prend pas | Aucune règle ACL dans le module |
4. Gestion des erreurs¶
4.1 Matrice des erreurs¶
| Erreur | Source | Comportement | Code HTTP |
|---|---|---|---|
| Métadonnée invalide | Validation DTO | Rejet avant signature | 400 |
| HSM indisponible | AuditSignatureService | Accès refusé (mode strict) | 503 |
| Journal indisponible (DB) | AuditLogService | Accès refusé | 503 |
| DLQ pleine | AuditDlqService | Log CRITICAL, accès refusé | 503 |
| Contexte incomplet | AccessAuditService | Rejet avant signature | 400 |
4.2 Mode strict (par défaut)¶
// access-audit.service.ts
async logAllow(context: AccessContext): Promise<void> {
try {
await this.auditLogService.log({
actionType: AuditActionType.ACCESS_ALLOW,
actorId: context.actorId,
entityId: context.entityId,
entityType: context.entityType,
metadata: this.buildPd17Payload('ALLOW', null, context),
ipAddress: context.ipAddress,
userAgent: context.userAgent,
});
} catch (error) {
// Mode strict : accès refusé si log impossible
this.logger.error(`Access audit failed, denying access: ${error.message}`);
throw new ServiceUnavailableException('Audit service unavailable');
}
}
4.3 Codes d'erreur PD-17¶
| Code | Description | Action |
|---|---|---|
PD17_INVALID_CONTEXT | Contexte d'accès incomplet | 400 Bad Request |
PD17_INVALID_DENY_CODE | Code refus hors taxonomie | 400 Bad Request |
PD17_AUDIT_UNAVAILABLE | HSM/DB indisponible | 503 Service Unavailable |
5. Impacts sécurité¶
5.1 Risques identifiés¶
| Risque | Probabilité | Impact | Mitigation |
|---|---|---|---|
| Bypass du guard | Faible | Élevé | Guard appliqué globalement + tests |
| Injection dans metadata | Faible | Moyen | Validation stricte DTO + sanitization |
| Volumétrie DENY | Moyenne | Moyen | Rate limiting + monitoring |
| Exposition IP/UA | Faible | Moyen | RLS PostgreSQL + anonymisation si exposé |
5.2 Mesures de sécurité¶
- Validation stricte : Toutes les entrées PD-17 validées via
class-validator - Sanitization : IP et User-Agent nettoyés avant stockage
- Rate limiting : Limite de DENY par IP/utilisateur (configurable)
- Audit du guard : Le guard lui-même est audité (méta-audit)
- Pas de décision :
AccessAuditServicene prend aucune décision d'autorisation
5.3 Données sensibles¶
| Donnée | Traitement | Justification |
|---|---|---|
actor_id | UUID opaque | Pas de PII direct |
ip_address | Stocké dans metadata signé | Requis pour traçabilité |
user_agent | Stocké dans metadata signé | Contexte technique |
deny_code | Code taxonomie fermée | Pas de message libre |
6. Hypothèses techniques¶
| ID | Hypothèse | Validation |
|---|---|---|
| H-01 | PD-37 est déployé et fonctionnel | Pré-requis vérifié avant démarrage |
| H-02 | AuditLogService.log() est la seule API de logging | Review architecture |
| H-03 | Les guards NestJS s'exécutent avant les contrôleurs | Comportement standard NestJS |
| H-04 | Le moteur d'autorisation est externe (PD existant) | PD-17 reçoit décision, ne décide pas |
| H-05 | metadata est inclus dans entry_canonical | Vérifié dans AuditSignatureService |
| H-06 | La taxonomie DENY est fermée et complète | Définie dans spec §10.1 |
| H-07 | Performance HSM < 20ms par signature | Benchmark PD-37 validé |
7. Points de vigilance¶
7.1 Conformité PD-37¶
- NE PAS modifier
audit-log.entity.ts - NE PAS créer de migration SQL
- NE PAS ajouter de colonne à
audit_log - TOUJOURS passer par
AuditLogService.log()
7.2 Structure metadata PD-17¶
Le payload PD-17 DOIT respecter strictement le schéma §5.0 :
interface Pd17Payload {
pd: 'PD-17';
access_result: 'ALLOW' | 'DENY';
deny_code: DenyCode | null;
context: {
actor_type: 'USER' | 'SERVICE' | 'SYSTEM';
actor_id: string;
entity_type: 'DOCUMENT' | 'ENVELOPE' | 'API';
entity_id: string;
ip_address: string;
user_agent: string;
};
}
7.3 Ordre d'exécution guards¶
1. AuthGuard (authentification)
2. RolesGuard (autorisation) ───► Décision ALLOW/DENY
3. AccessAuditGuard ───► Log de la décision
Le AccessAuditGuard doit s'exécuter après la décision mais avant le contrôleur.
7.4 Volumétrie¶
- Chaque accès (réussi ou refusé) génère une signature HSM
- Estimer 2x le volume actuel de signatures
- Monitorer la latence HSM et la taille du journal
8. Fichiers à créer¶
| Fichier | Description |
|---|---|
src/modules/audit/types/access-audit.types.ts | Types PD-17 |
src/modules/audit/services/access-audit.service.ts | Service facade |
src/modules/audit/services/access-audit.service.spec.ts | Tests unitaires |
src/modules/audit/guards/access-audit.guard.ts | Guard NestJS |
src/modules/audit/guards/access-audit.guard.spec.ts | Tests guard |
src/modules/audit/interceptors/access-audit.interceptor.ts | Interceptor (optionnel) |
9. Fichiers à modifier¶
| Fichier | Modification |
|---|---|
src/modules/audit/types/audit-action.types.ts | Ajout ACCESS_ALLOW, ACCESS_DENY |
src/modules/audit/audit.module.ts | Export nouveaux providers |
10. Ordre d'implémentation¶
- Types PD-17 (
access-audit.types.ts) AccessResultenumDenyCodeenum (taxonomie §10.1)AccessContextinterface-
Pd17Payloadinterface -
Ajout AuditActionType (
audit-action.types.ts) ACCESS_ALLOW = 'access.allow'-
ACCESS_DENY = 'access.deny' -
AccessAuditService (
access-audit.service.ts) logAllow(context)logDeny(context, denyCode)buildPd17Payload()- Validation contexte
-
Mode strict (exception si HSM indisponible)
-
Tests unitaires AccessAuditService
- Mock
AuditLogService - Test payload PD-17 conforme
- Test mode strict
-
Test validation contexte
-
AccessAuditGuard (
access-audit.guard.ts) - Récupération contexte depuis request
- Appel
AccessAuditService -
Gestion erreurs
-
Tests AccessAuditGuard
- Mock service
- Test flux ALLOW
- Test flux DENY
-
Test HSM unavailable
-
Update AuditModule
- Export
AccessAuditService -
Export
AccessAuditGuard -
Tests d'intégration
- Flux complet ALLOW → signature HSM → audit_log
- Flux complet DENY → signature HSM → audit_log
- Vérification payload PD-17 dans entry_canonical
11. Critères de validation¶
- Aucune modification de
audit-log.entity.ts - Aucune migration SQL créée
- Payload PD-17 conforme au schéma §5.0
- ALLOW et DENY génèrent des entrées signées HSM
- Mode strict : accès refusé si HSM indisponible
- Taxonomie DENY complète et validée
- Performance < 25ms par log (incluant signature)
- Tests couvrant tous les cas d'erreur §6
12. Hors périmètre (explicit)¶
- Dashboard ou UI de consultation
- Alerting temps réel
- Analyse des patterns DENY
- Mode dégradé (bypass audit)
- Export des logs PD-17 (utiliser export PD-37)
- Anonymisation automatique IP/UA
13. Dépendances¶
| Dépendance | Version | Usage |
|---|---|---|
| PD-37 | Déployé | Journal probatoire signé |
AuditLogService | PD-37 | API de logging |
class-validator | Existant | Validation DTO |
| NestJS Guards | ^10.x | Interception requêtes |
Annexe A — Taxonomie DENY (spec §10.1)¶
export enum DenyCode {
AUTH_INVALID = 'AUTH_INVALID', // Authentification invalide
AUTH_EXPIRED = 'AUTH_EXPIRED', // Session/token expiré
ACL_DENY = 'ACL_DENY', // ACL refuse l'accès
OWNER_MISMATCH = 'OWNER_MISMATCH', // Propriétaire différent
QUOTA_EXCEEDED = 'QUOTA_EXCEEDED', // Quota dépassé
SYSTEM_LOCKED = 'SYSTEM_LOCKED', // Système verrouillé
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', // Ressource inexistante
PERMISSION_DENIED = 'PERMISSION_DENIED', // Permission refusée
}
Annexe B — Exemple payload entry_canonical¶
{
"actorId": "550e8400-e29b-41d4-a716-446655440000",
"actionType": "access.deny",
"entityId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"entityType": "DOCUMENT",
"metadata": {
"pd": "PD-17",
"access_result": "DENY",
"deny_code": "ACL_DENY",
"context": {
"actor_type": "USER",
"actor_id": "550e8400-e29b-41d4-a716-446655440000",
"entity_type": "DOCUMENT",
"entity_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"ip_address": "192.168.1.100",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
}
},
"timestamp": "2025-12-22T14:30:00.000Z"
}
Ce JSON sera canonicalisé RFC 8785, hashé SHA3-256, et signé ECDSA P-256 par le HSM, exactement comme tout événement PD-37.
14. Prohibitions de sécurité (PD-17 audit)¶
Cette section documente les contre-mesures de sécurité ajoutées suite à l'audit du plan d'implémentation.
14.1 Prohibition dblink (BLOQUANT)¶
Risque identifié: L'extension dblink permettrait au rôle applicatif d'exécuter des requêtes arbitraires via transactions autonomes, contournant la protection append-only.
Contre-mesures:
- Migration
BlockDblinkExtension: supprime dblink si présent, révoque CREATE sur schemas - Fonction
vault_secure.check_no_dblink(): vérification au démarrage applicatif - Service
AuditSecurityMonitorService.verifyNoDblinkExtension(): échec fatal si dblink détecté
Fichiers:
src/database/migrations/1733700300000-BlockDblinkExtension.tssrc/modules/audit/services/audit-security-monitor.service.ts
14.2 Traçage violations via pg_notify (MAJEUR)¶
Risque identifié: Sans transactions autonomes, les violations UPDATE/DELETE ne seraient pas tracées car le rollback annule tout.
Contre-mesures:
- Trigger
audit_log_immutablemodifié : envoiepg_notify('audit_violation', ...)AVANT l'exception pg_notifysurvit au rollback de transaction (canal asynchrone)- Service
AuditViolationListenerService: écoute les notifications et crée des entrées signées HSM
Fichiers:
src/database/migrations/1733700500000-UpdateAuditLogTriggerPgNotify.tssrc/modules/audit/services/audit-violation-listener.service.ts
14.3 Validation acteur multi-couches (MAJEUR)¶
Risque identifié: actor_id validé uniquement par soft checks applicatifs, contournable si service bypassé.
Contre-mesures:
- CHECK constraint
chk_audit_log_actor_id_format: validation format UUID - Trigger
trg_validate_actor: vérifie existence USER dansvault_secure.usersà INSERT - Pas de FK (préserve WORM : suppression utilisateur ne cascade pas)
Fichiers:
src/database/migrations/1733700400000-AddActorValidation.ts
14.4 Protection DDL/TRUNCATE (MINEUR)¶
Risque identifié: Protection append-only ne couvre pas DDL/TRUNCATE par rôles privilégiés.
Contre-mesures:
- Schema
audit_ddlavec tableddl_logpour tracer DDL - Event trigger
audit_ddl_commandssur DDL avec alertepg_notify('security_ddl_alert', ...) - Fonction
vault_secure.check_security_invariants(): vérifie session_replication_role, triggers, etc. - Service
AuditSecurityMonitorService: cron minute pour vérifications périodiques
Fichiers:
src/database/migrations/1733700600000-CreateDdlAuditSchema.tssrc/modules/audit/services/audit-security-monitor.service.ts
14.5 Nouveaux types d'action¶
// src/modules/audit/types/audit-action.types.ts
export enum AuditActionType {
// ... existing types ...
// PD-17: Security monitoring events
AUDIT_VIOLATION_DETECTED = 'audit.violation.detected',
SECURITY_INVARIANT_VIOLATION = 'security.invariant.violation',
DDL_OPERATION_DETECTED = 'security.ddl.detected',
// PD-17: Access logging events
ACCESS_ALLOW = 'access.allow',
ACCESS_DENY = 'access.deny',
}
14.6 Critères de validation sécurité¶
-
SELECT * FROM pg_extension WHERE extname = 'dblink'retourne vide - Application refuse de démarrer si dblink détecté
- Tentative UPDATE/DELETE sur audit_log génère entrée signée HSM (type
audit.violation.detected) -
actor_idinvalide rejette INSERT avec exceptionforeign_key_violation - DDL sur
vault_securegénère alertepg_notify('security_ddl_alert', ...) -
session_replication_role != 'origin'déclenche alerte CRITICAL