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#
| Composant | Rôle | Ré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 courte | Interne uniquement, sans persistance |
Garde-fou LiteLLM (integrations/litellm/fahmek_guardrail.py) | Appelle fahmek avant l'envoi au modèle et après la réponse | Parle à fahmek et au proxy, jamais au fournisseur |
Packs de reconnaissance (recognizers/*.yaml) | Motifs, mots de contexte, sommes de contrôle, cas de test | Fichiers 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- Inspection. La passerelle envoie chaque message texte qui part vers le
fournisseur (
user,system,developer,assistant,tool) àPOST /v1/inspectavec 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 partiestype: textsont 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. - Décision à trois valeurs.
allow,redactoublock— jamais un booléen.redactrenvoie le texte original avec les valeurs remplacées par des jetons«ENTITÉ_n»;blockne renvoie jamais de texte. - Transmission. Le texte autorisé ou masqué part vers le fournisseur.
- Restauration (si
reversible: true). La réponse du modèle repasse parPOST /v1/restore; les jetons sont remplacés par les valeurs d'origine. - Réponse à l'utilisateur, avec ses identifiants intacts.
Traitement interne d'une inspection#
- 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. - 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.
- Politique : action par entité (héritée du pack ou surchargée), seuils
par action, cascade sûre (un
blocksous son seuil devientredact, jamaisallow). - 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ée | Emplacement | Durée | Journalisée ? |
|---|---|---|---|
| Texte des requêtes | Nulle part | — | Non |
| Valeurs détectées | Nulle 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 restore | Non |
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#
| Situation | Comportement | Signal |
|---|---|---|
Redis injoignable, reversible demandé, politique fail_open: true | Masquage effectué ; réversibilité seule dégradée | degraded: ["reversible"] |
Idem, politique fail_open: false | Décision block, aucun texte renvoyé | degraded: ["reversible"] |
| Moteur en erreur | allow (texte non inspecté) si fail_open: true, sinon block | findings: [] |
| fahmek injoignable depuis la passerelle | Le garde-fou honore le fail_open de la politique, mis en cache depuis GET /v1/policies ; politique inconnue ⇒ refus | Journal fahmek.degraded / fahmek.unreachable |
| Restauration impossible | La 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.