Files
rpa_vision_v3/docs/POC/AUDIT_TOKEN_PAR_POSTE_2026-06-01.md

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.pyPOST /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/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.pyPOST /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é) :

  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)

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
  • 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):

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/<machine_id> 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 :

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_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)