Ferme le finding HIGH de la revue Produits (#17) : le PIN d'action sensible
etait verifie sans limitation de tentatives. Conception via panel multi-agents
(3 lentilles + synthese + passe adversariale, holds=true) puis revue de
l'implementation (holds=true).
Dimension du throttle = UTILISATEUR AGISSANT (identite de session, RG-T02), pas
l'email cible (contournable par rotation) ni l'IP (collateral sur poste partage).
Table dediee pin_throttle (entite 22) STRICTEMENT SEPAREE des compteurs de login
(user.failed_login_attempts / login_throttle) : un echec de PIN n'incremente aucun
compteur de connexion (pas d'escalade DoS vers le login).
- db/migrations/0002_pin_throttle.sql : table cle sur actor_user_id (UNIQUE, FK
-> user ON DELETE CASCADE), separee du login. Appliquee a la base dev.
- ThrottlePolicy : dimension 'pin' (bornes propres PIN_THROTTLE_*, 30s..300s, plus
permissives que le login : controle de dissuasion, residuel Faible).
- PinThrottle (nouveau) : isLocked / recordFailure (upsert atomique + backoff, une
transaction, miroir d'AuthService) / reset (UPDATE simple). N'ecrit jamais
user/login_throttle/audit_log.
- PinVerifier::payTimingDecoy : parite de timing du chemin verrouille.
- ProductController update/destroy : gate AVANT verification (leurre + 422
generique, pas de pin.failed sous verrou actif = borne anti-flood de l'audit) ;
recordFailure sur PIN faux ; reset sur succes, cle sur l'acteur de SESSION.
- Docs Merise 21 -> 22 entites : RG-T22 (mlt), entite 22 pin_throttle
(mcd/mld/dictionary), couverture MCT 22/22 (mct).
- .env.example + docker-compose : PIN_THROTTLE_THRESHOLD/BASE/MAX/WINDOW.
- Journal RNCP : docs/journal/2026-06-15--p3-throttle-pin-rg-t22.md.
Tests : 188 verts (525 assertions), PHPStan L6 propre.
Three issues surfaced when running 'make init' on the deployment host
and were fixed in place:
- wakdo_internal network: explicit subnet 192.168.148.0/24 (RFC 1918,
in the free 192.168.144-159 gap). The host's Docker daemon has its
default address pools saturated by other stacks, so auto-allocation
failed. An explicit subnet bypasses the allocator and isolates Wakdo
from neighbour churn.
- wakdo-cron: init: true added so Docker injects tini as PID 1. Without
it, dcron loops on 'setpgid: Operation not permitted' because PID 1
in a container without an init system cannot change process groups
for its children.
- healthz vhost: served as a static file from /usr/local/apache2/htdocs/
instead of a RewriteRule [R=200] that triggered Apache's ErrorDocument
template (and leaked 'internal error' wording into a 200 response).
The file lives outside /var/www/html/ which is bind-mounted at runtime
and would otherwise mask the COPY.
Deliver the full Docker stack for Bloc 5 DevOps (Cr 7.c.3 and 7.c.4):
- docker/apache/ Custom httpd:2.4-alpine with hardened main config,
MPM event tuning and 3 vhosts (healthz, kiosk static,
admin reverse FCGI to wakdo-app:9000). Kiosk vhost
explicitly denies .php to enforce Bloc 1 isolation.
- docker/php-fpm/ Custom php:8.3-fpm-alpine3.20 with pdo_mysql, opcache,
intl, exif, zip and tini for signal handling.
Dynamic pool 3-10 workers listening on TCP 9000.
- docker/cron/ Custom alpine:3.20 with dcron, mariadb-client, gzip.
Nightly mysqldump at 03h00 with 14-day rotation and
512-byte sanity check. Purge and stats jobs templated.
- docker-compose.yml 4 services orchestrated on 2 networks (internal
bridge + external reverse-proxy). 2 named volumes
for DB and uploads, bind-mount for backups.
Traefik labels for 2 routers with HTTPS redirect.
Makefile adds `make backup` (manual dump) and `make backup-ls`.
.gitignore adds /var/ for backup bind-mount path.
docs/journal/2026-04-24--infra-docker.md documents 5 decisions with
alternatives, maps 16 RNCP criteria to artefacts and prepares 6 jury Q&A.
Validated: `docker compose config --quiet` passes. Smoke test deferred
to next session (requires server .env).