NOPE LinkedIn

Catégories:
IA
Security

opnsense-ai-firewall : embarquer un LLM dans le firewall, et mesurer pourquoi c'est une mauvaise idée en production

opnsense-ai-firewall : embarquer un LLM dans le firewall, et mesurer pourquoi c'est une mauvaise idée en production image

Rubrique: IA Rubrique: Security Tag: lora Tag: opnsense Tag: llm Tag: firewall Tag: llama.cpp Tag: phi-3 Tag: edge-ai Tag: freebsd Tag: tool-calling Tag: devsecops

opnsense-ai-firewall : embarquer un LLM dans le firewall, et mesurer pourquoi c’est une mauvaise idée en production

Résumé Exécutif (Management Summary)

La question expérimentale : est-il techniquement possible de faire tourner un assistant LLM à l’intérieur d’une VM OPNsense, capable de comprendre une intent administrateur en langage naturel et de la traduire en appels API REST OPNsense — sans aucun sidecar, sans aucun trafic LLM en clair sur le réseau, sans dépendance à un service externe ?

Réponse courte : oui, et la preuve de concept couvre 101 sur 102 fonctions canoniques de l’API OPNsense. Le repo public opnsense-ai-firewall documente l’architecture, fournit les artefacts (binaire llama-server natif FreeBSD, GGUF Phi-3+LoRA fusionné, agent Python local) et reproduit le pipeline intent → tool_call → API sur n’importe quelle VM OPNsense disposant de 8 GB de RAM.

La méthodologie en bref : la stack repose sur trois ingrédients indépendants : (1) une VM OPNsense quelconque, (2) un llama-server compilé natif FreeBSD (effort principal du projet — tag llama.cpp ≥ b9000 pour le support natif du format tools), et (3) un GGUF Phi-3 mini Q4_K_M fusionné avec un LoRA spécialisé sur les 102 fonctions canoniques de l’API OPNsense (patlegu/opnsense-agent-phi35 sur Hugging Face). Un agent Python local fait la traduction tool_call → REST en strict 127.0.0.1.

Le défi critique : la faisabilité technique n’est pas le sujet. Le vrai sujet est que cette topologie est délibérément déconseillée en production pour quatre raisons concrètes : surface d’attaque (un firewall doit être minimaliste, on lui ajoute ~125 MB de code et toute la stack llama.cpp/ggml/OpenBLAS), contention CPU pendant les ~10 s d’inférence (la latence pf s’effondre sous charge), cycle de vie incompatible (llama.cpp évolue chaque semaine, OPNsense en versions stables), et perte d’audit (un LLM qui modifie config.xml n’est pas un acteur identifiable au sens compliance).

Recommandation stratégique : ce projet sert trois usages valides — démonstration pédagogique, mesure des seuils (“à partir de quand ça casse”) pour informer un futur produit, et laboratoire d’expérimentation pour de nouveaux LoRAs ou patterns d’agent in-box. Toute organisation cherchant cette capacité en production doit revenir au modèle sidecar : LLM sur une VM dédiée, OPNsense vanilla, passerelle API auditée entre les deux.


I. Le pari : un LLM dans le firewall, et pas à côté

1.1. Contexte stratégique : Edge-AI maximale

L’écosystème actuel des assistants IA en sécurité réseau converge majoritairement vers un pattern simple : le LLM tourne dans le cloud (OpenAI, Anthropic, Mistral hébergés) ou sur un serveur d’entreprise dédié, et un client léger sur l’équipement (firewall, switch, routeur) lui envoie une requête. C’est le modèle évident, performant, scalable — et celui qui pose le plus de problèmes de confidentialité quand les données d’analyse sont des logs pf, des règles de filtrage ou des topologies réseau internes.

Une alternative connue est l’Edge-AI : déplacer l’inférence au plus près de l’équipement. Dans le marché des firewalls, cela se traduit aujourd’hui par des appliances avec NPU intégré, ou par un sidecar — une VM/conteneur séparé sur le même hôte de virtualisation, qui héberge le LLM et expose une API au firewall. C’est un excellent compromis : la donnée ne sort pas du périmètre, la séparation de responsabilités est propre, le cycle de vie du LLM est découplé du firewall.

Et puis il y a la question qui ne se pose presque jamais : pourquoi pas dans le firewall directement ? Pas à côté, pas sur un hôte adjacent — dedans. Même CPU, même RAM, même VM. Le firewall pilote sa propre API en strict 127.0.0.1, aucun trafic LLM n’existe sur le réseau, l’opérateur tape une commande SSH et reçoit une réponse — toute la chaîne est confinée.

C’est cette question expérimentale que le repo opnsense-ai-firewall traite, et la réponse a une forme particulière : « oui, c’est faisable, et voilà pourquoi il ne faut quand même pas le faire — mais maintenant on sait précisément pourquoi ».

1.2. Cadrage du PoC

Le périmètre du PoC est explicite et restreint :

  • Cible matérielle : VM Hetzner Cloud cx33 (4 vCPU partagés, 8 GB RAM, CPU-only). C’est délibérément modeste — on veut savoir si ça tourne partout, pas seulement sur du matériel exotique.
  • Cible logicielle : OPNsense 26.1.x (FreeBSD 14 amd64), llama.cpp tag b9000+, Phi-3 mini Q4_K_M, agent Python 3.11+.
  • Cible fonctionnelle : couvrir l’intégralité des fonctions de l’API REST OPNsense exposées par le SDK interne. La référence canonique est verify_opnsense_v2.py (run v7), qui définit 102 fonctions allant de get_system_status à block_ip en passant par add_vlan et restart_unbound.
  • Critère de succès : une intent en langage naturel ("Show system information", "Block IP 1.2.3.4 on WAN") doit produire un appel API correct et, pour les actions mutating, créer une vraie règle pf avec son UUID OPNsense.

Le repo principal est public (github.com/patlegu/opnsense-ai-firewall), le LoRA et le GGUF mergé sont sur Hugging Face (patlegu/opnsense-agent-phi35), et la documentation détaillée — y compris les logs de validation et les justifications de design — est versionnée en français et en anglais dans le repo.


II. Architecture concrète

2.1. La stack : trois ingrédients indépendants

Le repo insiste explicitement sur ce point : il n’y a que trois ingrédients réellement nécessaires pour faire tourner le système. Tout le reste (modules Tofu, automatisation Hetzner, scripts d’init) est du confort.

Ingrédient 1 — Une VM OPNsense. N’importe laquelle, n’importe quel hôte. OPNsense 26.1.x amd64 (FreeBSD 14), 8 GB de RAM minimum pour faire tenir le modèle (~2.4 GB) + pf + le reste de la VM. Ça peut être Hetzner Cloud, libvirt local sur un homelab, baremetal, peu importe.

Ingrédient 2 — Un llama-server compilé natif FreeBSD. C’est l’effort principal du projet, et le plus instructif. Pas de Linuxulator, pas de Docker (le firewall n’a pas Docker), pas de binaire pré-compilé qu’on trouverait sur internet (llama.cpp ne distribue pas de release FreeBSD officiel). On clone le repo ggerganov/llama.cpp sur la version b9000 (la première à supporter nativement le format tools + l’option --jinja pour les templates de chat), on pkg install cmake openblas python311 lapack pkgconf, et on lance un cmake -DGGML_OPENBLAS=ON -DBUILD_SHARED_LIBS=ON ... qui produit un binaire llama-server + 6 bibliothèques partagées (libllama, libggml*, libopenblas, libgfortran, libquadmath). Total : environ 8.9 MB de binaire + 36 MB de .so.

Ce build se fait sur une VM FreeBSD séparée (le firewall ne contient pas la toolchain), via un script scripts/build-llama-freebsd.sh qui automatise toute la séquence. Le résultat est ensuite SCPé sur l’OPNsense et stocké dans /var/llm/.

Ingrédient 3 — Le GGUF Phi-3+LoRA fusionné. Disponible sur Hugging Face : opnsense-agent-phi35-q4_k_m.gguf, environ 2.4 GB en quantization Q4_K_M. L’avantage du modèle fusionné (base Phi-3 mini + LoRA déjà appliqué) est que llama-server n’a qu’un seul fichier à charger, sans option --lora-init-without-apply (qui a un historique buggué selon les versions de llama.cpp). C’est plus simple, plus rapide à démarrer, et évite une classe entière de bugs.

Plus, côté OPNsense, un pkg install python311 pour l’agent local. C’est tout. Aucune dépendance Tofu, aucune dépendance externe. Avec ces trois éléments, le pipeline marche sur n’importe quelle OPNsense.

2.2. Topologie d’exécution

┌─────────────────────────────────────────────────┐
│  OPNsense 26.x (FreeBSD 14)                     │
│  ┌───────────────────────────────────────────┐  │
│  │ llama-server (FreeBSD natif, b9000+)      │  │
│  │   bind 127.0.0.1:8080                     │  │
│  │   merged Phi-3 mini Q4_K_M + LoRA OPNsense│  │
│  └─────────────────┬─────────────────────────┘  │
│                    │ HTTP local                  │
│  ┌─────────────────▼─────────────────────────┐  │
│  │ agent local (Python 3.11)                 │  │
│  │   intent NL → tool_call (101/102)         │  │
│  │   → OPNsense API 127.0.0.1:4443           │  │
│  │   scope_confirmed sur tout mutating       │  │
│  └───────────────────────────────────────────┘  │
│                                                 │
│   pf · NAT · DHCP · DNS · …                     │
└─────────────────────────────────────────────────┘

Trois propriétés fortes :

  • Aucun sidecar. Tous les composants vivent dans la même VM, partagent le même CPU/RAM/disque que pf.
  • Aucun trafic LLM en clair sur le réseau. Le binding llama-server est strictement 127.0.0.1:8080, jamais exposé sur WAN ni LAN. L’agent parle à llama-server en localhost et à l’API OPNsense en localhost (127.0.0.1:4443).
  • Aucun port externe lié au LLM. Du WAN, OPNsense ressemble à n’importe quel firewall OPNsense vanilla. Le LLM est invisible depuis l’extérieur.

llama-server est démarré par un service rc.d FreeBSD standard (/usr/local/etc/rc.d/llama), géré par daemon(8), avec rotation de logs dans /var/log/llama/. L’agent Python est un wrapper /usr/local/bin/oaf-agent qui appelle le script principal oaf_agent.py.

2.3. Le pipeline intent → tool_call → API : où est le travail intelligent ?

C’est la question qui doit être posée frontalement, parce que sinon l’architecture donne l’illusion d’être un simple dispatcher REST : « il y a un catalog de 116 endpoints OPNsense, des adaptateurs de payload, du routage HTTP — à quoi sert vraiment le LLM ? »

Le LLM fait les choses non mécaniques. Le reste du code est la table d’écriture entre ce que le LoRA produit et l’API REST. Décortiquons un appel complet sur une commande de blocage d’IP.

Étape 1 — L’humain écrit en langage naturel :

oaf-agent ask "Block IP 1.2.3.4 on WAN" --confirm

Étape 2 — Le LoRA Phi-3 fait quatre choses non triviales. L’agent envoie au LoRA l’intent textuelle plus la liste des 116 outils disponibles avec leurs descriptions (get_cron_jobs, add_filter_rule, block_ip, restart_unbound, …). Le modèle produit :

<|tool_calls|>
[{"id": "call_…",
  "type": "function",
  "function": {
    "name": "block_ip",
    "arguments": "{\"ip\": \"1.2.3.4\", \"interface\": \"wan\"}"
  }}]

Ces quatre décisions sont le travail intéressant :

  1. Compréhension de l’intent — “Block IP” est un blocage, pas un ajout d’alias, pas un NAT, pas une route. Distinguer entre les ~10 familles d’actions OPNsense possibles à partir d’une phrase libre.
  2. Choix du bon outil — parmi 116 tools dans le contexte, sélectionner block_ip (et pas add_filter_rule brut, ni add_to_alias). Le LoRA a appris cette correspondance pendant son fine-tuning sur ~13 700 exemples annotés.
  3. Extraction structurée des paramètres — repérer que 1.2.3.4 est l’IP cible, que WAN désigne l’interface, et produire un JSON propre. L’intent ne dit pas interface=wan ; le LoRA infère.
  4. Format OpenAI tool_call — émettre la réponse dans la structure exacte que l’agent peut parser (special tokens <|tool_calls|><|tool_response|> propres à Phi-3, respectés grâce au fine-tuning).

Étape 3 — La plomberie (mécanique, sans intelligence). Une fois le tool_call produit, l’agent fait du pur routage :

  • Lookup dans TOOLS_EFFECTIVE["block_ip"]("POST", "/api/firewall/filter/addRule", mutating=True).
  • mutating=True + --confirm présent → on continue (sinon, l’agent s’arrête et demande confirmation explicite).
  • ARG_ADAPTERS["block_ip"]({"ip": "1.2.3.4", ...}) transforme le payload simple émis par le LoRA en payload réel attendu par l’API OPNsense :
{"rule": {
  "enabled": "1", "action": "block", "interface": "wan",
  "direction": "in", "ipprotocol": "inet", "protocol": "any",
  "source_net": "1.2.3.4", "destination_net": "any",
  "description": "Blocked by oaf-agent"
}}
  • POST HTTPS vers 127.0.0.1:4443 avec Basic auth (clé/secret OPNsense, utilisateur breachsim dans le groupe admins).
  • OPNsense crée la règle pf et renvoie {"result": "saved", "uuid": "a17955a6-..."}.

Sans LoRA, pour faire la même chose à la main, il faudrait :

  1. Savoir que « bloquer une IP » se fait via le module firewall/filter, pas alias, pas source_nat.
  2. Connaître l’endpoint exact : /api/firewall/filter/addRule.
  3. Connaître tous les champs requis par le schema (8 champs obligatoires, dont direction et ipprotocol qui ne sont pas évidents).
  4. Construire le JSON manuellement.
  5. Envoyer le curl avec auth Basic et certificat self-signed.

Le LoRA fait les étapes 1-4 à partir d’une phrase. C’est ça qu’on embarque dans le firewall — pas un dispatcher REST. Un Phi-3 mini de base ne saurait pas qu’OPNsense a un endpoint /api/firewall/filter/addRule ni qu’un champ s’appelle source_net. Il hallucinerait des appels API plausibles mais faux. Le LoRA apporte la connaissance domain-specific sur les 102 fonctions canoniques de l’API.

C’est aussi pour ça qu’un modèle 3.8 B params suffit : on n’a pas besoin de raisonnement général, juste d’un mapping fiable intent → tool_call dans le domaine OPNsense. Phi-3 mini est le bon compromis taille/performance pour une inférence CPU-only sur 4 vCPU partagés.


III. Validation expérimentale

3.1. Couverture fonctionnelle : 101 / 102

Le LoRA est entraîné sur une grammaire d’intents qui couvre les 102 fonctions canoniques définies dans verify_opnsense_v2.py. Mais entre « le LoRA connaît la fonction » et « l’agent sait la dispatcher vers le bon endpoint REST », il y a un travail de mapping nom-LoRA → méthode-client → endpoint-REST qui est partiellement automatisé.

Le script generate-tools-catalog.py (resté côté GitLab privé pour ne pas exposer le repo de training) fait ce travail en trois passes :

  1. Extraction des noms canoniques depuis verify_opnsense_v2.py via parsing AST des _cap("name", ...).
  2. Extraction des méthodes du client SDK OPNsense avec résolution AST de la variable endpoint.
  3. Croisement avec alias automatiques (del_Xdelete_X) et alias manuels (NAME_ALIASES_TO_CLIENT pour les divergences sémantiques type block_ipadd_filter_rule).

Résultat sur le run v7 :

  • 97 fonctions auto-résolues et écrites dans agent/tools_catalog.py.
  • 4 fonctions ajoutées via TOOLS_LOCAL_OVERRIDES dans oaf_agent.py (alias sémantiques que l’AST ne peut pas détecter automatiquement, type list_firewall_states ou get_wireguard_peers).
  • 1 fonction non dispatchable : import_alias, qui n’a tout simplement pas d’endpoint REST natif côté OPNsense (c’est une fonction interne du WebUI).

Bilan : 101 / 102 fonctions canoniques opérationnelles, soit ~99 % de couverture du domaine.

3.2. Démo end-to-end : block_ip

Trace réelle de validation (extraite de docs/demo-results.md, légèrement simplifiée pour la lecture) :

$ oaf-agent ask "Block IP 1.2.3.4 on WAN" --confirm
[oaf] llama-server  : 200 OK (latence: 10.2 s)
[oaf] tool_call     : block_ip(ip="1.2.3.4", interface="wan")
[oaf] adapter       : payload OPNsense {"rule":{...,"source_net":"1.2.3.4"}}
[oaf] mutating      : --confirm présent → POST autorisé
[oaf] OPNsense API  : POST /api/firewall/filter/addRule
[oaf] OPNsense API  : 200 {"result":"saved","uuid":"a17955a6-...-..."}
[oaf] pf            : règle créée (UUID a17955a6)

Vérification côté OPNsense, juste après :

$ ssh -p 2222 root@<OPNSENSE_IP> 'pfctl -sr | grep 1.2.3.4'
block drop in quick on em0 inet from 1.2.3.4 to any

La règle pf est bien active. Une intent en langage naturel, ~10 secondes d’inférence, une règle réelle dans le firewall — sans qu’aucun trafic ne quitte la VM.

3.3. Latence mesurée

Sur cx33 Hetzner (4 vCPU partagés, CPU-only, OpenBLAS) :

  • Time-to-first-token : 1.5 à 3 s (variable selon la longueur du contexte de tools envoyé).
  • Throughput inférence : 7 à 10 tokens/seconde.
  • Latence totale typique d’une intent simple (tool_call court, ~80 tokens en sortie) : 8 à 12 s.
  • Latence totale d’une intent complexe (multi-paramètres, ~200 tokens) : 15 à 20 s.

Sur cx43 (8 vCPU partagés, 16 GB) : amélioration d’environ 30-40 % sur la latence totale. Pas linéaire — Phi-3 mini Q4_K_M plafonne sur la bande passante mémoire avant la pure puissance de calcul.

Ces chiffres sont à comparer avec un sidecar : une VM dédiée avec 8 vCPU et 16 GB tournant uniquement llama-server ferait la même inférence en 4-6 s. Mais elle coûterait une seconde VM, et surtout, elle ne répondrait pas à la question « peut-on tout faire tenir dans le firewall ».


IV. Pourquoi PAS en production : les quatre raisons critiques

C’est la partie la plus importante de ce projet, et le repo y consacre un document complet (docs/why-not-in-prod.md). Synthèse.

4.1. Surface d’attaque

Un firewall doit être minimaliste. Le PoC ajoute sur OPNsense :

  • Un binaire ELF de 8.9 MB (/var/llm/bin/llama-server).
  • 6 bibliothèques partagées (~36 MB cumulés), dont OpenBLAS (28 MB seul).
  • Python 3.13 (~80 MB via pkg) plus l’écosystème de sa stdlib.
  • Un service rc.d daemonisé (llama) qui écoute sur un port local.

Soit environ 125 MB de code supplémentaire et un sous-système entier à auditer / patcher / monitorer. Chaque CVE dans llama.cpp, ggml, OpenBLAS ou Python devient ta CVE. Pour comparaison, une OPNsense minimale fait ~600 MB. On augmente la base de code à auditer d’environ 20 %.

Mitigations déjà en place dans le repo : binding strict 127.0.0.1 (jamais sur WAN ni LAN), pas d’API publique, mais le service tourne en root sans wrapper de privilèges. Un attaquant qui obtient un shell local sur l’OPNsense a toute la boîte à outils en main — y compris la possibilité de remplacer le GGUF par un modèle prompt-injecté.

4.2. Contention CPU pendant l’inférence

Une intent administrateur = ~10 secondes d’inférence sur cx33 (4 vCPU partagés). Pendant ces 10 secondes :

  • llama-server sature les 4 vCPU à 100 % (matmul + BLAS).
  • pf (le firewall) continue à filtrer le trafic, mais en contention sur les mêmes cores.

Sur un firewall qui sert peu de trafic (lab, agence sans pic), c’est gérable. Sur un edge router qui forwarde 500 Mbps :

  • La latence forwarding p99 explose pendant l’inférence.
  • Le throughput peut baisser de 20 à 40 % pour la durée de la requête.
  • Les retransmissions TCP s’accumulent.

Mitigation possible : cpuset pour épingler llama-server sur un seul core. Mais les 7-10 tok/s deviennent alors 2-3 tok/s → 30 secondes par intent. On dégrade soit la latence pf, soit le LLM. Pas d’échappatoire CPU-only.

4.3. Cycle de vie incompatible

OPNsense suit FreeBSD avec un cycle de release stable (1 à 2 versions par an). llama.cpp bouge tous les jours :

  • Changements ABI/format GGUF tous les ~2 mois.
  • Nouveaux formats de quantization (Q4_K_M devient obsolète, Q4_K_M_v2, …).
  • Support de nouveaux templates de chat (notre passage b3813 → b9000 pour le support natif tools).

Si on déploie en production aujourd’hui et qu’on ne touche pas pendant 6 mois, le binaire FreeBSD est figé sur b9000 alors que le LoRA qu’on voudrait charger demain ne sera plus compatible. Le repo fournit un script de rebuild (scripts/build-llama-freebsd.sh) mais c’est toujours toi qui possèdes la cadence.

Sur un sidecar VM/conteneur, le LLM se met à jour indépendamment. OPNsense reste OPNsense.

4.4. Audit & imputabilité

Les firewalls passent en audit. Quand une règle change, on doit savoir qui l’a écrite et pourquoi. Avec un LLM qui modifie config.xml :

  • <modified><time></modified> dit « modified by root ».
  • Aucune trace de l’intent initiale qui a déclenché le changement.
  • Pas de four-eyes review (le LLM n’est pas une signature).
  • En cas d’incident : « le LLM a halluciné un block sur 8.8.8.8 » n’est pas une justification recevable en compliance.

Mitigation : le repo a un garde-fou --confirm côté agent + un log audit dans oaf_agent.py. Mais c’est un contrôle opérateur, pas un contrôle anti-malicieux ni une piste d’audit légale.

L’architecture sidecar permet, elle :

  • de logger l’intent originale + l’utilisateur humain qui l’a tapée ;
  • de générer la règle proposée + la faire valider par un humain via PR ;
  • de conserver un log audit immuable en dehors du firewall.

V. Recommandations stratégiques

5.1. Le pattern sidecar reste le bon en production

Pour toute organisation cherchant cette capacité (assistant LLM piloté en langage naturel sur le firewall) en production, la recommandation est sans ambiguïté : architecture sidecar.

┌─────────────────┐  HTTPS   ┌─────────────────────────┐
│  OPNsense (vanilla)         │  VM Debian dédiée         │
│  - pf, NAT, DHCP, DNS      │←─────────│  - llama-server                 │
│  - API REST 443             │  - agent Python                 │
│  - rien d'autre             │  - GGUF + LoRA                 │
└─────────────────┘          └─────────────────────────┘
        ▲                                   ▲
        │                                   │
        │     gateway API auditée (PR-based?)     │
        │            ┌────────┴────────┐
        └───────────│  humain operator    │
                       └─────────────────┘

Le pattern public se résume à : une VM Debian avec llama-server, plus un agent Python qui parle à l’API OPNsense en HTTPS. ~50 lignes de Tofu, un docker-compose ou un systemd unit, et c’est plié. Quelques implémentations existent dans l’écosystème *-forge interne, mais le pattern est suffisamment trivial pour être reproduit indépendamment.

5.2. Cas d’usage légitimes du in-box

Le repo opnsense-ai-firewall a trois usages valides :

  1. Démonstration pédagogique — montrer que c’est techniquement possible, pour les ateliers, les conférences, les démos clients qui comprennent visuellement ce que fait un LLM in-box. C’est puissant comme outil de pédagogie « voici l’extrême ».
  2. Mesure de seuils — quantifier « à partir de quand ça casse » pour informer un futur produit. Combien de Mbps pf peut-il forwarder pendant qu’une inférence sature les CPU ? À quel point la latence p99 dégrade ? Quelles sont les fenêtres de tolérance ? Ces mesures (palier E.3 du repo) sont précisément la valeur de cette topologie « mauvaise idée ».
  3. Laboratoire de recherche — tester de nouveaux LoRAs, de nouveaux formats de prompt, de nouveaux patterns d’agent in-box. Le coût d’expérimentation est minimal (une VM cx33 à ~5 € par mois) et l’environnement est complet.

5.3. Au-delà du PoC : ouverture

Plusieurs pistes ouvertes :

  • Mesures de perf rigoureuses (palier E.3) : iperf3 baseline + sous charge inférence, p99 forwarding, throughput dégradation par tranches de tok/s. C’est ce qui transforme « c’est une mauvaise idée » en « voici précisément où et de combien ».
  • Élargissement du LoRA au-delà des 102 fonctions canoniques : VLANs complexes, IPsec multi-phase, OPNsense plugins (Suricata, ClamAV, ZenarmoR). Le format de training est documenté côté repo privé, l’ouverture éventuelle dépend de la maturité de la pipeline.
  • Modèles encore plus petits : Phi-3 mini est déjà petit, mais Phi-3.5 mini Q3_K_S (~1.8 GB) ou Gemma 2 2B Q4 (~1.5 GB) pourraient suffire pour ce domaine très contraint. Gain RAM significatif → compatibilité cx32 4 GB ou homelab basse consommation.
  • Pattern « mock LLM » pour la CI : remplacer llama-server par un mock qui renvoie des tool_calls scriptés en CI, pour tester la chaîne agent → API sans payer 10 s d’inférence par test. Le repo expose déjà l’interface, la séparation est propre.

VI. Pour aller plus loin

Le repo principal

github.com/patlegu/opnsense-ai-firewall — code, documentation bilingue FR/EN, scripts de build et de déploiement, logs de validation. Le README.md insiste explicitement sur les 3 prérequis minimaux (OPNsense + llama-server FreeBSD + GGUF) pour souligner que le pattern est reproductible partout, et pas un produit clé-en-main Hetzner.

Les modèles

huggingface.co/patlegu/opnsense-agent-phi35 — LoRA + GGUF fusionné, documentation HF complète sur le training set (~13 700 exemples annotés), les métriques de validation (102 / 102 sur le run v7) et les limitations connues (pas de raisonnement multi-step, pas de pondération de risque entre actions, pas de planning).

La documentation détaillée

Dans le repo, en français et en anglais :

  • docs/why-not-in-prod.md — les 4 raisons en version longue.
  • docs/build-llama-freebsd.md — la compilation native FreeBSD avec tous les pièges (SONAME des .so, ETXTBSY au SCP, ABI breakage entre tags).
  • docs/embedded-llm.md — l’architecture du service rc.d + l’agent local.
  • docs/demo-results.md — les traces de validation read + write.
  • docs/access-credentials.md — secrets, SSH, API OPNsense.

Conclusion

Mettre un LLM dans un firewall, c’est techniquement faisable. Le PoC opnsense-ai-firewall valide la chaîne complète : 101/102 fonctions canoniques OPNsense dispatchables depuis une intent en langage naturel, ~10 s de latence sur du matériel modeste, aucun trafic LLM sur le réseau, aucune dépendance externe. La stack tient sur trois ingrédients indépendants (OPNsense, llama.cpp FreeBSD, GGUF Phi-3+LoRA) que n’importe qui peut reproduire.

Et c’est délibérément une mauvaise idée en production, pour quatre raisons que le projet documente méthodiquement : surface d’attaque, contention CPU, cycle de vie, audit. La valeur de ce projet n’est pas de devenir un produit — c’est de savoir précisément pourquoi ça n’en deviendra pas un, et d’informer les futures architectures (sidecar, appliance NPU dédiée, agents API gateway) avec des mesures concrètes plutôt qu’avec de l’intuition.

C’est exactement le genre de PoC qui mérite d’exister : faisable, instrumenté, transparent sur ses propres limites, et ouvertement utile à ne pas faire en l’état. Le pattern à retenir en production reste le sidecar, mais le savoir vient d’avoir mesuré l’extrême.