ADR-012 — Rôles PostgreSQL séparés, sessions révocables, rotation de clé (M7, lot 2)¶
- Statut : proposé
- Date : 2026-10-04
- Décideurs : à valider par le porteur du projet
- Concerne :
illwatch/app/db_roles.py,illwatch/modules/foundation/sessions.py,illwatch/app/security.py,illwatch/app/api/v1/auth.py, migrationf41c7a9d2e86,docker-compose.yml
Contexte¶
ADR-011 a rendu la chronologie et l'audit immuables pour l'application, avec une limite explicite : le compte de connexion de l'application possédait les tables. Il pouvait donc supprimer un déclencheur, modifier la structure, et l'immuabilité reposait sur la bonne conduite du code. Trois autres faiblesses restaient :
- un jeton d'accès JWT de 60 min n'était pas révocable : un jeton volé restait valable une heure, et aucun moyen ne permettait de couper une session ;
- changer
SECRET_KEYdéconnectait tout le monde d'un coup, ce qui décourage la rotation ; - Redis tournait sans mot de passe.
Décision¶
- Deux rôles PostgreSQL. Le propriétaire (
MIGRATION_DATABASE_URL) exécute les migrations ; l'application se connecte avec un rôle (DATABASE_URL,DATABASE_APP_ROLE) qui n'a queSELECT, INSERT, UPDATE, DELETEsur les données,SELECT, INSERTseulement surincident_eventsetaudit_events,SELECTsuralembic_version, aucun droit de structure.illwatch db upgraderéapplique ces droits après chaque migration ;illwatch db app-role ROLE [--create]les pose à la demande. Le rôle est refusé s'il est superutilisateur ou propriétaire d'une table. - Sessions. Jeton d'accès de 15 min ; jeton de rafraîchissement opaque (256 bits, empreinte
SHA-256 seule en base), 7 jours, rotatif : chaque usage le remplace. Un jeton déjà
remplacé qui revient révoque toute la lignée (
reuse_detected).POST /auth/refresh,POST /auth/logout,illwatch users disable|enable|revoke-sessions. - Rotation de clé. Chaque JWT porte
kid(empreinte SHA-256 tronquée de sa clé).SECRET_KEYsigne ;SECRET_KEY_PREVIOUSest acceptée en vérification seulement. Une valeur vide vaut « non définie ». - Redis protégé par mot de passe dans Compose ; en production, ILLWATCH refuse de démarrer sans mot de passe ou avec la valeur de développement.
Justification¶
- Révocation des JWT par liste noire (
jti) : écartée. Elle impose une lecture Redis ou SQL à chaque requête pour un gain marginal face à un accès de 15 min ; la désactivation du compte, relue en base à chaque requête, coupe déjà l'accès immédiatement. - Jeton de rafraîchissement JWT : écarté au profit d'un jeton opaque, révocable par nature et sans valeur si la base fuit.
- Rejeu toléré quelques secondes (requêtes parallèles d'un même client) : écarté. Un client qui rafraîchit deux fois en parallèle est déconnecté ; c'est le comportement strict recommandé par l'OAuth 2.0 Security BCP, et ILLWATCH n'a pas encore de client web.
- Rôles créés par une migration : écarté. Un rôle est un objet du cluster, pas de la base, et son mot de passe ne doit pas figurer dans l'historique Git.
Conséquences¶
- Positives : même un code applicatif compromis ne peut plus altérer la chronologie, l'audit ni la structure ; un vol de jeton de rafraîchissement est détecté au premier rejeu ; la clé de signature peut changer sans déconnexion générale ; plus aucun service sans mot de passe.
- Négatives :
- deux URL de base à configurer, et un rôle à créer une fois sur les volumes existants ;
- un mot de passe contenant
:,@ou%doit être encodé dans l'URL (%3A,%40,%25) ; - la table
refresh_tokenscroît : purge des jetons expirés à prévoir (M11) ; - les clients doivent gérer le rafraîchissement (le scénario et la CLI le font ; une future interface web aussi).
Suivi¶
- Lot 3 : reverse proxy TLS, scan d'image (Trivy), conteneur de migration distinct.
- M11 : purge planifiée de
refresh_tokens, alerte surauth.refreshDENIED.