LLM Security

Sandboxing d'un agent IA : architecture de confinement sécurisée

Sandboxing des agents IA exécutant du code : conteneur, microVM (Firecracker, gVisor), e2b.dev/Modal, network egress, secrets, threat model et patterns prod.

Naim Aouaichia
12 min de lecture
  • sandboxing
  • confinement
  • architecture
  • code execution
  • LLM security

Un agent IA qui exécute du code généré par LLM, accède à un filesystem ou parle au réseau doit être confiné. Pas pour le confort opérationnel : pour la même raison qu'un binaire untrusted ne tourne pas en prod sans sandbox — la confiance est mathématiquement impossible. AutoGPT en 2023 a documenté plusieurs cas d'évasion via shell access mal configuré ; les serveurs MCP avec permissions OS larges sont la même classe en 2025-2026. Un sandbox bien conçu n'est pas une option, c'est l'hygiène de base.

Cet article documente les patterns de sandboxing (conteneur, microVM Firecracker, gVisor, Kata, application-level Pyodide/WASM), les services managés (e2b.dev, Modal, AWS Bedrock Agents), les patterns de filesystem, network et secrets, et un threat model spécifique aux agents. Pour la surface globale agents : sécuriser un agent IA autonome. Pour MCP : sécurité MCP.

Pourquoi un agent IA exige un sandbox spécifique

Trois propriétés rendent le sujet critique :

  1. Code généré par LLM = code untrusted. L'agent reçoit un input utilisateur, génère du code (Python pour analyse, shell pour deploy, SQL pour requête), exécute. La frontière "input externe → code exécuté" est franchie à chaque tour.
  2. Tools systèmes. Filesystem, shell, navigateur, accès réseau. Chaque tool est une voie d'évasion potentielle si le confinement est faible.
  3. Manipulabilité par injection. Prompt injection, tool poisoning, sub-goal hijacking — l'agent peut être amené à exécuter ce qu'il ne devrait pas. Le sandbox est la dernière ligne quand toutes les couches au-dessus ont été contournées.

Info — La catégorie OWASP de référence est LLM06 Excessive Agency combinée avec LLM05 Improper Output Handling quand le code généré est l'objet de l'attaque.

Threat model : que doit contrer le sandbox

MenaceManifestationImpact si non contrée
Code execution arbitraireAgent exécute Python/shell généré par LLMCompromission hôte
Filesystem escapeLecture / écriture hors périmètreVol de secrets, persistence
Network exfiltrationConnexion vers domaine attaquantExfiltration données
Process escapeÉvasion du conteneur vers l'hôteCompromission complète
Privilege escalationÉlévation root via exploit kernelIdem
Resource exhaustionDoS local (CPU, mémoire, disque)Indisponibilité
Lateral movementPivot vers d'autres servicesCompromission étendue
Token / secret leakLecture de variables d'environnementAccès cloud / APIs

Chaque menace doit être contrée par une couche dédiée. Aucune ne se couvre seule.

Trois niveaux de sandboxing

Niveau 1 — Conteneur durci (Docker/Podman)

Le minimum opérationnel pour un agent qui n'exécute pas de code arbitraire mais a des tools systèmes restreints.

FROM python:3.12-slim
 
# User non-root
RUN useradd -r -u 1001 -m agent
USER agent
WORKDIR /home/agent
 
# Déps fixées
COPY --chown=agent:agent requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
 
COPY --chown=agent:agent ./app /home/agent/app
 
ENTRYPOINT ["python", "/home/agent/app/main.py"]
# docker-compose.yml — durcissement
services:
  agent:
    build: .
    read_only: true
    tmpfs:
      - /tmp:size=100M,noexec
      - /home/agent/work:size=500M,noexec
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp-strict.json
      - apparmor:agent-profile
    user: "1001:1001"
    networks:
      - agent-egress
    pids_limit: 100
    mem_limit: 1g
    cpus: 1.0
 
networks:
  agent-egress:
    driver: bridge
    ipam:
      config:
        - subnet: 172.30.0.0/24

Limites du niveau 1 : un kernel exploit, un misconfig (mount socket Docker, capabilities ajoutées, host networking) suffit à évader. Acceptable pour un agent sans exécution de code arbitraire.

Niveau 2 — Isolation kernel-level (gVisor, Kata, Firecracker)

Pour tout agent qui exécute du code généré par LLM. Trois technologies dominantes :

gVisor (Google)

Implémente un kernel applicatif en userspace qui intercepte les syscalls. Démarrage rapide, overhead modéré.

# Runtime gVisor en remplacement de runc
docker run --runtime=runsc \
  --read-only \
  --tmpfs /tmp \
  --memory=1g --cpus=1 \
  --network=agent-egress \
  agent-image

Kata Containers

Lance chaque conteneur dans une VM légère avec son propre kernel. Isolation forte, démarrage 1-2s.

# RuntimeClass Kubernetes
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata
---
apiVersion: v1
kind: Pod
spec:
  runtimeClassName: kata
  containers:
    - name: agent
      image: agent:1.0
      securityContext:
        runAsNonRoot: true
        readOnlyRootFilesystem: true

Firecracker (AWS)

microVM minimaliste, démarrage < 125 ms. Utilisé par AWS Lambda et la majorité des services serverless. Standard de l'industrie pour les sandboxes éphémères.

# Lancement Firecracker minimaliste
firecracker --config-file vm-config.json
{
  "boot-source": {
    "kernel_image_path": "vmlinux-5.10",
    "boot_args": "console=ttyS0 reboot=k panic=1 nomodules"
  },
  "drives": [
    {
      "drive_id": "rootfs",
      "path_on_host": "agent-rootfs.ext4",
      "is_root_device": true,
      "is_read_only": true
    }
  ],
  "machine-config": {
    "vcpu_count": 1,
    "mem_size_mib": 512
  },
  "network-interfaces": [
    {
      "iface_id": "eth0",
      "host_dev_name": "tap-agent-1",
      "guest_mac": "AA:FC:00:00:00:01"
    }
  ]
}

Choix typique

CasRecommandation
Agent custom self-hostedKata sur K8s, ou gVisor pour densité
Sandbox éphémère par requêteFirecracker (e2b, Modal, custom)
Code execution Python onlyPyodide dans worker isolé + gVisor en backstop
Multi-tenant SaaSFirecracker microVM par tenant/session

Niveau 3 — Application-level (Pyodide, WASI)

Sandbox dans le langage, complémentaire au niveau OS.

Pyodide

Python compilé en WebAssembly. Exécution dans navigateur ou worker, pas d'accès direct OS.

import { loadPyodide } from "pyodide";
 
const pyodide = await loadPyodide();
// Le code Python s'exécute dans un environnement WASM isolé
const result = pyodide.runPython(`
import math
math.pi * 2
`);
// Pas d'accès OS, pas de réseau (sauf si fetch wrappé explicitement)

WebAssembly (WASI)

Standard plus général. Modules WASM compilés depuis Rust, Go, C, etc., s'exécutent dans un runtime sandboxé (wasmtime, wasmer).

# Wasmtime côté Python
from wasmtime import Engine, Store, Module, Instance
 
engine = Engine()
store = Store(engine)
module = Module.from_file(engine, "untrusted.wasm")
# Pas d'accès syscalls par défaut, capabilities explicites via WASI
instance = Instance(store, module, [])

Limites

WASM/Pyodide sont excellents pour le calcul pur, limités pour les agents qui ont besoin d'I/O réseau ou filesystem. Pattern recommandé : WASM/Pyodide à l'intérieur d'un microVM Firecracker — défense en profondeur, isolation à deux niveaux.

Patterns de filesystem

Pattern 1 — Read-only rootfs + tmpfs writable

# Conteneur en read-only avec /tmp tmpfs
docker run \
  --read-only \
  --tmpfs /tmp:size=100M,mode=1777,noexec \
  --tmpfs /home/agent/work:size=500M,mode=755,noexec \
  agent-image

Aucune persistance possible. Si l'agent crée des fichiers, ils disparaissent à la fin de la session.

Pattern 2 — Volume éphémère scope par session

volumes:
  - type: bind
    source: /var/lib/sandbox/sessions/${SESSION_ID}/work
    target: /home/agent/work
    read_only: false

Le volume est créé au démarrage, supprimé à la fin. Permet la persistance pendant la session, isole les sessions entre elles.

Pattern 3 — Snapshots immutables

Pour les microVMs (Firecracker), utiliser des snapshots de rootfs immutables. Chaque session démarre depuis le snapshot — pas d'altération possible du système de base.

Patterns de network

Pattern 1 — No network

Pour les sandboxes purs (calcul, parsing, validation). Aucune sortie réseau.

docker run --network=none agent-image

Pattern 2 — Egress allowlist via proxy

Le sandbox a accès réseau uniquement via un proxy filtrant. Le proxy autorise par regex de domaine.

# Proxy egress avec allowlist
services:
  egress-proxy:
    image: tinyproxy:latest
    volumes:
      - ./tinyproxy.conf:/etc/tinyproxy/tinyproxy.conf:ro
    networks:
      agent-egress:
        ipv4_address: 172.30.0.10
 
  agent:
    networks:
      - agent-egress
    environment:
      - HTTP_PROXY=http://172.30.0.10:8888
      - HTTPS_PROXY=http://172.30.0.10:8888
# tinyproxy.conf — allowlist
Allow 172.30.0.0/24
Filter "/etc/tinyproxy/filter"
FilterDefaultDeny Yes
 
# /etc/tinyproxy/filter
^api\.openai\.com$
^api\.anthropic\.com$
^api\.yourcompany\.com$
^pypi\.org$

Tout autre domaine → refusé. Tentative d'exfiltration vers attacker.example → bloquée au proxy + alerte SOC.

Pattern 3 — Network namespace dédié avec eBPF

Pour les déploiements à grande échelle, utiliser Cilium ou équivalent pour des règles réseau eBPF par pod/sandbox. Plus performant, plus expressif que les ACL iptables traditionnelles.

Gestion des secrets

Règle absolue : aucun secret dans les variables d'environnement du sandbox. Aucun secret dans le filesystem du sandbox. Aucun secret dans les manifests visibles au sandbox.

Pattern recommandé : ephemeral credentials via mediator

┌──────────────┐  request token   ┌────────────────┐
│ Sandbox      │ ────────────────►│ Credentials    │
│ (LLM agent)  │                  │ Mediator        │
│              │ ◄────────────────│ (host-side)    │
│              │  ephemeral token │                │
└──────────────┘  scope=narrow    └────────────────┘
        │                                  │
        │ uses token (TTL 30s)             │ vault auth
        ▼                                  ▼
┌──────────────┐                  ┌────────────────┐
│ Tool / API   │                  │ Vault / KMS    │
│              │                  │                │
└──────────────┘                  └────────────────┘

Le sandbox ne voit jamais les credentials persistantes. Il demande un token court via un mediator host-side (mediator a accès au vault, sandbox n'y a pas accès direct). Token TTL court (30s), scope minimal pour la requête. Cf. privilege escalation.

Services managés en 2026

ServiceTypeCas typique
e2b.devSandbox Firecracker à la demandeCode execution agent, fast boot
ModalServerless avec isolation forteFunction-style agent execution
AWS Bedrock AgentsManagé AWS, action sandboxingStack AWS native
DaytonaDev workspaces sandboxésCoding agents
RizaCode execution sandbox spécialiséMath/code execution agent
Anthropic Code ExecutionSandbox managé ClaudeClaude-native code execution

Avantages managés : démarrage rapide, scaling, conformité (SOC 2, ISO), maintenance déléguée. Inconvénients : data residency, pricing pay-per-use, dépendance vendor.

Pour les déploiements self-hosted : Firecracker ou Kata sur K8s, gVisor pour densité, custom orchestrator.

Cas réels d'évasion documentés

CasAnnéePattern d'évasion
AutoGPT shell access exploits2023Shell tool sans confinement
ChatGPT Code Interpreter sandboxing research2023+Tests d'évasion publics, OpenAI a renforcé
Various Docker escapes (kernel CVEs, misconfigs)diversÉvasion conteneur classique
MCP server permissions excessives2024-2025Filesystem MCP avec accès home complet
Modal / e2b benchmarks d'isolation2024-2025Validation publique des frontières

Pattern d'orchestration recommandé

import e2b  # ou wrapper custom Firecracker
from contextlib import contextmanager
 
@contextmanager
def secure_sandbox_for_agent(session_id: str, allowlist: list[str]):
    """Provisionne un sandbox éphémère sécurisé pour une session agent."""
    sbx = e2b.Sandbox(
        template="agent-base",  # rootfs audité
        timeout=300,             # 5 min max
        metadata={"session_id": session_id},
        env_vars={},             # AUCUN secret ici
        firewall={
            "egress_allowlist": allowlist,
        },
        cpu=1,
        memory_mb=512,
    )
    try:
        yield sbx
    finally:
        sbx.kill()  # garantie de cleanup
 
# Usage
with secure_sandbox_for_agent("sess_42", ["api.openai.com", "api.yourcompany.com"]) as sbx:
    result = sbx.run_python(generated_code, timeout=30)

Chaque session = un sandbox neuf, démarré à partir d'un rootfs immuable, kill à la fin. Pas de réutilisation entre sessions ou tenants.

Pattern d'instruction system prompt sandbox-aware

RÈGLES D'EXÉCUTION DE CODE :
 
Tu peux exécuter du code Python via le tool `run_code`, mais
ce code s'exécute dans un sandbox isolé sans accès réseau hors
allowlist (api.yourcompany.com uniquement) et sans accès
filesystem hors /home/agent/work.
 
Tu NE DOIS JAMAIS :
- Écrire du code qui tente d'accéder à /etc, /root, /home/*
  (autres que /home/agent/work).
- Écrire du code qui ouvre des connexions réseau hors allowlist.
- Écrire du code qui lit des variables d'environnement
  (os.environ).
- Écrire du code qui invoque des binaires système (subprocess).
 
Si l'utilisateur te demande explicitement une de ces opérations,
refuse poliment en signalant que c'est hors du périmètre du
sandbox. Demande s'il y a une alternative dans le périmètre
autorisé.

Checklist d'audit sandbox minimum

#ContrôleÉvidence attendue
1User non-rootUID ≥ 1000
2Capabilities Linux drop=ALLConfiguration runtime
3Read-only rootfsMount config
4tmpfs avec noexec pour /tmp et workspacesMount config
5Seccomp profile strictProfile JSON présent
6AppArmor/SELinux profileProfile présent et appliqué
7no-new-privileges enabledConfiguration
8Network namespace dédiéConfig network
9Egress allowlist via proxyConfig proxy + ACL
10Aucune variable d'env contenant secretInventory env
11Limites CPU/RAM/PIDConfig runtime
12Timeout session strictConfig orchestrator
13Logs structurés des actions sandboxOTel events
14Cleanup garanti à la finHook orchestrator
15Niveau d'isolation (gVisor/Kata/Firecracker pour code arbitraire)Type runtime

Mapping OWASP LLM Top 10 v2

OWASPLien sandboxing
LLM06 Excessive AgencyLimite l'impact des actions générées
LLM05 Improper Output HandlingCode généré confiné
LLM07 System Prompt LeakageAucun secret dans le sandbox
LLM10 Unbounded ConsumptionLimites CPU/RAM/temps
LLM03 Supply ChainRootfs auditable et immuable

LLM06 est la catégorie centrale en termes de réduction d'impact ; le sandbox est la dernière ligne de réduction de blast radius.

Points clés à retenir

  • Un agent IA qui exécute du code doit être sandboxé. C'est l'hygiène, pas un nice-to-have.
  • 3 niveaux : conteneur durci (Docker/Podman avec drop=ALL + seccomp + read-only), isolation kernel-level (gVisor / Kata / Firecracker), application-level (Pyodide / WASI). Combiner pour défense en profondeur.
  • Pour code arbitraire généré par LLM : Firecracker (e2b, Modal) ou gVisor minimum. Docker simple insuffisant.
  • 3 patterns network : no network, egress allowlist via proxy, eBPF avec règles.
  • 3 patterns filesystem : read-only rootfs + tmpfs, volume éphémère scope session, snapshots immutables.
  • Aucun secret dans variables d'env / filesystem / manifests sandbox. Pattern : ephemeral credentials via mediator host-side.
  • Services managés 2026 : e2b.dev, Modal, AWS Bedrock Agents, Daytona, Riza, Anthropic Code Execution.
  • Checklist 15 contrôles minimum pour tout déploiement entreprise.
  • Le sandbox est la dernière ligne : si toutes les couches au-dessus (input filter, system prompt, tool allowlist) ont été bypassées, le sandbox limite encore le blast radius.

Le sandboxing d'agent IA est aujourd'hui un domaine techniquement mature (Firecracker, gVisor, e2b) avec des services managés crédibles et des patterns documentés. Ne pas l'investir, c'est accepter qu'une simple injection puisse compromettre l'hôte. C'est le coût défensif le plus rentable au regard de l'impact évité.

Questions fréquentes

  • Pourquoi un agent IA a-t-il besoin d'un sandbox spécifique vs une app classique ?
    Trois raisons. (1) **L'agent peut exécuter du code généré par un LLM** — un input externe se transforme en code exécutable, ce qui rend la confiance impossible. (2) **Tools accédant au système** : filesystem, shell, network, secrets — chaque tool est une voie potentielle d'évasion. (3) **L'agent est manipulable** par prompt injection, tool poisoning, sub-goal hijacking — il peut être amené à exécuter des actions hors de son périmètre. Un sandbox classique (au sens browser/JVM) ne suffit pas. Il faut un confinement OS-level fort, équivalent à ce qu'on exige d'un binaire untrusted.
  • Conteneur Docker classique suffit-il à sandboxer un agent qui exécute du code ?
    Pas en config par défaut. Un conteneur Docker partage le kernel hôte ; une vulnérabilité kernel ou un misconfig (capabilities, privileged mode, host networking) permet d'évader. Pour un agent qui exécute du code généré par LLM, il faut **au minimum** : user non-root, capabilities Linux drop=ALL, seccomp profile strict, no-new-privileges, read-only rootfs, network namespace dédié avec allowlist, pas de mount de socket Docker, pas de mount du filesystem hôte. Pour les cas critiques : gVisor, Kata Containers, ou Firecracker microVM (isolation kernel-level).
  • Quelle différence entre gVisor, Kata Containers et Firecracker ?
    Trois approches d'isolation forte. **gVisor** (Google) intercepte les syscalls et les implémente en userspace — kernel applicatif distinct, démarrage rapide, overhead modéré. **Kata Containers** lance chaque conteneur dans une légère VM avec son propre kernel — isolation forte, démarrage 1-2s. **Firecracker** (AWS) est une microVM minimaliste pour fonctions courtes — isolation forte, démarrage < 125 ms, utilisée par Lambda et de nombreux services serverless. Pour agents IA exécutant du code untrusted : Firecracker (e2b.dev, Modal) ou gVisor sont les standards en 2026.
  • Comment configurer le réseau d'un sandbox d'agent ?
    Trois patterns selon le besoin. (1) **No network at all** : pour code arithmétique pur (python eval), le sandbox n'a aucun accès réseau. Maximum sécurité. (2) **Egress allowlist** : accès uniquement à des domaines spécifiques (PyPI, GitHub, APIs internes whitelisted) via proxy filtrant. Pattern le plus courant. (3) **Network proxy avec inspection** : tout trafic passe par un proxy qui logue et autorise/refuse par règle. Pour les cas critiques. Jamais d'accès Internet sans filtrage. Outils proxy : Squid, Tinyproxy avec ACL, ou solutions managées (Cilium, AWS PrivateLink).
  • Peut-on faire du sandboxing au niveau application (Pyodide, WebAssembly) ?
    Oui, complémentaire au sandbox OS-level. **Pyodide** : Python compilé en WebAssembly, exécution dans le navigateur ou dans un worker isolé. Excellent pour code analytics simple, limité pour I/O. **WebAssembly (WASI)** : sandbox général pour tout langage compilé en WASM. Très fort sur l'isolation, plus complexe pour les agents qui ont besoin de tools systèmes. Combinaison fréquente : sandbox OS-level (Firecracker) avec runtime application sandboxé (Pyodide) à l'intérieur — défense en profondeur.
  • Quels services managés existent pour le sandboxing d'agents ?
    Plusieurs en 2026. **e2b.dev** : sandboxes Firecracker à la demande, API simple, pricing pay-per-use. **Modal** : serverless avec isolation forte, bonne intégration agents. **AWS Bedrock Agents** : sandboxing managé pour les actions des agents AWS Bedrock. **Daytona** : workspaces dev sandboxés. **Riza** : code execution sandboxes spécialisées. Pour les déploiements custom self-hosted : Firecracker ou gVisor directement, Kata Containers sur Kubernetes. Le bon choix dépend de la fréquence d'usage, de la latence acceptable, et de la conformité (data residency).

Écrit par

Naim Aouaichia

Expert cybersécurité et fondateur de Zeroday Cyber Academy

Expert cybersécurité avec un master spécialisé et un parcours hybride : développement, DevOps, DevSecOps, SOC, GRC. Fondateur de Hash24Security et Zeroday Cyber Academy. Formateur et créateur de contenu technique sur la cybersécurité appliquée, la sécurité des LLM et le DevSecOps.