This blog post is also available in English
TL;DR
So habe ich den Netzwerkzugang meiner Sandbox gezielt abgesichert:
- Squid als Forward Proxy mit einer individuell gepflegten Positivliste, sodass ausschließlich freigegebene Ziele erreichbar sind.
- Sämtlicher Datenverkehr der VM-Sandbox wird verbindlich über diesen Proxy geführt, umgesetzt durch entsprechende Proxy-Konfiguration und nftables-Regeln.
In einem vorherigen Beitrag habe ich meine Lösung beschrieben, wie ich eine Development-Sandbox für meine Coding-Agents aufsetze. Sie basiert auf einer Lima-Virtual-Machine (VM) unter macOS, und ich reduziere die User-Capabilities in dieser VM auf das absolute Minimum. Da mein User in der VM nur die nötigen Rechte hat, ist ein Coding-Agent, der mit den Rechten meines Users läuft, entsprechend ebenfalls eingeschränkt. Der Beitrag beschreibt damit den ersten notwendigen Schritt für eine Sandbox, die stark begrenzt, auf wie viel Code ein Agent überhaupt zugreifen kann. Das schützt vor dem ersten Teil der lethal trifecta, indem Zugriff auf private Daten so weit wie irgend möglich verhindert wird.
Allerdings schützt diese Lösung nicht vor den beiden anderen Bestandteilen der lethal trifecta: Kontakt mit nicht vertrauenswürdigen Inhalten und die Möglichkeit, nach außen zu kommunizieren. Denn die VM, die ich als Sandbox nutze, hängt weiterhin am Internet. Beliebige HTTP-Requests können abgesetzt werden – und damit kann Information aus der Sandbox herausleaken. Außerdem wird das Ergebnis eines solchen Requests ungeprüft in den Systemkontext übernommen, wodurch unsere Agents mit nicht vertrauenswürdigen Inhalten in Kontakt kommen können.
Bislang habe ich mich selbst in der Schleife gehalten und die eingebauten Network-Policy-Tools in Codex und Claude Code genutzt, um jede Verbindung ins Internet explizit zu genehmigen. Ich möchte mich aber wirklich aus dieser Schleife herausnehmen und klare Regeln definieren, welche Websites ohne mein ausdrückliches Einverständnis frei erreichbar sein sollen.
Dieser Beitrag beschreibt meine Lösung für das Problem. Vorab der Hinweis: Damit bewege ich mich etwas außerhalb meiner Komfortzone. Ich bin Full-Stack-Entwickler und halte mich normalerweise von Linux-Konfiguration und Networking eher fern. Ich musste einiges wieder ausgraben, was ich vergessen hatte (wie sieht der TLS-Handshake nochmal genau aus?), und auch vieles lernen, das ich vorher nicht wusste. Aber wenn uns der Aufstieg von KI nicht gelegentlich dazu zwingt, über den Tellerrand dessen hinauszugehen, was wir gewohnt sind – was dann?
Ich habe KI genutzt, um unterschiedliche Lösungsansätze zu analysieren, und KI hat mir geholfen, die Konfiguration zu erstellen, die ich in diesem Beitrag teile. Aber zu keinem Zeitpunkt habe ich einen Agenten „losgelassen“, um entweder den Rechner oder die Sandbox selbst zu konfigurieren. Ich habe die Dokumentation gelesen, alles doppelt und dreifach geprüft und aktiv jede Frage gestellt, die mir eingefallen ist. Wenn dir etwas auffällt, das man verbessern könnte, melde dich bitte. Wie schon beim ersten Beitrag habe ich außerdem unsere INNOQ-Security-Expert:innen gebeten, meinen Ansatz ebenfalls zu prüfen.
Proxy auf dem Host betreiben: Zugriff nur auf definierte Domains
Der erste Schritt war, auf dem Host-System einen Proxy aufzusetzen. Damit kann ich ausgehenden Traffic aus meiner Development-Sandbox überwachen und Regeln definieren, welche Websites erlaubt sind und welche blockiert werden.
Der Weg zu einer Lösung, die für mich funktioniert, war allerdings voller Umwege.
Ich habe schnell gemerkt, dass ich auf keinen Fall jede einzelne IP-Adresse manuell in einer Firewall auf dem Host freigeben will – schon allein, weil ich gar nicht zuverlässig sehen kann, zu welcher Domain diese IPs jeweils gehören.
Zwischendurch hat die KI vorgeschlagen, einen Man-in-the-Middle-Proxy zu erweitern, um jeden Request zu prüfen. Das kann ein valider Ansatz sein, wenn man weiß, was man tut, und es kann hilfreich sein, die Inhalte der Requests, die zur und von der Sandbox gehen, zu inspizieren und zu überwachen. Dafür müsste man allerdings ein selbstsigniertes Zertifikat erzeugen und alle Trust Stores auf der Client-Seite so konfigurieren, dass dieses Zertifikat vertraut wird – und genau das wollte ich lieber vermeiden.
Am Ende habe ich mich für einen Forward Proxy entschieden: einen Proxy, der Requests abfängt und an das Ziel weiterleitet. Damit konnte ich jede CONNECT-Methode abfangen, die die Sandbox sendet, wenn sie eine TLS-Verbindung zum Zielserver aufbauen will. So kann ich diesen Request gegen eine konkrete Allowlist von Domains prüfen. Passt er nicht, wird die Verbindung verworfen.
Um das Rad nicht neu zu erfinden, habe ich mich für den Proxy Squid entschieden. Squid positioniert sich zwar primär als Caching-Proxy, kann aber auch als CONNECT-only-Allowlist-Proxy genutzt werden. Da ich auf macOS arbeite, habe ich Squid über Homebrew installiert. Anschließend habe ich Folgendes in meine Squid-Konfiguration (bei mir unter /opt/homebrew/etc/squid.conf) ergänzt und den Service neu gestartet:
############################################
# Custom: CONNECT-only-Allowlist-Proxy
############################################
# Dev-Proxy soll auf Port 8888 lauschen
http_port 8888
# CONNECT nur zum Standard-TLS-Port 443 erlauben
acl SSL_ports port 443
acl CONNECT method CONNECT
http_access deny CONNECT !SSL_ports
# Proxy-Nutzung nur aus dem Sandbox-Netz erlauben
acl vmnet src 127.0.0.1/32
# Allowlist der Ziel-Domains
acl allowed_domains dstdomain "/opt/homebrew/etc/squid/allowed_domains.txt"
# Nur erlauben: Sandbox-Netz + CONNECT + allowlistete Domains
http_access allow vmnet CONNECT allowed_domains
# Alles andere blockieren
http_access deny allDie Liste allowed_domains.txt lässt sich einfach so konfigurieren, dass nur bestimmte Domains oder Wildcards erlaubt sind:
example.org
.openai.comVM so konfigurieren, dass sie den Proxy auf dem Host nutzt
Dass auf dem Host ein Proxy läuft, ist schön und gut – aber wenn die Development-Sandbox ihn nicht nutzt, bringt das wenig. Als Nächstes musste ich die Umgebung so einrichten, dass die Tools ihren Traffic tatsächlich über den Proxy auf dem Host routen.
Viele Tools (z. B. curl oder Coding-Agents wie codex und claude) verstehen die folgende Konfiguration:
export HOST_IP="<My Host IP>"
export PROXY_PORT="8888"
export HTTPS_PROXY="http://$HOST_IP:$PROXY_PORT"
export HTTP_PROXY="$HTTPS_PROXY"
export NO_PROXY="localhost,127.0.0.1"Für Gradle musste ich zusätzlich meine ~/.gradle/gradle.properties so konfigurieren, dass Folgendes enthalten ist:
systemProp.http.proxyHost=<My Host IP>
systemProp.http.proxyPort=8888
systemProp.https.proxyHost=<My Host IP>
systemProp.https.proxyPort=8888
systemProp.http.nonProxyHosts=localhost|127.0.0.1
systemProp.https.nonProxyHosts=localhost|127.0.0.1Erzwingen, dass die Sandbox ausschließlich über den Proxy kommuniziert
Das Setup bisher erlaubt der Development-Sandbox, mit dem Proxy über den Host zu kommunizieren – aber es zwingt nicht, dass wirklich jeder Traffic aus der Sandbox über den Proxy geht. Wenn sich die Coding-Agents an die Regeln halten, bietet das zwar einen gewissen Schutz, aber eben keinen absoluten.
Um das zu erzwingen, habe ich nftables installiert und die folgende Konfiguration /etc/nftables-proxy-egress.nft erstellt:
table inet sandbox {
chain output {
type filter hook output priority 0; policy drop;
# Loopback-Traffic erlauben
oif "lo" accept
# Bestehende/zugehörige Verbindungen erlauben
ct state established,related accept
# DNS nach außen erlauben (udp/tcp 53).
# (diese Policy ließe sich verschärfen, indem DNS nur zu bestimmten IPs erlaubt wird)
udp dport 53 accept
tcp dport 53 accept
# Lokale Docker-Netze erlauben (für Testcontainers, DBs usw.)
ip daddr 172.17.0.0/16 accept
ip daddr 172.18.0.0/16 accept
# Traffic zum Proxy erlauben
ip daddr <My Host Ip> tcp dport 8888 accept
}
}Ich habe die Regeln mit sudo nft -f /etc/nftables-proxy.egress.nft geladen und getestet, dass Internetzugriff außerhalb meiner definierten Allowlist nicht möglich ist (z. B. indem ich curl mit deaktivierten Proxy-Environment-Variablen ausführe). Außerdem habe ich mit sudo nft list ruleset geprüft, dass die Sandbox-Regeln geladen wurden.
Leider haben die Regeln einen Reboot nicht überlebt. Man kann die Regeln persistent machen, aber das hat bei mir Probleme verursacht: Docker fügt ebenfalls Regeln zu nftables hinzu, und die müssen vor meinen Sandbox-Regeln aktiv sein, damit meine Integrationstests (die Testcontainers verwenden) korrekt funktionieren. Als Workaround habe ich auf der Linux-VM einen kleinen Service erstellt, der nach Docker läuft und die Regeln ins System einfügt. Das ist meine /etc/systemd/system/nftables-proxy-egress.service:
[Unit]
Description=Apply nftables proxy egress rules
After=network-online.target docker.service
Wants=network-online.target
OnFailure=proxy-egress-console-alert.service
[Service]
Type=oneshot
ExecStartPre=/usr/sbin/nft -c -f /etc/nftables-proxy-egress.nft
ExecStart=/usr/sbin/nft -f /etc/nftables-proxy-egress.nft
[Install]
WantedBy=multi-user.targetDer Check nft -c sorgt dafür, dass das System im Fehlerfall sofort abbricht (fail fast). Die Direktive OnFailure ruft bei einem Fehler proxy-egress-console-alert.service auf. Damit kann ich mich benachrichtigen lassen, falls das Laden der Regeln fehlschlägt. Sonst würde ich es womöglich gar nicht merken, dass der Service nicht läuft und die Netzwerkregeln nicht geladen wurden – und würde das System nutzen, ohne die Sicherheit zu haben, dass die VM wirklich allen Traffic über den Host leitet.
Hier ist meine /etc/systemd/system/proxy-egress-console-alert.service, die bei Problemen einfach die Datei /var/lib/nft-egress-failed anlegt.
[Unit]
Description=Console alert if nftables proxy egress fails
[Service]
Type=oneshot
ExecStart=/usr/bin/touch /var/lib/nft-egress-failedDanach habe ich ein paar Zeilen in meine ~/.bashrc aufgenommen, die meinem Prompt eine blinkende 🚨🚨 NFT-EGRESS-FAILED 🚨🚨-Meldung hinzufügen, um mich zu warnen, dass das Laden der Egress-Regeln fehlgeschlagen ist:
if [ -f /var/lib/nft-egress-failed ]; then
PS1='\[\033[5;1;31m\]🚨🚨 NFT-EGRESS-FAILED 🚨🚨\[\033[0m\]\n\[\033[1;31m\]\u@\h:\w\$ \[\033[0m\]'
fiAls der Service stand, habe ich ihn neu geladen und neu gestartet:
sudo systemctl daemon-reload
sudo systemctl restart nftables-proxy-egress.serviceEin Kollege hat vorgeschlagen, bei einem Fehler gleich das komplette Netzwerk offline zu nehmen. Als ich das ausprobiert habe, habe ich es allerdings geschafft, die ganze VM zu „bricken“, weil Limas shell-Kommando via ssh über das Netzwerk mit der VM kommuniziert. Deshalb bleibe ich vorerst bei der blinkenden Prompt-Meldung. Dazu möchte ich auch anmerken: Dass ich mit nftables herumfummeln muss, um sämtlichen Traffic über einen Proxy auf dem Host zu routen, ist möglicherweise eine Einschränkung der VM-Technologie, die ich gewählt habe. Wenn du eine VM nutzt, die mit Vagrant provisioniert werden kann (z. B. Virtual Box), gibt es ein Plugin, mit dem sich das deklarativ definieren lässt.
Als letzten Quick-Check ist wichtig sicherzustellen, dass nft, die Konfiguration /etc/nftables-proxy-egress.nft sowie die Services nur von root veränderbar sind! Sonst könnte ein Agent theoretisch diese Regeln so abändern, dass er das Netzwerk wieder öffnet – mit deinen Rechten.
Traffic beobachten und Allowlist erweitern
Zum Schluss beobachte ich den Traffic aus der VM und erweitere die Allowlist so, dass die Requests durchgehen, die wir im Entwicklungsalltag brauchen. Die Logs von Squid kann man auf der Kommandozeile einsehen (bei mir unter /opt/homebrew/var/logs/access.log).
sudo tail -f /opt/homebrew/var/logs/access.logJeder Request, der verworfen wird, wird mit der Methode TCP_DENIED geloggt. Wenn du Requests zu dieser URL erlauben willst, kannst du deine allowed_domains.txt um die neue Domain ergänzen und Squid neu starten.
brew services restart squidIn der Praxis musste ich diese Liste bislang nur selten anpassen. Ich habe 12 URLs in meiner Allowlist, und das reicht offenbar aus. Ich habe zwar Package-Registries für Gradle und npm in der Liste – was theoretisch Supply-Chain-Angriffe ermöglicht –, aber das Risiko ist stark reduziert, und ich fühle mich endlich wohl damit, meine Guardrails etwas zu lockern und zu sehen, was meine Agents leisten.
Coding-Agents zum Programmieren, Chatbots zum Suchen
Ein Grund, warum die URL-Liste so kurz ist: Standardmäßig holen sich die Coding-Agents nur die Application-Dependencies (aus Maven- oder npm-Repositories) und kommunizieren mit den LLMs direkt über die APIs der jeweiligen Anbieter (z. B. OpenAI oder Anthropic). Von sich aus machen Coding-Agents keine Websuche, solange man das nicht explizit anfordert. Das heißt: Beim Programmieren beziehen sie ihre Informationen aus den Modellen – nicht von irgendwelchen beliebigen Websites im Internet. Das reduziert das Risiko von Prompt Injection.
Ein Proxy vor den Coding-Agents bedeutet für mich außerdem zusätzliche Reibung, wenn ich Websuche in den Agents aktivieren wollte. Dann müsste ich den Proxy vorübergehend deaktivieren oder anpassen und selbst wieder in der Schleife sein, um jeden Webrequest zu genehmigen, den der Agent ausführen will. Das will ich nicht, weil ich in der Sandbox oft länger laufende Tasks habe, bei denen der Proxy dauerhaft aktiv bleiben soll.
Praktisch heißt das: Für reine Programmieraufgaben verlasse ich mich ausschließlich auf die Modelle ohne Websuche. Und wenn ich Websuche will, um die aktuellsten Informationen zu finden, nutze ich Chatbots im Browser. Aus Sicht der lethal trifecta ist das ideal: Die langen Tasks ohne direkte Aufsicht haben nur Zugriff auf einen begrenzten Datenausschnitt und extrem eingeschränkten Netzwerkzugang. Die kurzen Recherche-Tasks mit Websuche haben zwar Internetzugang, finden aber im Browser statt – mit extrem begrenztem Zugriff auf Daten.
Mögliche nächste Schritte: Das Sahnehäubchen
Für mein aktuelles Threat Model bin ich mit der Lösung aus diesem Artikel zufrieden. Es gibt aber ein paar weitere Schritte, mit denen man die Sandbox noch stärker machen könnte. Einer davon wäre, Network-Analyse- und Threat-Detection-Software wie Suricata in die Sandbox-Lösung zu integrieren. Dann würde man bei auffälligem Verhalten direkt alarmiert und könnte leichter nachvollziehen, was schiefgelaufen ist. In eine ähnliche Richtung geht Digital-Forensics-Software wie Velociraptor: Damit ließen sich detaillierte Informationen sammeln, was innerhalb der Sandbox tatsächlich passiert. Das würde Einblicke in laufende Prozesse geben und helfen festzustellen, ob es bösartiges Verhalten gab. Außerdem möchte ich die DNS-Konfiguration in der nftables-Regel noch weiter verschärfen, um Datenexfiltration via DNS-Tunneling zu verhindern.
Den Coding-Agents mehr Freiraum geben
An diesem Punkt habe ich für jeden Teil der lethal trifecta Gegenmaßnahmen. Eine VM mit so wenigen Privilegien wie möglich stellt die Sandbox für meine Coding-Agents bereit und sorgt dafür, dass sie keine kritischen Daten erreichen können, die im Kompromittierungsfall leaken würden. Und dadurch, dass ich den gesamten Traffic durch einen Proxy auf dem Host mit sehr strikter Allowlist zwinge, begrenze ich sowohl die Exposition gegenüber nicht vertrauenswürdigen Inhalten als auch die Möglichkeit, nach außen zu kommunizieren, massiv.
Auf dieser Basis fühle ich mich endlich wohl dabei, die strikten Guardrails meiner Coding-Agents zu lockern. codex --yolo und claude --dangerously-skip-permissions zu nutzen, war richtig spannend, weil ich damit detaillierte, mehrstufige Aufgaben definieren kann, die meine Agents ausführen, ohne dass ich permanent in der Schleife bleiben und alles überwachen muss. Das verschafft mir wiederum mehr Zeit für andere Aufgaben, ohne mich mental zu überlasten, weil ich meinen Fokus nicht ständig wechseln muss. Ich möchte aber klar betonen: Dass ich diese Guardrails lockere, halte ich nur deshalb für vertretbar, weil ich meine Sandbox und Netzwerk-Policy so aufgebaut habe, dass sie sogar mehr Schutz bieten als die Agent-Tools allein.
Meine aktuelle Lösung ist noch sehr grundlegend, aber ich glaube, sie liefert ein solides Fundament, auf dem ich weitere Features aufbauen kann, ohne Sicherheit zu opfern.