# AUDIT — Migration Token Global → Token Par-Poste ## rpa_vision_v3 — POC Clinique Wallerstein **Status** : DRAFT lecture seule — audit factuel — pas encore appliqué **Date** : 2026-06-01 **Scope** : Identification des modifications pour passer d'un token API global (partagé entre tous les postes) à un token unique par-poste permettant la révocation chirurgicale indépendante **Contexte** : POC sur ≤5 TIM pendant plusieurs mois ; passage de 3 mois (doc initiale) à durée longue → risque amplifié de compromission d'un poste --- ## 1. Source du Token Actuel — Facteurs Identifiés ### 1.1 Génération et Stockage Côté Serveur **Fichier** : `agent_v0/server_v1/api_stream.py` (lignes 285-363) | Point | Facteur | |-------|---------| | **Variable env** | `RPA_API_TOKEN` (obligatoire en production) | | **Mode génération** | Lecture depuis l'env OBLIGATOIRE (`os.getenv("RPA_API_TOKEN", "").strip()`) | | **Tolérance dev** | Génération aléatoire via `secrets.token_hex(32)` si `RPA_AUTH_DISABLED=true` | | **Fail-closed** | Production : token manquant → `sys.exit(1)` immédiat, pas de génération silencieuse | | **Scope du token** | **GLOBAL** : un seul token pour tous les postes clients (tous les TIM partagent la même valeur) | | **Type de valeur** | Hex string de 32 caractères (e.g. `a1b2c3d4e5f6...`) | | **Lifecycle** | Aucune rotation built-in ; révocation → rotation manuelle de la var env + redémarrage du serveur | ### 1.2 Distinction Admin/ReadOnly **Constat** : **N'existe pas actuellement.** - Un seul niveau de droit : droits R/W complets - Tous les postes ont capacité à : capturer screenshot, invoquer VLM, lire/exécuter workflows - Aucune notion de scope ou permission granulaire par token ### 1.3 Lecture au Démarrage du Serveur **Code** (api_stream.py:301-328) : ```python _API_TOKEN_ENV = os.environ.get("RPA_API_TOKEN", "").strip() if _AUTH_DISABLED: API_TOKEN = _API_TOKEN_ENV or secrets.token_hex(32) elif not _API_TOKEN_ENV: logger.critical("[SÉCURITÉ] FATAL — RPA_API_TOKEN est absent ou vide. ...") sys.exit(1) else: API_TOKEN = _API_TOKEN_ENV ``` **Impact** : Token chargé une seule fois au boot, inchangé pendant toute la session du serveur. --- ## 2. Fabrication du ZIP Installeur Léa — Flux Token ### 2.1 Pipeline de Téléchargement du ZIP **Endpoint** : `web_dashboard/app.py` → `POST /api/fleet/download/` (ligne 2156) **Flux** : ``` 1. Dashboard Frontend (ui) demande au Dashboard Backend (5001) ↓ 2. Backend lit la liste des agents enregistrés (AgentRegistry SQLite) ↓ 3. Requête POST vers serveur streaming (5005) : /api/v1/agents/fleet ↓ 4. Streaming renvoie liste agents (enrolled_agents table) ↓ 5. Backend construit config.txt avec : - RPA_SERVER_URL (toujours conforme, finit par /api/v1) - RPA_API_TOKEN = valeur globale unique (tous les postes) - RPA_MACHINE_ID = fourni par l'UI - RPA_USER_LABEL = nom du collaborateur ``` ### 2.2 Source du Token Injecté dans le ZIP **Fichier** : `web_dashboard/app.py` (ligne 2183) ```python api_token = os.environ.get('RPA_API_TOKEN', '') # <- MÊME VALEUR pour tous les postes custom_config = _build_custom_config(machine_id, user_name, api_token) ``` **Constat** : - Le token global de l'environnement du Dashboard (5001) est utilisé - **Tous les ZIPs téléchargés contiennent le même token** (pas de diversification par poste) - Aucune génération de token unique lors du POST /download ### 2.3 Machine ID — Source et Validation **Provenance du machine_id** : - Côté **client Windows** (Léa.bat / install.bat) : Inno Setup génère un UUID via la page custom d'enrollment - Côté **serveur** : Enrôlement possible sans vérification du machine_id source (endpoint `/api/v1/agents/enroll` accepte n'importe quel machine_id du client) **Validation** : - Pas de vérification cryptographique du machine_id (e.g. pas de signature pour prouver l'authenticité) - Le machine_id est traité comme **identifiant de confiance** après enrôlement (logging, last_seen_at) ### 2.4 Endpoint d'Enrôlement — État Actuel **Endpoint** : `agent_v0/server_v1/api_stream.py` → `POST /api/v1/agents/enroll` (ligne 6638) | Aspect | État Actuel | |--------|-------------| | **Qui génère le token ?** | Dashboard Backend (lit env `RPA_API_TOKEN`) | | **Le client enrôlé reçoit quoi ?** | Le token global dans la réponse `/api/v1/agents/enroll` (ligne 6696) | | **Existe-t-il un endpoint qui génère token unique ?** | Non. Phase 2 signalée dans le commentaire (ligne 6647) | | **Stockage des tokens par-poste ?** | Non. Table `enrolled_agents` enregistre le poste mais pas le token | **Code** (api_stream.py:6688-6698) : ```python return { "status": "enrolled", "created": result["created"], "reactivated": result["reactivated"], "machine_id": machine_id, # Phase 1 : on renvoie le token global pour que le client puisse # verifier qu'il est bien aligne avec le serveur. Phase 2 pourra # emettre un token par poste (issued_token != API_TOKEN global). "api_token": API_TOKEN, # <- GLOBAL "agent": agent, } ``` --- ## 3. Validation du Token Côté Serveur ### 3.1 Middleware de Vérification **Fichier** : `agent_v0/server_v1/api_stream.py` (ligne 351) ```python async def _verify_token(request: Request): """Middleware de vérification du token API Bearer.""" if _AUTH_DISABLED: return if request.url.path in _PUBLIC_PATHS: return auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer ") or auth[7:] != API_TOKEN: raise HTTPException(status_code=401, detail="Token API invalide") ``` **Constat** : - **Comparaison à une constante** unique (pas de lookup table) - Tous les endpoints protégés (sauf 5 publics) nécessitent ce token exact - Le token est envoyé dans chaque requête HTTP : `Authorization: Bearer ` ### 3.2 Granularité des Droits **Constat** : **Un seul niveau : R/W complet** - Pas de distinction admin/user/readonly - Pas de scope granulaire (e.g. "capture seulement" vs "exécution workflow") - Tous les agents ayant le bon token peuvent accéder à toutes les fonctions ### 3.3 Machine ID et Audit **Machine_id dans les requêtes** : Le client (Léa) envoie le machine_id dans les headers ou dans le body de certaines requêtes. Côté serveur : - `audit_trail.record(AuditEntry(..., machine_id=client_provided_machine_id, ...))` (enregistré, pas vérifié) - `agent_registry.touch_last_seen(machine_id)` (mise à jour timestamp) **Risque** : Aucune vérification que le machine_id envoyé par le client correspond au token utilisé. Un client pourrait usurper l'identité d'une autre machine en envoyant un autre machine_id + le token global. --- ## 4. Points d'Appel Côté Agent Windows ### 4.1 Lecture de config.txt au Démarrage **Fichier** : `agent_v0/agent_v1/config.py` (ligne 57) ```python API_TOKEN = os.environ.get("RPA_API_TOKEN", "") ``` Le token vient de trois sources (ordre de priorité) : 1. Variable env `RPA_API_TOKEN` (posée par Lea.bat après parsing de config.txt) 2. Fichier `config.txt` dans le ZIP (contient le token global) 3. Défaut : chaîne vide (agent lancé sans token) **Fichier** : `deploy/lea_package/Lea.bat` (ligne 40-43) ```batch if exist "config.txt" ( for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do ( if not "%%a"=="" if not "%%b"=="" set "%%a=%%b" ) ) ``` Lea.bat parse config.txt ligne par ligne et pose les variables d'environnement (y compris `RPA_API_TOKEN`). ### 4.2 Envoi du Token dans les Requêtes **Streaming WebSocket** : `agent_v0/agent_v1/network/streamer.py` (ligne ~80-90) ```python from ..config import API_TOKEN, STREAMING_ENDPOINT # Dans la fonction d'auth : if API_TOKEN: return {"Authorization": f"Bearer {API_TOKEN}"} ``` **Requêtes REST (executor)** : `agent_v0/agent_v1/core/executor.py` ```python if API_TOKEN: headers["Authorization"] = f"Bearer {API_TOKEN}" ``` **Constat** : - Token présent dans **chaque** requête (REST + WebSocket streaming) - Format standard Bearer : `Authorization: Bearer ` - Aucune logique de refresh ou renouvellement ### 4.3 Renouvellement / Refresh du Token **Constat** : **N'existe pas.** - Token lu une seule fois au démarrage de l'agent - Aucun mécanisme pour redemander un token au serveur sans redémarrer l'agent - Révocation d'un token global = tous les agents existants deviennent invalides immédiatement (pas de transition gracieuse) --- ## 5. Architecture Cible : Token Par-Poste ### 5.1 Schéma de Stockage Côté Serveur **Nouvelle table SQLite** dans `data/databases/rpa_data.db` : ```sql CREATE TABLE IF NOT EXISTS postes_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, machine_id TEXT NOT NULL UNIQUE, token_hash TEXT NOT NULL, -- SHA256(token) token_readable TEXT NOT NULL, -- token lisible, jamais loggé status TEXT DEFAULT 'active', -- 'active', 'revoked' created_at TEXT NOT NULL, -- ISO 8601 UTC revoked_at TEXT, -- ISO 8601 UTC si révoqué rotated_at TEXT, -- ISO 8601 UTC si régénéré last_seen_at TEXT, -- Dernière utilisation rotation_reason TEXT -- 'manual_revoke', 'rotation_request', etc. ); CREATE INDEX idx_postes_tokens_machine_id ON postes_tokens(machine_id); CREATE INDEX idx_postes_tokens_status ON postes_tokens(status); ``` **Co-habitation** : Table dans la même DB que `enrolled_agents` (même `data/databases/rpa_data.db`). ### 5.2 Endpoint d'Enrôlement Amélioré **Route modifiée** : `POST /api/v1/agents/register` (nouvelle, plutôt que d'écraser `/agents/enroll`) ```python @app.post("/api/v1/agents/register", status_code=201) async def agents_register(request: AgentRegisterRequest): """ Enrôlement du poste + génération du token unique. Comportement : - Enregistre le poste (ou le réactive) dans enrolled_agents - Génère un token unique via secrets.token_urlsafe(32) - Stocke le hash SHA256 du token dans postes_tokens - Retourne le token UNE SEULE FOIS dans la réponse Client doit sauvegarder ce token dans config.txt immédiatement. Aucune autre route ne renvoie le token en clair. """ machine_id = (request.machine_id or "").strip() if not machine_id: raise HTTPException(status_code=400, detail="machine_id obligatoire") # 1. Enregistrer le poste (ou le réactiver) try: result = agent_registry.enroll(machine_id=machine_id, ...) except AgentAlreadyEnrolledError: # Poste déjà actif : générer un nouveau token pour rotation pass # 2. Générer token unique token_value = secrets.token_urlsafe(32) # e.g. "aB1_cD2-eF3...XyZ" token_hash = hashlib.sha256(token_value.encode()).hexdigest() # 3. Insérer ou mettre à jour dans postes_tokens postes_registry.create_or_rotate( machine_id=machine_id, token_hash=token_hash, token_readable=token_value, # Stocké temporairement rotation_reason="enrollment" ) # 4. Retourner le token UNE SEULE FOIS return { "status": "registered", "machine_id": machine_id, "token": token_value, # <- À SAUVEGARDER IMMÉDIATEMENT "expires_in": "never", # Sans expiration, mais révocable à tout moment "agent": {...} } ``` ### 5.3 Modification du Middleware d'Auth **Fichier** : `agent_v0/server_v1/api_stream.py` (remplacer `_verify_token`) ```python async def _verify_token(request: Request): """Middleware de vérification du token par-poste. Lookup du token dans postes_tokens au lieu de comparaison constante. Cache mémoire pour éviter hammer SQLite à chaque requête. """ if _AUTH_DISABLED: return if request.url.path in _PUBLIC_PATHS: return auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): raise HTTPException(status_code=401, detail="Token invalide") token_value = auth[7:] token_hash = hashlib.sha256(token_value.encode()).hexdigest() # 1. Chercher dans le cache (dict en mémoire) if token_hash in _token_cache: cached = _token_cache[token_hash] if cached["status"] != "active": raise HTTPException(status_code=401, detail="Token révoqué") cached["last_used"] = time.time() request.scope["machine_id"] = cached["machine_id"] # Injecter pour l'audit return # 2. Chercher en DB row = postes_registry.get_by_hash(token_hash) if row is None or row["status"] != "active": raise HTTPException(status_code=401, detail="Token invalide") # 3. Mettre en cache _token_cache[token_hash] = { "machine_id": row["machine_id"], "status": row["status"], "last_used": time.time(), } # Limiter la taille du cache (LRU, max 1000 tokens) if len(_token_cache) > 1000: oldest = min(_token_cache.items(), key=lambda x: x[1]["last_used"]) del _token_cache[oldest[0]] # Injecter machine_id pour l'audit request.scope["machine_id"] = row["machine_id"] return ``` ### 5.4 Endpoint de Révocation **Route** : `POST /api/v1/postes//revoke` (sécurisé par auth Bearer) ```python @app.post("/api/v1/postes/{machine_id}/revoke") async def revoke_poste(machine_id: str): """ Révoque le token du poste (soft delete, audit conservé). Dashboard ou administrateur : révoque un poste compromis. """ # Vérifier que le token Bearer utilisé a le droit de révoquer # (optionnel : admin-only, ou même machine_id peut révoquer son propre token) row = postes_registry.revoke(machine_id=machine_id, reason="admin_revoke") if row is None: raise HTTPException(status_code=404, detail="Machine not found") # Invalider le cache if row["token_hash"] in _token_cache: del _token_cache[row["token_hash"]] logger.info(f"[SECURITY] Poste révoqué : {machine_id}") return {"status": "revoked", "machine_id": machine_id} ``` ### 5.5 Endpoint de Régénération **Route** : `POST /api/v1/postes//rotate` (pour l'utilisateur du poste) ```python @app.post("/api/v1/postes/{machine_id}/rotate") async def rotate_poste_token(machine_id: str): """ L'utilisateur du poste demande un nouveau token (le vieil est invalidé). Retourne le nouveau token (à injecter dans config.txt). """ # Vérifier que le token actuel appartient à ce machine_id # (via injection dans request.scope par le middleware) new_token = secrets.token_urlsafe(32) new_hash = hashlib.sha256(new_token.encode()).hexdigest() postes_registry.rotate( machine_id=machine_id, new_token_hash=new_hash, rotation_reason="user_request" ) # Invalider l'ancien token du cache old_row = postes_registry.get_by_machine_id(machine_id) if old_row and old_row["token_hash"] in _token_cache: del _token_cache[old_row["token_hash"]] return { "status": "rotated", "machine_id": machine_id, "new_token": new_token, "hint": "Mettez à jour config.txt avec ce nouveau token" } ``` ### 5.6 UI Dashboard — Onglet Postes **Nouvelle page** : `/templates/fleet_management.html` Fonctionnalités : - Liste des postes actifs (machine_id, user_name, last_seen_at, enrolled_at) - Liste des postes révoqués (avec date révocation + raison) - Boutons par poste : - **Révoquer** : POST `/api/v1/postes/{machine_id}/revoke` → impossibilité pour le poste de se connecter - **Régénérer token** : POST `/api/v1/postes/{machine_id}/rotate` → nouveau token à faire entrer sur le poste - **Afficher détails** : historique de révocation/rotation - Filtres : statut (actif/révoqué), plage de dates, collaborateur --- ## 6. Chiffrage Qualitatif de la Migration ### 6.1 Fichiers à Modifier | Fichier | Modification | Effort | Risque | |---------|--------------|--------|--------| | `agent_v0/server_v1/api_stream.py` | Remplacer `_verify_token`, ajouter `_token_cache`, endpoints `/api/v1/postes/*/revoke` et `/rotate` | **Moyen** (~200-250 lignes) | Moyen : tous les endpoints dépendent du middleware | | `agent_v0/server_v1/agent_registry.py` | Ajouter classe `PostesTokenRegistry` (CRUD postes_tokens table) | **Moyen** (~150-200 lignes) | Faible : isolé, pas d'impact existant | | `agent_v0/server_v1/` (nouveau) | `postes_registry.py` (classe PostesTokenRegistry) | **Moyen** (~150-200 lignes) | Faible : nouveau module | | `web_dashboard/app.py` | Remplacer `_build_custom_config` + endpoint `/api/fleet/download` pour générer token unique | **Moyen** (~100-150 lignes) | Moyen : impact sur téléchargement ZIP | | `web_dashboard/templates/` | Ajouter `fleet_management.html` (onglet postes) | **Moyen** (~300-400 lignes HTML/JS) | Faible : interface nouvelle, pas de régression | | `agent_v0/agent_v1/config.py` | Pas de modification (lecture env inchangée) | **Aucun** | Aucun | | `deploy/installer/Lea.iss` | Pas de modification (Inno Setup pose la var env) | **Aucun** | Aucun | **Total effort** : ~1000-1300 lignes de code nouveau / modifié ### 6.2 Migration Progressive en 3 Étapes #### Étape A : Fondations (1-2 jours) **Objectif** : Ajouter la table + le registre, mais le serveur accepte les 2 modes. 1. Créer table `postes_tokens` + index dans `data/databases/rpa_data.db` 2. Implémenter classe `PostesTokenRegistry` (CRUD simple) 3. Modifier `_verify_token` pour : - D'abord chercher le token en par-poste (lookup table) - Si non trouvé, faire un fallback sur le mode global (API_TOKEN) 4. Logger chaque validation en clair : `[AUTH] Token trouvé : par-poste | global` **Pre-requis** : Aucun **Durée estimée** : 1-2 jours **Point de non-retour** : Non (le fallback permet de revenir en arrière) **Impact utilisateurs** : Nul (transparent, les agents existants continuent avec le global) **Code exemple (middleware)**: ```python async def _verify_token(request: Request): auth = request.headers.get("Authorization", "") if not auth.startswith("Bearer "): raise HTTPException(status_code=401) token_value = auth[7:] token_hash = hashlib.sha256(token_value.encode()).hexdigest() # Mode 1 : Par-poste row = postes_registry.get_by_hash(token_hash) if row and row["status"] == "active": logger.info(f"[AUTH] Token par-poste validé : {row['machine_id']}") request.scope["machine_id"] = row["machine_id"] return # Mode 2 : Global (fallback, phase 1) if token_value == API_TOKEN: logger.info(f"[AUTH] Token global validé (fallback phase 1)") return raise HTTPException(status_code=401, detail="Token invalide") ``` #### Étape B : Redéploiement sur Postes Wallerstein (3-5 jours) **Objectif** : Migrer les 5 postes du POC en par-poste (ZIPs avec tokens uniques). 1. Nouvelle version du Dashboard : endpoint `/api/v1/agents/register` qui génère tokens uniques 2. Modifier `/api/fleet/download/` pour injecter le token unique (pas le global) 3. Sur chaque poste Wallerstein : - Télécharger le nouveau ZIP via le Dashboard - Extraire et remplacer config.txt - Redémarrer Léa **Pre-requis** : Étape A complète et testée **Durée estimée** : 3-5 jours (déploiement sur le terrain + tests) **Point de non-retour** : OUI — une fois que tous les postes ont le par-poste, l'admin peut supprimer API_TOKEN global **Impact utilisateurs** : Très faible (ZIPs pré-générés, transparents pour les utilisateurs) **Checklist de déploiement** : - [ ] Tester endpoint `/api/v1/agents/register` en lab - [ ] Générer 5 ZIPs avec tokens uniques (une clé par poste) - [ ] Vérifier que config.txt contient le bon token pour chaque machine_id - [ ] Déployer sur poste 1, vérifier dernière utilisation dans Dashboard - [ ] Idem postes 2-5 - [ ] Vérifier historique audit : chaque poste tracé séparément #### Étape C : Durcissement — Suppression du Global (1 jour) **Objectif** : Désactiver le mode global, garantir que seule l'authentification par-poste fonctionne. 1. Retirer le fallback global de `_verify_token` 2. Supprimer variable env `RPA_API_TOKEN` du bootstrap 3. Logger un FATAL si la variable est trouvée (debug) 4. Mettre à jour la doc : "Mode global dépréciéé depuis 2026-06-XX" **Pre-requis** : Tous les postes en par-poste (Étape B complète) **Durée estimée** : 1 jour **Point de non-retour** : OUI — après ce point, les anciens agents (mode global) ne peuvent plus se connecter **Impact utilisateurs** : ÉLEVÉ si Étape B incomplète (agents bloqués) --- ## 7. Risques Non Couverts Par la Migration ### 7.1 Vol Physique du PC TIM avec config.txt en Clair **Risque** : Token stocké en clair dans le fichier `config.txt` (Windows, pas chiffré par défaut) **Mitigations possibles** : - DPAPI Windows : Chiffrer config.txt via `CryptProtectData` (nécessite clé utilisateur Windows) - Stockage sécurisé : Léa peut demander token à la première utilisation (sans le persister) - JWT court + refresh : Token courte durée de vie (~30 min) + refresh token distinct (mais complexe) - Jeton hardware : Clé USB ou smartcard (hors scope POC) **Recommandation** : Pour Wallerstein, utiliser DPAPI Windows (effort faible, sécurité augmentée). ### 7.2 Replay Attacks sur le Streaming WebSocket **Risque** : Sans TLS strict (HTTPS/WSS), un attaquant en MITM peut rejouer les requêtes vidées du token Bearer. **Mitigations actuelles** : - Reverse proxy NPM (déjà en place) expose lea.labs.laurinebazin.design en HTTPS - WSS (WebSocket Secure) utilisé en production (non en lab) **Recommandation** : Vérifier que tous les endpoints streaming utilisent WSS en production (config NPM). ### 7.3 Token Enregistré dans les Logs **Risque** : Si Flask ou Uvicorn log les en-têtes HTTP en DEBUG, le token Bearer s'affiche en clair. **Prévention** : - Mode production (défaut) : `docs_url=None, redoc_url=None` (api_stream.py:469-471) - Vérifier que les logs ne contiennent jamais `Authorization: Bearer ...` **Commande de vérification** : ```bash grep -r "Authorization.*Bearer" /path/to/logs/ # Ne doit retourner rien grep -r "RPA_API_TOKEN" /path/to/logs/ # Ne doit retourner rien ``` ### 7.4 Usurpation d'Identité : Machine_ID **Risque** : Actuellement, aucune vérification que le machine_id envoyé par le client correspond au token utilisé. **Scénario** : Client A possède token de poste A. Poste A envoie requête avec `machine_id=poste_B` → audit imputé à poste B. **Mitigation dans l'architecture cible** : - Dès que le token est validé, extraire automatiquement le machine_id depuis la table postes_tokens - Rejecter toute tentative du client d'envoyer un machine_id différent ```python async def _verify_token(request: Request): # ... validation token ... row = postes_registry.get_by_hash(token_hash) # Injecter l'identité vraie dans la requête request.scope["machine_id"] = row["machine_id"] # <- Vrai machine_id request.scope["user_id"] = row["user_id"] # <- Vrai user_id # Ne pas faire confiance au client pour l'identité ``` ### 7.5 Absence de Rotation Forcée **Risque** : Aucune rotation obligatoire (ex. tous les 90 jours). Un token compromis et non détecté reste valide indéfiniment. **Mitigation pour Wallerstein** : - Ajouter `last_rotation_at` et `rotation_period_days` à la table postes_tokens - Endpoint `/api/v1/postes/{machine_id}/status` retourne un warning si rotation > 90 jours - Dashboard affiche visuel "Token à renouveler" (jaune : 60j, rouge : >90j) **Effort** : Moyen (~50-100 lignes) --- ## 8. Verdict et Recommandations ### Effort Global **Catégorie** : **MOYEN** (4-8 jours-homme, 1000-1300 lignes, pas d'architecte externe requis) - Fondations (Étape A) : 1-2 jours - Déploiement terrain (Étape B) : 3-5 jours (nécessite accès aux postes Wallerstein) - Durcissement (Étape C) : 1 jour ### Durée Estimée | Phase | Durée | Bloqué Par | |-------|-------|-----------| | A (Fondations) | 1-2 j | Aucun | | B (Déploiement) | 3-5 j | A complète | | C (Durcissement) | 1 j | B complète | | **TOTAL** | **5-8 j** | — | ### Dépendances Connues **Aucune dépendance blocage avec autres chantiers actuels** : - Worktree guard Qwen : Isolé, pas d'impact (module core/) - Gap propagation verdict : Isolé (migration replay) - Mismatch captures persistées : Isolé (API /traces/upload) **Dépendance suggérée** : Implémenter mitigations § 7.1-7.5 après la migration (durcissement sécurité, effort marginal). ### Recommandation **GO pour le POC Wallerstein** (approche par étapes) : 1. **Étape A maintenant** : Ajouter table + registre + middleware 2 modes (1-2 jours) 2. **Étape B début Wallerstein** : Redéployer les 5 postes avec tokens uniques (3-5 jours, pendant POC) 3. **Étape C post-POC** : Durcir si compromise d'un poste détectée, ou en fin de POC Cette approche offre la **révocation chirurgicale** dès l'Étape B (capacité à révoquer un poste sans affecter les autres), tout en gardant une marche arrière jusqu'à la fin de l'Étape B. --- ## Annexe A : Schéma de Synthèse ``` ┌─────────────────────────────────────────────────────────────┐ │ Système Actuel (Phase 1 — Token Global) │ ├─────────────────────────────────────────────────────────────┤ │ • RPA_API_TOKEN = "a1b2c3d4...xyz" (1 seul, dans .env) │ │ • Tous les postes envoient le MÊME token │ │ • Aucune révocation fine (révoque tout ou rien) │ │ • Audit : machine_id non vérifiable (peut être usurpé) │ └─────────────────────────────────────────────────────────────┘ ↓ (Étape A : 1-2j) ↓ ┌─────────────────────────────────────────────────────────────┐ │ État Intermédiaire (Phase 1.5 — Dual Mode) │ ├─────────────────────────────────────────────────────────────┤ │ • Table postes_tokens ajoutée │ │ • Middleware accepte tokens par-poste OU global │ │ • Anciens agents (global) : continuent à fonctionner │ │ • Nouveaux agents (par-poste) : peuvent être enrôlés │ └─────────────────────────────────────────────────────────────┘ ↓ (Étape B : 3-5j, terrain) ↓ ┌─────────────────────────────────────────────────────────────┐ │ Cible (Phase 2 — Token Par-Poste) │ ├─────────────────────────────────────────────────────────────┤ │ • Poste A : token_A (unique) → machine_id_A │ │ • Poste B : token_B (unique) → machine_id_B │ │ • Poste C : token_C (unique) → machine_id_C │ │ • ... │ │ • Révocation poste A : ne touche pas B, C, D, E │ │ • Audit : machine_id garanti (extrait du token validé) │ └─────────────────────────────────────────────────────────────┘ ↓ (Étape C : 1j) ↓ ┌─────────────────────────────────────────────────────────────┐ │ Durcissement (Phase 3 — Suppression Global) │ ├─────────────────────────────────────────────────────────────┤ │ • RPA_API_TOKEN supprimé du bootstrap │ │ • Middleware accepte UNIQUEMENT tokens par-poste │ │ • Anciens agents (global) : rejetés (404) │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Annexe B : Checklist de Vérification Pré-Déploiement - [ ] **DB** : Table postes_tokens créée et indexée, migration de la DB testée - [ ] **Code** : Middleware dual-mode (par-poste + fallback global) compilé - [ ] **Cache** : Vérifier que le cache mémoire ne fuit pas (taille bornée à 1000 tokens) - [ ] **Audit** : Chaque validation loggée avec indication du mode (par-poste ou global) - [ ] **Endpoint register** : Génère token unique, hash stocké en DB, token retourné UNE FOIS - [ ] **Endpoint revoke** : Invalidation du token en DB + vidage du cache - [ ] **Endpoint rotate** : Génère nouveau token, ancien invalidé - [ ] **UI Dashboard** : Affichage liste postes (actifs + révoqués), boutons revoke/rotate - [ ] **Tests unitaires** : Vérifier que token global + par-poste fonctionnent (Étape A) - [ ] **Tests intégration** : Workflow complet (enroll → register → streaming → audit) - [ ] **Logs de sécurité** : Aucun token en clair, aucune variable d'env loggée - [ ] **Documentation** : Mise à jour du PLAYBOOK_DSI_RSSI avec procédure par-poste --- **Document généré** : 2026-06-01 **Auteur** : Audit factuel (sans modifications de code) **Validation requise avant implémentation** : Dom (chef projet)