fahmek
Dans ce guide

fahmek — Architecture et flux de données

Document destiné au comité sécurité. Version du moteur : voir engine_version dans toute réponse de l'API.

1. Ce que fait le service, en une phrase#

fahmek se place entre la passerelle LLM de l'organisation (LiteLLM, Portkey, Kong…) et les fournisseurs de modèles, et remplace les identifiants personnels marocains présents dans les messages par des jetons opaques avant que le texte ne quitte le périmètre. Les identifiants détectés et masqués selon la politique sont remplacés par des jetons. La couverture n'est pas absolue : un identifiant sans contexte peut rester sous le seuil, et une politique fail-open peut laisser passer une requête lors d'une panne. L'évaluation par registre doit mesurer ces limites avant toute promesse de couverture.

2. Composants#

ComposantRôleRéseau
fahmek (service HTTP, Python/FastAPI)Détection, décision, masquage, restaurationÉcoute sur le réseau interne uniquement
Redis (facultatif)Carte de ré-identification jeton → valeur, en mémoire, par locataire, à durée de vie courteInterne uniquement, sans persistance
Garde-fou LiteLLM (integrations/litellm/fahmek_guardrail.py)Appelle fahmek avant l'envoi au modèle et après la réponseParle à fahmek et au proxy, jamais au fournisseur
Packs de reconnaissance (recognizers/*.yaml)Motifs, mots de contexte, sommes de contrôle, cas de testFichiers chargés au démarrage
Politiques (policies/*.yaml)Entités actives, actions, seuils, fail_open, TTL de réversibilitéFichiers chargés au démarrage

Le service n'embarque aucun modèle d'apprentissage automatique : expressions régulières, sommes de contrôle (mod 97, ISO 7064) et mots de contexte. Il n'effectue aucun appel réseau sur le chemin de la requête, hormis Redis lorsque la réversibilité est explicitement demandée. Pas de télémétrie, pas de serveur de licence, pas de téléchargement à l'exécution : il fonctionne isolé du réseau (air-gapped).

3. Flux d'une requête#

utilisateur ──▶ passerelle LLM ──(1)──▶ fahmek /v1/inspect ──▶ décision
                                             │
                 texte masqué ◀──(2)─────────┘
                      │
                     (3) ──▶ fournisseur de modèle (ne voit que des jetons)
                      │
                 réponse ──(4)──▶ fahmek /v1/restore ──(5)──▶ utilisateur
  1. Inspection. La passerelle envoie chaque message texte qui part vers le fournisseur (user, system, developer, assistant, tool) à POST /v1/inspect avec l'identifiant de politique, du locataire et un espace de noms opaque propre au message. Ainsi, deux identifiants du même type dans des messages distincts ne partagent jamais un jeton. Les tableaux OpenAI de parties type: text sont concaténés pour l'inspection, puis renvoyés sous forme d'une partie texte masquée. Les autres formats et les arguments d'appels d'outils sont refusés (400), même sans champ content.
  2. Décision à trois valeurs. allow, redact ou block — jamais un booléen. redact renvoie le texte original avec les valeurs remplacées par des jetons «ENTITÉ_n» ; block ne renvoie jamais de texte.
  3. Transmission. Le texte autorisé ou masqué part vers le fournisseur.
  4. Restauration (si reversible: true). La réponse du modèle repasse par POST /v1/restore ; les jetons sont remplacés par les valeurs d'origine.
  5. Réponse à l'utilisateur, avec ses identifiants intacts.

Traitement interne d'une inspection#

  1. Normalisation : NFKC, conversion des chiffres arabes-indiens (٠١٢٣, ۰۱۲۳) en ASCII, suppression du tatweel et des caractères de contrôle bidirectionnels (U+202E…). Une table d'index relie chaque position du texte normalisé à la position originale, en points de code Unicode. Cela neutralise les contournements classiques : chiffres en écriture arabe, insertion d'un contrôle bidi au milieu d'un numéro.
  2. Détection sur le texte normalisé : motifs, puis somme de contrôle (un RIB à clé invalide n'est pas un RIB), puis ajustement de la confiance par les mots de contexte et les listes de préfixes best-effort.
  3. Politique : action par entité (héritée du pack ou surchargée), seuils par action, cascade sûre (un block sous son seuil devient redact, jamais allow).
  4. Masquage sur l'original : les positions sont reprojetées sur le texte d'origine ; les portions non masquées sont identiques octet pour octet à l'entrée. Le texte normalisé ne sort jamais du service.

4. Ce qui est stocké, et où#

DonnéeEmplacementDuréeJournalisée ?
Texte des requêtesNulle part—Non
Valeurs détectéesNulle part sauf si reversible: true—Jamais
Carte jeton → valeur (réversibilité)Redis, en mémoire, clé dérivée de tenant_id + request_id (SHA-256)TTL de la politique (300 s par défaut), consommée au premier restoreNon
Résultats (findings)Réponse HTTP uniquement : type, positions, confiance, action—À la charge de la passerelle

Règle non négociable : Redis tourne avec save "" et appendonly no. fahmek vérifie ces réglages au démarrage ; il avertit bruyamment par défaut et refuse de démarrer si une politique chargée est fail_open: false. Un déploiement avec persistance activée viole le contrat.

Isolation entre locataires : un locataire B ne peut pas restaurer une requête du locataire A (clé différente par construction) ; la réponse est identique qu'une carte n'existe pas ou appartienne à un autre locataire, sans fuite d'existence.

5. Modes de défaillance#

SituationComportementSignal
Redis injoignable, reversible demandé, politique fail_open: trueMasquage effectué ; réversibilité seule dégradéedegraded: ["reversible"]
Idem, politique fail_open: falseDécision block, aucun texte renvoyédegraded: ["reversible"]
Moteur en erreurallow (texte non inspecté) si fail_open: true, sinon blockfindings: []
fahmek injoignable depuis la passerelleLe garde-fou honore le fail_open de la politique, mis en cache depuis GET /v1/policies ; politique inconnue ⇒ refusJournal fahmek.degraded / fahmek.unreachable
Restauration impossibleLa réponse est renvoyée avec ses jetons (les valeurs restent protégées)Journal fahmek.degraded stage=post_call

La direction sûre est toujours la même : en cas de doute, masquer davantage, jamais laisser fuir.

6. Limites déclarées#

La détection est probabiliste pour les identifiants sans spécification publique (CIN, CNSS) et de forme seule pour ceux dont l'algorithme de contrôle n'est pas publié (ICE). Le catalogue de détection, généré automatiquement depuis les packs, porte pour chaque identifiant son niveau de confiance et la formulation exacte de ses limites. Hors périmètre en v1 : reconnaissance d'entités nommées arabe, détection d'injection de prompt, inspection en flux, interface de contrôle.

7. Éléments de preuve#

  • Suite de tests automatisée exécutée à chaque modification : normalisation (propriété d'aller-retour sur des entrées arbitraires), packs (cas dans les cinq registres linguistiques, négatifs inclus), API (paires requête/réponse de référence), réversibilité (aller-retour identique octet pour octet, expiration, Redis en panne, isolation locataire), garde-fou, budget de latence (p99 < 15 ms sur 1 000 requêtes mixtes).
  • Test d'intégration bout en bout : pile compose complète, charge utile reçue par le fournisseur vérifiée sans identifiant original.
  • Aucune donnée personnelle réelle dans le dépôt : tous les jeux d'essai sont synthétiques, avec des préfixes réservés ou fictifs.

Cette page est le guide livré avec fahmek, tenu à jour à chaque version.