NOPE LinkedIn

Catégories:
IA

Hors de la boîte : pourquoi le LLM ne doit pas vivre dans le firewall

Hors de la boîte : pourquoi le LLM ne doit pas vivre dans le firewall image

Rubrique: IA Tag: IA Tag: Sécurité Tag: OPNsense Tag: portabilité Tag: no-GPU Tag: DevSecOps

🇬🇧 English version 💻 Code source (AGPL) : github.com/patlegu/cyber-agent-engine

Une question de topologie, pas seulement de sécurité

Les trois premiers articles de cette sĂ©rie ont dĂ©taillĂ© comment cyber-agent-engine encadre un LLM qui pilote un pare-feu de production : jetons plutĂ´t que valeurs rĂ©elles (article 2), politique fail-closed et approbation humaine (article 3). Celui-ci s’attaque Ă  une question antĂ©rieure, presque architecturale avant d’ĂŞtre sĂ©curitaire : oĂą ce LLM doit-il physiquement s’exĂ©cuter ? Sur l’Ă©quipement qu’il pilote, ou Ă  distance, de l’autre cĂ´tĂ© d’un appel rĂ©seau ?

La rĂ©ponse n’est pas arbitraire — elle a dĂ©jĂ  Ă©tĂ© mesurĂ©e, dans l’autre sens, par un projet prĂ©cĂ©dent de ce blog.

Le contraste : ce que l’approche in-box a mesurĂ©

opnsense-ai-firewall (OAF) est un PoC qui pousse la logique inverse jusqu’au bout : le LLM tourne embarquĂ© Ă  l’intĂ©rieur mĂŞme de la VM OPNsense. Pas de sidecar, pas d’appel rĂ©seau vers un service externe — un llama-server compilĂ© natif pour FreeBSD, avec un adaptateur LoRA fine-tunĂ©, tournant sur le mĂŞme noyau que le pf qui filtre le trafic, et pilotant directement l’API REST du firewall depuis une intention en langage naturel. Le PoC fonctionne — 101 fonctions sur 102 validĂ©es, latences mesurĂ©es — et c’est prĂ©cisĂ©ment parce qu’il fonctionne que ses propres conclusions comptent : ses docs listent, mĂ©thodiquement, quatre raisons pour lesquelles cette topologie ne doit jamais aller en production.

Surface d’attaque. OAF chiffre lui-mĂŞme le coĂ»t : la stack llama.cpp/ggml/OpenBLAS ajoutĂ©e pèse environ 125 Mo de code supplĂ©mentaire Ă  auditer, patcher et surveiller, sur une base OPNsense minimale d’environ 600 Mo — une hausse de l’ordre de 20 % de la surface de code d’un Ă©quipement dont la doctrine, historiquement, est justement de rester minimaliste. Chaque CVE dans cette stack devient une CVE du firewall lui-mĂŞme.

Contention CPU. L’infĂ©rence mesurĂ©e par OAF prend environ 10 secondes sur les cĹ“urs de la VM — les mĂŞmes cĹ“urs qui font tourner pf. Pendant ce temps, le filtrage continue, mais en contention, et la latence rĂ©seau du plan de donnĂ©es s’en ressent directement. Un firewall n’a normalement aucune raison de voir sa capacitĂ© de filtrage varier selon qu’un modèle de langage est ou non en train de « rĂ©flĂ©chir ».

Cycle de vie couplĂ©. llama.cpp Ă©volue Ă  un rythme hebdomadaire ; OPNsense publie des versions stables Ă  un rythme bien plus lent. Embarquer le premier dans le second, c’est forcer deux cadences de mise Ă  jour incompatibles Ă  cohabiter dans le mĂŞme artefact de dĂ©ploiement — chaque mise Ă  jour de l’un devient un risque de rĂ©gression pour l’autre.

Audit compromis. Quand un LLM modifie directement config.xml, il n’est pas, au sens compliance, un acteur identifiable de la mĂŞme façon qu’un administrateur nommĂ© passant par une console d’audit. OAF note lui-mĂŞme que le garde-fou existant Ă  ce niveau (--confirm cĂ´tĂ© agent, log local) est un contrĂ´le opĂ©rateur, pas une piste d’audit lĂ©gale ni un rempart anti-malveillant.

Ces quatre points ne sont pas une critique externe portĂ©e sur OAF : ce sont ses propres conclusions, obtenues en poussant le PoC jusqu’au bout pour pouvoir les mesurer plutĂ´t que les supposer. C’est prĂ©cisĂ©ment ce travail qui rend la conclusion inverse — celle que cyber-agent-engine met en Ĺ“uvre — actionnable plutĂ´t que dogmatique.

La réponse externe : rien ne rentre dans la boîte

cyber-agent-engine inverse la topologie terme Ă  terme. Le composant qui « rĂ©flĂ©chit » — le CoordinatorLLM de coordinator/llm/coordinator_llm.py, qui propose des intentions Ă  partir du contexte tokenisĂ© — ne tourne jamais sur l’Ă©quipement rĂ©seau qu’il pilote. Il tourne sur l’hĂ´te du coordinateur, quel qu’il soit, et ne parle au firewall qu’au travers d’un appel HTTPS vers son API REST, sur le rĂ©seau LAN. Terme Ă  terme, les quatre objections d’OAF s’Ă©vanouissent :

  • Surface d’attaque : le firewall ne gagne aucun code, aucune dĂ©pendance, aucun binaire supplĂ©mentaire. Il reste l’Ă©quipement OPNsense qu’il Ă©tait, interrogĂ© par un client HTTP externe — exactement le mĂŞme genre de trafic qu’un tableau de bord de supervision ou un script d’automatisation classique.
  • Contention CPU : l’infĂ©rence consomme les cycles CPU/GPU de l’hĂ´te du coordinateur, jamais ceux qui filtrent le trafic rĂ©seau. pf ne partage rien avec le LLM.
  • Cycle de vie : le coordinateur se dĂ©ploie, se met Ă  jour et se redĂ©marre indĂ©pendamment du firmware OPNsense. Une mise Ă  jour du backend de raisonnement n’implique aucune fenĂŞtre de maintenance sur le firewall.
  • Audit : chaque appel Ă  l’API OPNsense passe par la frontière dĂ©crite dans les articles 1 et 3 — politique fail-closed, approbation humaine sur les capacitĂ©s sensibles, journal d’audit bornĂ©, jetons plutĂ´t que valeurs rĂ©elles. L’acteur qui modifie la configuration n’est plus un processus opaque Ă  l’intĂ©rieur du firewall : c’est un appel HTTP authentifiĂ©, tracĂ© de bout en bout, dont chaque dĂ©cision a laissĂ© une entrĂ©e d’audit avant exĂ©cution.

Le firewall redevient ce qu’il devrait toujours ĂŞtre : un Ă©quipement rĂ©seau qui expose une API, rien de plus. Toute l’intelligence — et tout le risque qu’elle porte — se dĂ©place de l’autre cĂ´tĂ© du câble.

Portabilité : un cœur sans GPU, des backends interchangeables

Sortir le LLM de la boĂ®te n’a de valeur que si le composant qui l’hĂ©berge n’est pas lui-mĂŞme figĂ© sur une machine particulière. C’est le deuxième bĂ©nĂ©fice de la topologie externe, et il se lit directement dans pyproject.toml : les dĂ©pendances de base du paquet —

dependencies = [
    "pydantic>=2.0.0",
    "httpx>=0.25.0",
    "pyyaml>=6.0",
    "jinja2>=3.0.0",
    "cryptography>=42.0.0",
    "fastapi>=0.110.0",
    "requests>=2.31.0",
    "anthropic>=0.40.0",
    "uvicorn>=0.30.0",
]

— ne contiennent ni torch, ni vllm, ni aucun poids de modèle. torch, vllm et unsloth sont isolés dans un extra optionnel, [gpu] :

[project.optional-dependencies]
gpu = [
    "torch>=2.1.0",
    "vllm>=0.6.0",
    "unsloth",
]

clients/gpu.py matĂ©rialise cette frontière dans le code : le chargeur vLLM in-process n’est jamais importĂ© au chargement d’un module, uniquement Ă  la demande, et si l’extra [gpu] n’est pas installĂ©, l’Ă©chec est un GpuExtraRequired explicite plutĂ´t qu’un ImportError opaque au milieu d’une stack trace. Le cĹ“ur du coordinateur — politique, tokenisation, boucle gatĂ©e, audit — s’installe et tourne sans jamais toucher une carte graphique.

Le corollaire de ce cĹ“ur allĂ©gĂ©, c’est que le backend de raisonnement devient un paramètre, pas une dĂ©pendance structurelle. coordinator/llm/coordinator_llm.py le documente sans ambiguĂŻtĂ© :

Backends configurables via COORDINATOR_BACKEND :
  - "anthropic"      : Claude API (claude-sonnet-4-6) — recommandé, aucun service local requis
  - "openai"         : API OpenAI-compatible — fonctionne aussi avec vLLM HTTP (port 8000)
  - "vllm"           : NativeVLLMClient direct (instance séparée, charge Qwen2.5-7B)
  - "ollama"         : Ollama local (si disponible)

Et dans le code, la sĂ©lection est un simple branchement sur une variable d’environnement, avec anthropic en valeur par dĂ©faut :

COORDINATOR_BACKEND = os.getenv("COORDINATOR_BACKEND", "anthropic")
async def init(self) -> None:
    """Initialise le backend LLM sélectionné."""
    if self._backend == "anthropic":
        self._init_anthropic()
    elif self._backend == "openai":
        self._init_openai_client()
    elif self._backend == "vllm":
        await self._init_vllm()
    else:  # ollama
        self._http = httpx.AsyncClient(
            base_url=OLLAMA_BASE_URL,
            timeout=httpx.Timeout(120.0),
        )

Quatre chemins, une seule interface publique (chat(messages, max_tokens)), et aucun des trois autres appelants du coordinateur — proposer, boucle, audit — n’a besoin de savoir lequel est actif. En pratique, ça veut dire qu’on peut dĂ©marrer un dĂ©ploiement sur l’API Anthropic (zĂ©ro service local, latence de quelques secondes, comme Ă©voquĂ© dans un article prĂ©cĂ©dent sur les compromis OpenRouter/LoRA de ce blog), puis basculer vers un vLLM auto-hĂ©bergĂ© ou un Ollama local pour des raisons de souverainetĂ© ou de coĂ»t, sans toucher une ligne du core/, de la politique ou de la boucle gatĂ©e. Le raisonnement est un module remplaçable ; la confiance structurelle ne l’est pas.

Cette portabilitĂ© tient aussi Ă  ce que cyber-agent-engine ne dĂ©pend pas d’un pipeline d’entraĂ®nement privĂ© pour fonctionner. Le repo public sur lequel repose tout cet article s’auto-suffit : coordinator/, core/, clients/ et agents/ sont les seuls modules dont dĂ©pend le raisonnement, et aucun d’eux n’importe de paquet d’entraĂ®nement LoRA propriĂ©taire. La fabrication des adaptateurs LoRA — quand on choisit malgrĂ© tout le backend vllm avec un modèle fine-tunĂ© — est un sujet de dĂ©pĂ´t sĂ©parĂ©, dĂ©couplĂ© : le coordinateur ne sait rien de la façon dont un poids a Ă©tĂ© produit, il consomme une interface de chat gĂ©nĂ©rique. C’est ce dĂ©couplage, autant que l’absence de GPU dans le cĹ“ur, qui rend le choix du backend rĂ©versible plutĂ´t que figĂ© au moment du design.

Cibler un vrai firewall : un client REST comme un autre

Concrètement, comment le coordinateur atteint-il l’Ă©quipement ? Pas par un protocole maison, ni par un accès privilĂ©giĂ© quelconque — par un client HTTP standard, OPNsenseAPIClient (clients/opnsense_api_client.py), dont le constructeur ne cache aucune magie :

def __init__(
    self,
    base_url: str,
    api_key: str,
    api_secret: str,
    verify_ssl: bool = True,
    timeout: int = 30
):
    self.base_url = base_url.rstrip('/')
    self.api_key = api_key
    self.api_secret = api_secret
    self.verify_ssl = verify_ssl
    self.timeout = timeout
    self.client = httpx.AsyncClient(
        base_url=self.base_url,
        auth=(api_key, api_secret),
        verify=verify_ssl,
        timeout=timeout
    )

auth=(api_key, api_secret) : de l’authentification HTTP Basic, la mĂŞme que l’API REST d’OPNsense attend nativement — rien de spĂ©cifique Ă  ce projet. verify_ssl reste un paramètre explicite, jamais implicitement dĂ©sactivĂ© dans le code du client lui-mĂŞme.

CĂ´tĂ© serveur d’agents, server.py lit quatre variables d’environnement pour construire cette configuration :

OPNSENSE_URL    = os.getenv("OPNSENSE_URL", "https://192.168.1.1")
OPNSENSE_KEY    = os.getenv("OPNSENSE_API_KEY")
OPNSENSE_SECRET = os.getenv("OPNSENSE_API_SECRET")
"verify_ssl": os.getenv("OPNSENSE_VERIFY_SSL", "False").lower() == "true",

OPNSENSE_URL a pour dĂ©faut de code https://192.168.1.1 — la passerelle LAN la plus courante d’un OPNsense fraĂ®chement installĂ©, pas un secret, juste un dĂ©faut raisonnable Ă  surcharger. OPNSENSE_API_KEY et OPNSENSE_API_SECRET n’ont, eux, aucun dĂ©faut : sans eux, OPNSENSE_KEY reste None et l’agent OPNsense bascule en mode simulation plutĂ´t que d’Ă©chouer silencieusement. OPNSENSE_VERIFY_SSL est Ă  False par dĂ©faut — cohĂ©rent avec le certificat auto-signĂ© qu’expose une interface d’administration OPNsense fraĂ®chement dĂ©ployĂ©e sur son LAN, Ă  surcharger Ă  true dès qu’un certificat de confiance est en place.

Le README du projet documente cette section sous le titre « Targeting a real OPNsense (interop) », avec le mĂŞme tableau de variables et la mĂŞme recommandation : la joignabilitĂ© est vĂ©rifiĂ©e par un GET /api/core/system/status, et c’est prĂ©cisĂ©ment ce endpoint que server.py sonde au dĂ©marrage pour confirmer que la connexion fonctionne avant de marquer l’agent OPNsense comme opĂ©rationnel. Le README va jusqu’Ă  nommer explicitement OAF comme cible de test possible pour quiconque veut expĂ©rimenter cette architecture externe sans monter une VM OPNsense de zĂ©ro — la boucle se referme : le projet qui a mesurĂ© pourquoi le LLM ne doit pas vivre dans le firewall sert aussi de banc d’essai pour la topologie qui le fait vivre Ă  cĂ´tĂ©.

La réserve qui ne se négocie pas

Pointer le coordinateur sur un vrai OPNsense Ă©largit la surface d’exposition d’une seule chose : l’API REST elle-mĂŞme. D’oĂą une règle qui n’est pas une suggestion mais une condition de dĂ©ploiement : l’API OPNsense doit ĂŞtre exposĂ©e sur l’interface LAN uniquement, jamais sur le WAN, et une règle de pare-feu doit explicitement autoriser l’hĂ´te qui fait tourner le coordinateur Ă  atteindre ce port — pas un any gĂ©nĂ©reux, une règle nominative. C’est la mĂŞme discipline de moindre privilège que le reste de l’architecture applique au raisonnement du LLM ; elle s’applique tout aussi strictement Ă  la connectivitĂ© rĂ©seau qui le relie Ă  l’Ă©quipement qu’il pilote. Sortir le modèle de la boĂ®te ne dispense de rien cĂ´tĂ© rĂ©seau — ça dĂ©place simplement le pĂ©rimètre Ă  protĂ©ger d’un processus embarquĂ© vers un chemin HTTPS LAN, plus facile Ă  isoler, Ă  journaliser et Ă  couper.

flowchart LR
  subgraph Box["VM OPNsense"]
    API["API REST OPNsense — interface LAN"]
    inbox["approche in-box (OAF)<br/>LLM dans le firewall"]
  end
  Coord["cyber-agent-engine<br/>coordinateur hors-boîte, sur le LAN"] -->|"HTTPS LAN + clé/secret"| API
  Coord -->|"backend LLM (hors-boîte)"| LLM["Anthropic / OpenAI-compat / vLLM / Ollama"]
  inbox -.->|"contention CPU, surface d'attaque"| API

Ce que cet article change dans la lecture de la série

Les articles prĂ©cĂ©dents montraient une frontière de confiance interne au coordinateur — jetons, politique, approbation. Celui-ci ajoute une frontière topologique : le LLM lui-mĂŞme est de l’autre cĂ´tĂ© d’un appel rĂ©seau, jamais colocalisĂ© avec l’Ă©quipement qu’il gouverne. Les deux frontières se complètent plutĂ´t que de se substituer l’une Ă  l’autre — retirer le modèle de la boĂ®te ne dispense d’aucune des garanties dĂ©crites dans les articles 2 et 3, et inversement, un core/ fail-closed parfaitement conçu ne rend pas anodine une infĂ©rence tournant Ă  l’intĂ©rieur du pare-feu qu’elle pilote. C’est la combinaison des deux qui rend l’ensemble opĂ©rable en production — le sujet du prochain article de cette sĂ©rie.


Cet article fait partie de la sĂ©rie sur cyber-agent-engine. Le premier article pose les cinq principes de la frontière de confiance ; un article Ă  venir montrera comment ces briques — politique, boucle gatĂ©e, approbation, audit, et maintenant la topologie hors-boĂ®te — s’assemblent en un système opĂ©rable en production.

Pour aller plus loin :

  • opnsense-ai-firewall — le PoC in-box dont les quatre limites mesurĂ©es (surface d’attaque, contention CPU, cycle de vie, audit) motivent directement l’architecture externe dĂ©crite ici.
  • Article 1 — trust core — la thèse gĂ©nĂ©rale de la sĂ©rie et le rĂ´le du core/ fail-closed.