Aller au contenu

ADR-0008 — Secret de transit SLY par application et trajectoire mTLS

Date : 2026-06-10 Catégorie : A (convention de sécurité technique, dans le cadre de l'architecture inter-app déjà validée) Décideur : Lead architecte DYNORS Statut : Acceptée — palier 1 implémenté 2026-06-11 Implémentation palier 1 : - core 4f6ef51 (SlyTransitV2 HKDF verification + SlyTransitFilter modes SHARED/DUAL/PER_APP + 14 tests red-team) - dynors-platform eb0caf8 (SlyTransitHkdf + SlyForwardSigningFilter pose les 2 signatures en DUAL) - dynors-internal 93fcf60 (slots Codex TAIL_SLY_TRANSIT_MODE + TAIL_SLY_TRANSIT_KEY_V2) Test pivot du blast radius : SlyTransitV2ContractTest.redTeamCleExfiltreeNePeutPasForgerVersAutreBackend — vérifie qu'une clé k_pai exfiltrée du backend paiement ne permet pas de signer un transit valide vers FISCAL. Objectif §Suivi satisfait. Reste à faire : - Provisionnement Vault des clés V2 par app/cercle (Ansible dynors-storage-pattern). - Bascule progressive SHARED → DUAL → PER_APP app par app, paiement d'abord (sensibilité haute). - Palier 2 mTLS : ADR de suivi dédié à écrire quand la PKI interne est prête.


Contexte

Tout appel inter-app DYNORS transite par SLY, qui signe chaque requête forwardée avec un HMAC-SHA256 (X-Sly-Timestamp + X-Sly-Signature). Chaque backend vérifie cette signature via SlyTransitFilter.

À ce jour, le secret de signature est unique et partagé par tout l'écosystème :

  • côté SLY : dynors.interapp.forward-secret: ${SLY_FORWARD_SECRET} (voir selebeyone/src/main/resources/application.yml, SlyForwardSigningFilter).
  • côté backends : le même SLY_FORWARD_SECRET est injecté dans FISCAL, BOOKS, DYNORS PAIEMENT, JARAAF, et tous les satellites, et vérifié par un SlyTransitFilter identique.

C'est une clé symétrique unique. Conséquence directe sur la surface d'attaque : la compromission d'un seul backend (ou d'un seul secret mal géré dans un seul environnement client) donne la capacité de forger des signatures de transit valides vers n'importe quel autre backend, y compris le hub paiement. Le rayon de souffle d'une fuite est l'écosystème entier.

Pour un hub qui manipule de l'argent (/paiement/**, payouts, settlement), ce niveau de couplage cryptographique est inacceptable. Le document de sécurité DYNORS PAIEMENT évoque déjà un mesh mTLS (Istio/Linkerd) comme cible — cette ADR formalise la trajectoire et un palier intermédiaire actionnable tout de suite.

Décision

Remplacer le secret de transit unique par une stratégie à deux paliers :

Palier 1 (court terme) — secret de transit dérivé par source-app

SLY ne signe plus avec un secret global, mais avec un secret dérivé par application appelante :

k_app = HKDF-SHA256(masterKey = SLY_FORWARD_ROOT, info = "sly-transit:" + sourceApp)
signature = HMAC-SHA256(k_app, timestamp + "." + method + "." + path + "." + bodySha256)
  • La racine SLY_FORWARD_ROOT ne quitte jamais SLY (Vault, lecture SLY uniquement).
  • Chaque backend reçoit uniquement son propre k_app (chemin Vault dynors/{env}/{app}/sly-transit#key), jamais la racine ni les clés des autres apps.
  • SlyTransitFilter vérifie avec sa clé locale ; il ne peut donc valider que les requêtes signées pour lui, et ne détient pas de quoi forger vers un tiers.
  • Le source-app entre dans la dérivation : un backend compromis ne peut pas se faire passer pour une autre app appelante.

Palier 2 (cible) — mTLS de service à service

Sur le mesh (à partir du chemin /paiement/**, puis FISCAL/BOOKS), passer à mTLS : SLY présente un certificat client, chaque backend valide l'identité par le SAN du certificat (spiffe://dynors/{app}). La signature HMAC applicative devient une défense en profondeur, plus l'unique barrière.

L'ordre de migration est dicté par la sensibilité : paiement d'abord, puis FISCAL et BOOKS, puis les satellites.

Conséquences

Bénéfices

  • La compromission d'un backend ne donne plus aucun pouvoir de forge vers les autres : le rayon de souffle est réduit à l'app compromise.
  • L'usurpation de source-app devient cryptographiquement impossible (le source-app est lié à la clé).
  • Rotation ciblée : on peut révoquer/roter la clé d'une seule app sans toucher aux autres (impossible aujourd'hui — toute rotation est globale et bloquante).
  • Compatible avec la stratégie Vault/ESO existante (un chemin de secret par app, principe du moindre privilège déjà en vigueur).

Coûts / contraintes induites

  • SLY doit dériver k_app par appel (coût négligeable, HKDF mis en cache par source-app).
  • Chaque backend doit recevoir un secret dédié au lieu du secret partagé → mise à jour des ExternalSecret/manifestes SIRRAT (un slot Vault par app au lieu d'un slot commun).
  • Migration non instantanée : nécessite une fenêtre de double vérification (voir Plan d'exécution) pour ne pas casser l'inter-app en cours de bascule.
  • SlyTransitFilter (fourni par dynors-commons) doit accepter transitoirement les deux schémas, puis être durci.

Composants impactés

  • dynors-platform/selebeyoneSlyForwardSigningFilter, SlyProperties (racine + dérivation).
  • dynors-commonsSlyTransitFilter (vérification par clé locale, support double-schéma transitoire).
  • Manifestes SIRRAT / ExternalSecret de chaque app — un slot sly-transit par app.
  • Codex SIRRAT — TAIL_SECRET_* : référence du secret de transit par app.
  • DYNORS PAIEMENT en premier consommateur de la cible mTLS.

Alternatives considérées

Alternative A — Statu quo (secret unique partagé). Écartée : rayon de souffle = écosystème entier, rotation globale bloquante, usurpation source-app triviale en cas de fuite. Incompatible avec un hub financier.

Alternative B — Secrets indépendants par app, gérés à la main (pas de dérivation). Écartée : N secrets à générer/distribuer/roter manuellement, surface d'erreur opérationnelle élevée. La dérivation HKDF depuis une racine donne le même isolement avec une seule racine à protéger.

Alternative C — Aller directement au mTLS sans palier 1. Écartée comme étape unique : le mTLS demande une maturité mesh (PKI interne, rotation de certs, sidecars) qui n'est pas en place sur tous les environnements (notamment on-premise client / docker-compose). Le palier 1 réduit le risque immédiatement sans dépendre de cette maturité ; le mTLS reste la cible.

Plan d'exécution

  1. HKDF dans SLY : implémenter la dérivation k_app + cache, derrière un flag dynors.interapp.transit-mode = SHARED | PER_APP. Défaut SHARED au déploiement.
  2. Double vérification backends : SlyTransitFilter accepte signature SHARED ou PER_APP pendant la fenêtre de migration.
  3. Provisionnement Vault : créer dynors/{env}/{app}/sly-transit#key pour chaque app, dérivé hors-ligne depuis la racine. Mettre à jour les manifestes SIRRAT.
  4. Bascule par app : passer transit-mode = PER_APP app par app, en commençant par DYNORS PAIEMENT. Vérifier les métriques d'erreur de signature à chaque étape.
  5. Durcissement : une fois toutes les apps en PER_APP, retirer le support SHARED de SlyTransitFilter et supprimer SLY_FORWARD_SECRET global.
  6. mTLS (cible) : ADR de suivi dédié quand la PKI interne est prête, en commençant par le chemin paiement.

Suivi

Critère de succès palier 1 : après bascule, aucun backend ne détient un secret permettant de signer un transit vers une autre app ; la rotation de la clé d'une app n'impacte aucune autre.

Critère de mesure : un test de sécurité (red-team interne) prouve qu'un secret de transit exfiltré depuis l'app A ne permet pas d'appeler /paiement/** au nom de l'app B.

Signal de régression : toute nouvelle app qui réutilise le secret partagé après la fin de migration est un défaut de conformité à refuser en revue.

Références

  • Règle inter-app : .cursor/rules/security-interapp-tenants.mdc §3, §5
  • Stratégie secrets : .cursor/rules/vault-secrets-dynors.mdc
  • Sécurité paiement (cible mTLS) : docs/projet-nouveau/DynorsPaiement/DYNORS_PAIEMENT_SYNTHESE_PDF_v1_0.md §6
  • Gouvernance URLs / SLY : claude-handoff-dynors/DYNORS_SLY_URL_GOVERNANCE_PORTAL.md