30 KiB
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) :
_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/<machine_id> (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)
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/enrollaccepte 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) :
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)
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 <token>
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)
API_TOKEN = os.environ.get("RPA_API_TOKEN", "")
Le token vient de trois sources (ordre de priorité) :
- Variable env
RPA_API_TOKEN(posée par Lea.bat après parsing de config.txt) - Fichier
config.txtdans le ZIP (contient le token global) - Défaut : chaîne vide (agent lancé sans token)
Fichier : deploy/lea_package/Lea.bat (ligne 40-43)
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)
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
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 <token_value> - 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 :
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)
@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)
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/<machine_id>/revoke (sécurisé par auth Bearer)
@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/<machine_id>/rotate (pour l'utilisateur du poste)
@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
- Révoquer : POST
- 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.
- Créer table
postes_tokens+ index dansdata/databases/rpa_data.db - Implémenter classe
PostesTokenRegistry(CRUD simple) - Modifier
_verify_tokenpour :- D'abord chercher le token en par-poste (lookup table)
- Si non trouvé, faire un fallback sur le mode global (API_TOKEN)
- 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):
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).
- Nouvelle version du Dashboard : endpoint
/api/v1/agents/registerqui génère tokens uniques - Modifier
/api/fleet/download/<machine_id>pour injecter le token unique (pas le global) - 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/registeren 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.
- Retirer le fallback global de
_verify_token - Supprimer variable env
RPA_API_TOKENdu bootstrap - Logger un FATAL si la variable est trouvée (debug)
- 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 :
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
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_atetrotation_period_daysà la table postes_tokens - Endpoint
/api/v1/postes/{machine_id}/statusretourne 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) :
- Étape A maintenant : Ajouter table + registre + middleware 2 modes (1-2 jours)
- Étape B début Wallerstein : Redéployer les 5 postes avec tokens uniques (3-5 jours, pendant POC)
- É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)