Einrichtung einer sicheren Python-Sandbox für LLM-Agents
Anton Shemyakov
Da Large Language Models zunehmend in Rechensysteme integriert werden, wächst ihre Bedeutung für die Steigerung der Effizienz und Genauigkeit von Anwendungen. Diese erweiterten Fähigkeiten bringen jedoch neue Risiken mit sich, insbesondere bei der Ausführung von autonom generiertem Code.
Dieser Blogartikel untersucht, wie eine sichere Python-Sandbox für LLM-Agents eingerichtet werden kann. Wir werden die Bedrohungen beleuchten, die von LLM-generiertem Code ausgehen, und eine Sandbox-Lösung vorstellen, die auf gVisor und Jupyter Notebook basiert.
LLM-Agenten
Die Einbindung von LLMs in Softwareanwendungen kann auf verschiedene Weise erfolgen, die sich auf einem sogenannten „Agency-Spektrum“ (Handlungsspielraum) bewegen. Am unteren Ende dieses Spektrums steht die einfache Nutzung von LLMs, bei der die Software API-Aufrufe tätigt und Antworten analysiert. Dieser Ansatz ist zwar unkompliziert, aber anfällig für Fehler und Halluzinationen. Am oberen Ende der Skala befinden sich hochentwickelte agentische Systeme, in denen LLMs die Autonomie besitzen, Tools zur Erfüllung von Aufgaben einzusetzen. Diese Systeme zeichnen sich durch ihre Fähigkeit aus, Szenarien ohne vorgegebenen Workflow zu bewältigen – ein Fall, der in realen Anwendungen häufig eintritt.
Während einige agentische Systeme benutzerdefinierte Workflows definieren, indem sie sich für die Nutzung vordefinierter Funktionen (sogenannter Tools) mit spezifischen Parametern entscheiden, können LLM-Agents am oberen Ende des Spektrums eigenen Code schreiben und ausführen. Diese Fähigkeit ist besonders in dynamischen Umgebungen oder bei komplexen Aufgaben nützlich, wie etwa der Erstellung maßgeschneiderter Datenvisualisierungen, die präzise und individuelle Lösungen erfordern. Durch die Generierung und Ausführung von Code können sich diese Agents an die spezifischen Anforderungen jedes Problems anpassen und ein Maß an Individualisierung erreichen, das Standardfunktionen nicht bieten können.
Sandboxing von Code
Mit zunehmendem Handlungsspielraum in KI-Systemen steigen auch die Risiken. Die Ausführung von potenziell unsicherem Code, der von diesen Agents generiert wurde, kann Systeme Sicherheitsrisiken aussetzen. Dazu gehören die Ausführung von beliebigem Code (os.system, subprocess usw.), Ressourcenerschöpfung (Denial-of-Service-Angriffe durch CPU-, Speicher- oder Festplattenüberlastung), unbefugter Dateisystemzugriff (Lesen/Schreiben von Dateien) und vieles mehr. Die Implementierung einer sicheren Methode zur Ausführung dieses Codes ist daher unerlässlich.
Diese Risiken lassen sich durch die Implementierung einer sicheren Python-Sandbox minimieren. Das wesentliche Ziel einer solchen Sandbox besteht darin, Ressourcen zu verwalten und sichere Ausführungsumgebungen zu schaffen, die potenziell schädlichen Code kapseln und verhindern, dass er das Gesamtsystem beeinträchtigt.
Die Demo-Lösung
Eine mögliche Lösung zur sicheren Remote-Ausführung von Python-Code besteht aus einem FastAPI-Server, der einen Jupyter-Notebook Kernel innerhalb eines gVisor-Containers betreibt. So arbeiten die verschiedenen Komponenten der Lösung zusammen:
Jupyter Notebook ermöglicht die Ausführung interaktiver Code-Notebooks. Jupyter-Kernel unterstützen verschiedene Umgebungen, darunter Python, R, Julia und JavaScript. Diese Kernel sind isoliert und verfügen über eingeschränkte Berechtigungen, bieten jedoch keine weiteren Sicherheitsfunktionen. In unserer Lösung fungiert Jupyter Notebook als sofort einsatzbereite Code-Ausführungsumgebung.
FastAPI ist ein modernes Web-Framework für die Erstellung von APIs mit Python. FastAPI dient als Schnittstelle zwischen dem LLM-Agent und dem Jupyter-Kernel und ermöglicht es dem Agent, Code zur Ausführung über das Netzwerk zu senden und Ergebnisse zu empfangen. FastAPI hilft uns dabei, den Agent und die Ausführungsumgebung voneinander zu entkoppeln, was für das Ressourcenmanagement und die Skalierung der Sandbox wichtig ist.
gVisor ist ein User-Space-Kernel, der eine sichere Umgebung für die Ausführung von nicht vertrauenswürdigem Code bietet. Er fungiert als Barriere zwischen dem Code und dem Host-Betriebssystem und verhindert unbefugten Zugriff auf Systemressourcen. gVisor fängt Systemaufrufe des Codes ab und setzt Sicherheitsrichtlinien durch, sodass nur sichere Operationen zugelassen werden. Dies ist eine entscheidende Schutzschicht für das Host-System vor potenziellen Bedrohungen durch die Ausführung von beliebigem Code.
Der folgende Code führt den FastAPI-Sandbox-Server aus:
# ./main.py
import asyncio
from asyncio import TimeoutError, wait_for
from contextlib import asynccontextmanager
from typing import List
from fastapi import FastAPI, HTTPException
from jupyter_client.manager import AsyncKernelManager
from pydantic import BaseModel
app = FastAPI()
allowed_packages = ["numpy", "pandas", "matplotlib", "scikit-learn"]
installed_packages: List[str] = []
class CodeRequest(BaseModel):
code: str
class InstallRequest(BaseModel):
package: str
class ExecutionResult(BaseModel):
output: str
@asynccontextmanager
async def kernel_client():
km = AsyncKernelManager(kernel_name="python3")
await km.start_kernel()
kc = km.client()
kc.start_channels()
await kc.wait_for_ready()
try:
yield kc
finally:
kc.stop_channels()
await km.shutdown_kernel()
async def execute_code(code: str) -> str:
async with kernel_client() as kc:
msg_id = kc.execute(code)
try:
while True:
reply = await kc.get_iopub_msg()
if reply["parent_header"]["msg_id"] != msg_id:
continue
msg_type = reply["msg_type"]
if msg_type == "stream":
return reply["content"]["text"]
elif msg_type == "error":
return f"Error executing code: {reply['content']['evalue']}"
elif msg_type == "status" and reply["content"]["execution_state"] == "idle":
break
except asyncio.CancelledError:
raise
return ""
async def install_package(package: str) -> None:
if package not in installed_packages and package in allowed_packages:
async with kernel_client() as kc:
try:
kc.execute(f"!pip install {package}")
while True:
reply = await kc.get_iopub_msg()
if (
reply["msg_type"] == "status"
and reply["content"]["execution_state"] == "idle"
):
break
installed_packages.append(package)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error installing package: {str(e)}")
@app.post("/install")
async def install(request: InstallRequest):
try:
await wait_for(install_package(request.package), timeout=120)
except TimeoutError:
raise HTTPException(status_code=400, detail="Package installation timed out")
return {"message": f"Package '{request.package}' installed successfully."}
@app.post("/execute", response_model=ExecutionResult)
async def execute(request: CodeRequest) -> ExecutionResult:
try:
output = await wait_for(execute_code(request.code), timeout=120)
except TimeoutError:
raise HTTPException(status_code=400, detail="Code execution timed out")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return ExecutionResult(output=output)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)Diese minimalistische Sandbox-Implementierung stellt zwei Endpunkte zur Verfügung: /execute zur Ausführung von Code und /install zur Installation von Paketen, die auf einer Whitelist stehen. Die Code-Ausführung erfolgt in einem separaten Jupyter-Kernel, der vom AsyncKernelManager verwaltet wird; der Text der Konsolenausgabe wird anschließend an den Client zurückgegeben. Der Server ist darauf ausgelegt, Timeouts und Exceptions kontrolliert zu behandeln.
Das folgende Dockerfile erstellt das Container-Image für den Sandbox-Server:
# Dockerfile
FROM jupyter/base-notebook
WORKDIR /app
COPY main.py /app/main.py
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
# Switch to jovyan non-root user defined in the base image
USER jovyan
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Obwohl dieses Dockerfile sehr einfach gehalten ist, ermöglicht es das Deployment der Sandbox-Lösung in einer containerisierten Umgebung. Der Container wird als Non-Root-User ausgeführt, was einer bewährten Security-Best-Practice entspricht.
Bei dida nutzen wir die Google Kubernetes Engine(GKE) zur Verwaltung unserer Kubernetes-Cluster, welche gVisor nativ als Container-Runtime unterstützt. Um das Deployment von durch gVisor geschützten Workloads zu ermöglichen, müssen wir zunächst einen Node-Pool erstellen, der die GKE-Sandbox aktiviert. Beachten Sie, dass der Cluster zur Aktivierung dieses Sicherheitsfeatures über einen zweiten Standard-Node-Pool verfügen sollte, da von GKE verwaltete System-Workloads getrennt von nicht vertrauenswürdigen, in einer Sandbox ausgeführten Workloads betrieben werden müssen.
Sobald der Node-Pool erstellt ist, können wir das Sandbox-Container-Image mit dem folgenden Kubernetes-Manifest im Cluster bereitstellen:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: agent-sandbox
namespace: demos
labels:
app: agent-sandbox
spec:
replicas: 1
selector:
matchLabels:
app: agent-sandbox
template:
metadata:
labels:
app: agent-sandbox
spec:
runtimeClassName: gvisor
containers:
- name: agent-sandbox
image: "${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}:${IMAGE_TAG}"
ports:
- name: http
containerPort: 8000
protocol: TCP
resources:
requests:
memory: "250Mi"
cpu: "250m"
limits:
memory: "500Mi"
cpu: "500m"Beachten Sie, dass das Feld runtimeClassName auf gvisor gesetzt ist. Dies weist Kubernetes an, gVisor als Container-Runtime für dieses Deployment zu verwenden. Um die Ressourcenallokation der Sandbox zu steuern, definieren wir resource requests und limits für CPU und Arbeitsspeicher. Dies stellt sicher, dass der Sandbox-Container über ausreichend Ressourcen für den Betrieb verfügt, während gleichzeitig verhindert wird, dass er übermäßig viele Ressourcen verbraucht, was andere Workloads im Cluster beeinträchtigen könnte.
Fähigkeiten der Demo-Lösung
Die Demo-Lösung ist einfach zu deployen und zu verwalten, wodurch sie sich für verschiedene Anwendungsfälle eignet. Die Schnittstelle ist über eine REST-API zugänglich, was sie framework-agnostisch macht und die Integration mit jedem LLM-Agent ermöglicht. Die Lösung ist modular aufgebaut, sodass bei Bedarf neue Funktionen und Erweiterungen hinzugefügt werden können. Beispielsweise lassen sich Support für weitere Programmiersprachen ergänzen oder Integrationen mit anderen Tools und Services realisieren. Zudem kann die Lösung problemlos skaliert werden, um erhöhte Workloads zu bewältigen, indem mehrere Instanzen des Sandbox-Containers in einem Kubernetes-Cluster bereitgestellt werden. Containerization minimiert den Performance-Overhead im Vergleich zu herkömmlichen virtuellen Maschinen, was sie für Hochleistungsanwendungen geeignet macht. Obwohl es sich um einen Proof of Concept für eine Code-Sandbox handelt, demonstriert die Demo die folgenden Sicherheitsmerkmale:
Eine eigenständige containerisierte Sandbox bietet Isolation und minimiert Abhängigkeiten zwischen Agents.
Python-Imports sind begrenzt, was Risiken im Zusammenhang mit Dependency-Bedrohungen reduziert.
Die folgenden Sicherheitsmerkmale werden durch die Nutzung von gVisor als Container-Runtime bereitgestellt:
Isolation der Ausführungsumgebung vom Host-System.
Sandboxing von gVisor selbst gegenüber dem Host-Kernel.
Betrieb des Containers mit minimalen Berechtigungen.
Kontinuierliche Entwicklung und Wartung von gVisor durch Security-Experten, wodurch aktuelle Sicherheitsfeatures gewährleistet sind.
Kubernetes ermöglicht ein effizientes Ressourcenmanagement für CPU, Arbeitsspeicher und Speicherplatz.
Einschränkungen der Demo-Lösung
Die folgenden Einschränkungen der Demo sollten behoben werden, bevor sie in der Produktion eingesetzt werden kann:
Momentan erzeugt jede Anfrage an die Sandbox einen neuen Jupyter-Kernel, was ineffizient ist. Dies kann durch die Wiederverwendung bestehender Kernel oder die Implementierung einer anspruchsvolleren Strategie für das Kernel-Management verbessert werden.
Zusätzlich zur Verwaltung des Lebenszyklus von Jupyter-Kernels sollte die Lösung auch das Session- und State-Management übernehmen. Dies umfasst Authentifizierung, Autorisierung und die Aufrechterhaltung von Benutzersitzungen, um einen sicheren Zugriff auf die Sandbox-Umgebung zu gewährleisten.
Für LLM-Agents kann es vorteilhaft sein, Antworten zu generieren, die nicht-textuelle Elemente enthalten, insbesondere Bilder. Die aktuelle Lösung unterstützt diese Antworttypen nicht, obwohl Bildausgaben von Jupyter unterstützt werden.
Filterung des Sandbox-Ingress- und Egress-Verkehrs, um Data Exfiltration und unbefugten Zugriff auf externe Ressourcen zu verhindern.
Fazit
Die Demo-Lösung umfasst Funktionen wie ein einfaches Deployment, eine framework-agnostische Integration und Skalierbarkeit durch Kubernetes. Sie isoliert Ausführungsumgebungen effektiv mittels gVisor und gewährleistet so eine robuste Security bei minimalem Performance-Overhead. Dennoch müssen für den produktiven Einsatz einige Einschränkungen behoben werden, wie etwa die Optimierung des managements der Jupyter-Kernel, die Aktivierung von Authentifizierung und Autorisierung sowie die Durchsetzung strenger Netzwerksicherheitskontrollen.
Durch den Einsatz von Code-Sandboxes können Teams fortschrittliche LLM-Lösungen mit hohem Handlungsspielraum entwickeln. Dies ermöglicht es diesen Anwendungen, Aufgaben autonom auszuführen und gleichzeitig Sicherheitsrisiken zu minimieren. Da sich die Technologie hinter LLMs ständig weiterentwickelt, wird es für die Nutzung ihres vollen Potenzials auf innovative und wirkungsvolle Weise entscheidend sein, mit robusten und flexiblen Sicherheitsmaßnahmen Schritt zu halten.