DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

Docker Sandboxes for AI Agents: The Right Way to Isolate Untrusted Code Execution

Docker Sandboxes for AI Agents: The Right Way to Isolate Untrusted Code Execution

In 2023, a researcher showed that a GPT-4 agent with access to a standard Docker terminal could mount the host filesystem in 4 conversation turns. The agent did not plan the attack. It followed ambiguous instructions, and Docker's default permissions did the rest.

Code execution is the most dangerous tool you can give an AI agent. Not because the model is malicious, but because models follow instructions with ambiguity. Docker defaults were built for development convenience, not adversarial containment.

Why Code Execution Is Different from Other Tools

Agents with read-only tools (web search, file reading) have limited attack surface. An agent with a terminal can install dependencies, create files, and modify system state. It can also make network calls to exfiltrate data and scan environment variables for secrets.

The prompt injection problem is specific here. Code fetched from an external source may carry embedded agent instructions. Those instructions redirect behavior before execution completes. The boundary between data and instruction collapses at the terminal.

At mago.team, we built OSINT pipelines where agents inspect URLs, extract scripts, and analyze content. Every step that touches potentially malicious external code requires a container that fails safely, not conveniently.

What Standard Docker Protects

A standard Docker container provides namespace isolation (PID, UTS, IPC, mount, network) and a separate filesystem via union mount. Resource limits via cgroups are possible, but not configured by default.

What it does not provide: syscall restrictions, mandatory access control (MAC) profiles, protection against privilege escalation via setuid, or real user isolation.

A process running as root inside a standard container has access to approximately 300 syscalls. ptrace, mount, clone with specific flags, keyctl: all available. The container isolates the filesystem and network, but not the kernel.

CVE-2019-5736 exploited this directly: a process inside the container overwrote the runc binary on the host during execution. No host root required, only root inside the container, which is the default.

The Isolation Stack That Actually Works

The correct approach is not a single flag. It is redundant layers where each one reduces attack surface independently.

seccomp: Filter Syscalls at the Kernel

Docker applies a default seccomp profile that blocks around 44 syscalls. That is not enough for untrusted code.

For AI agents, use an allowlist instead of a denylist:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "open", "openat", "close", "stat", "fstat",
                "mmap", "mprotect", "munmap", "brk", "exit_group", "futex",
                "getpid", "getuid", "getgid", "arch_prctl", "set_tid_address",
                "set_robust_list", "pread64", "pwrite64", "readv", "writev",
                "access", "pipe", "select", "dup", "dup2", "nanosleep",
                "fork", "execve", "wait4", "clone", "kill", "socket",
                "connect", "sendto", "recvfrom", "shutdown"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Any syscall not listed returns EPERM. mount, ptrace, keyctl, perf_event_open, bpf: blocked by default.

AppArmor: Mandatory Access Policy

AppArmor operates on top of seccomp, at the filesystem and capabilities level:

profile docker-agent flags=(attach_disconnected,mediate_deleted) {
  network inet tcp,
  network inet udp,
  deny /proc/sys/kernel/** w,
  deny /proc/sysrq-trigger rwklx,
  deny /sys/** w,
  deny /etc/passwd w,
  deny /etc/shadow rwklx,
  /tmp/** rw,
  /app/** r,
}
Enter fullscreen mode Exit fullscreen mode

Docker's default profile (docker-default) allows writes to /proc and access to capabilities that agent code should never have.

Read-Only Rootfs with Explicit tmpfs

docker run \
  --read-only \
  --tmpfs /tmp:size=100m,noexec,nosuid \
  --tmpfs /run:size=10m \
  my-agent-image
Enter fullscreen mode Exit fullscreen mode

--read-only alone is not enough: the agent can still write to /tmp by default. The noexec flag on tmpfs prevents execution of binaries loaded into memory. nosuid prevents escalation via setuid files in temporary storage.

no-new-privileges

docker run --security-opt no-new-privileges:true my-agent-image
Enter fullscreen mode Exit fullscreen mode

This flag blocks execve() from gaining additional capabilities via setuid or setcap bits. A setuid-root binary inside the container cannot escalate privileges with this flag active.

User Namespaces: The Most Underrated Layer

By default, root inside the container (UID 0) maps to root on the host (UID 0). CVE-2019-5736 depended on this.

With user namespaces enabled on the daemon:

{
  "userns-remap": "default"
}
Enter fullscreen mode Exit fullscreen mode

Root inside the container maps to an unprivileged UID on the host, typically 100000+. A process that escapes the container has no real privileges on the host.

The cost: some images that rely on real root break. For agent code containers, that is the right tradeoff.

Network Isolation

docker network create \
  --driver bridge \
  --internal \
  agent-sandbox-net
Enter fullscreen mode Exit fullscreen mode

--internal blocks routing outside the Docker network. The container communicates with other containers on the same network but has no route to the internet.

For agents that need selective external access, use an explicit proxy that filters destinations. Do not use --network=none if the agent needs to report results: that also blocks communication with the control service.

Escape Vectors That Still Work When You Get It Wrong

Docker Socket Mounted

# Never do this in agent containers:
docker run -v /var/run/docker.sock:/var/run/docker.sock agent
Enter fullscreen mode Exit fullscreen mode

A process with access to the Docker socket can create new containers without the restrictions above, mount the host filesystem, and effectively become root on the host. Common CI/CD pipelines do this; agent containers never should.

Capabilities Not Dropped

Docker inherits a default set of capabilities even in non-privileged containers: NET_RAW, SYS_CHROOT, AUDIT_WRITE, among others. For code agents:

docker run \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  agent-image
Enter fullscreen mode Exit fullscreen mode

NET_RAW allows building arbitrary packets and ARP spoofing inside the Docker network. If the agent does not need raw sockets, drop the capability.

Base Images with Attack Tools

An image based on ubuntu:latest includes curl, wget, nc, python3. Each one is a vector for exfiltration or payload download. Use distroless or minimal alpine images, and include only what the agent genuinely needs.

Missing Resource Limits

Without configured cgroups, an agent can consume unlimited CPU and memory, creating a DoS on the host. Always configure:

docker run \
  --memory=512m \
  --memory-swap=512m \
  --cpus=1.0 \
  --pids-limit=100 \
  agent-image
Enter fullscreen mode Exit fullscreen mode

--pids-limit is frequently forgotten: without it, a fork bomb inside the container affects the host system.

Reference Configuration

This is the minimum acceptable configuration for agent code execution in production:

# docker-compose.yml: agent sandbox
services:
  code-sandbox:
    image: gcr.io/distroless/python3-debian12:latest
    read_only: true
    user: "65534:65534"
    security_opt:
      - no-new-privileges:true
      - seccomp:./seccomp-agent.json
      - apparmor:docker-agent
    cap_drop:
      - ALL
    tmpfs:
      - /tmp:size=100m,noexec,nosuid,nodev
    networks:
      - agent-internal
    mem_limit: 512m
    memswap_limit: 512m
    cpus: 1.0
    pids_limit: 100
    environment:
      - PYTHONDONTWRITEBYTECODE=1

networks:
  agent-internal:
    driver: bridge
    internal: true
Enter fullscreen mode Exit fullscreen mode

This configuration does not guarantee absolute security: it guarantees that any escape requires exploiting a kernel vulnerability, not a misconfiguration. Kernel exploits are rare and patchable; misconfigurations are permanent until someone notices.

How MAGO Handles This

At mago.team, OSINT analysis agents receive URLs from untrusted sources and inspect scripts, pages, and files. Every execution operation runs in an ephemeral container with the configuration above, destroyed after completion.

The agent has no access to the Docker socket: it submits code to the harness, which controls the container lifecycle. The result returns via API; the container never accesses the system's internal state.

This separation between "what the agent sees" and "what the container does" is the correct pattern. The agent is a client of the sandbox, not its operator.

The Principle That Drives Everything

AI agent containers must be configured as if the code inside them is hostile by design. Not because the model will act in bad faith, but because it will execute what it receives. And what it receives may come from sources controlled by attackers.

Docker's default configuration was built to isolate development processes from each other. The threat model changes when the process inside the container can be influenced by arbitrary external content. Treat it as hostile by default, and adjust upward only when necessary.

Top comments (0)