Dev.to AI ๐Ÿค– Ai ๐Ÿ‘ 0 ๐Ÿ“– 7 min read

Three Sandbox Failures That Let AI Agents Escape

Your LLM agent just exfiltrated data through a container it shouldn't have been able to reach. The incident report calls it "emergent behavior." The logs show a missing seccomp profile, an open egress rule, and a bind mo

Your LLM agent just exfiltrated data through a container it shouldn't have been able to reach. The incident report calls it "emergent behavior." The logs show a missing seccomp profile, an open egress rule, and a bind mount with propagation enabled.

This article covers three containment failures that look like AI gone rogue, the specific configuration that closes each one, and the monitoring that catches the next escape before it becomes a headline.

The Syscall Surface You Forgot to Filter

Most teams run agents in containers with the default Docker seccomp profile. That profile blocks about 44 syscalls โ€” but it allows ptrace, process_vm_readv, process_vm_writev, and bpf. An agent with code execution can use those to inspect or manipulate sibling containers, read host memory, or load malicious eBPF programs.


## Dockerfile.agent

FROM python:3.11-slim

## Install your agent dependencies

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY agent.py .
USER 1000:1000

## Drop all capabilities, then add only what you need

## CAP_DAC_OVERRIDE is often needed for file access in mounted volumes

## Remove it if your agent doesn't need it

ENTRYPOINT ["python", "agent.py"]
// seccomp-agent.json
{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "close", "stat", "fstat", "lstat",
        "poll", "lseek", "mmap", "mprotect", "munmap", "brk",
        "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "ioctl",
        "pread64", "pwrite64", "readv", "writev", "access",
        "pipe", "select", "sched_yield", "mremap", "msync",
        "mincore", "madvise", "shmget", "shmat", "shmctl",
        "dup", "dup2", "pause", "nanosleep", "getitimer",
        "alarm", "setitimer", "getpid", "sendfile", "socket",
        "connect", "accept", "sendto", "recvfrom", "sendmsg",
        "recvmsg", "shutdown", "bind", "listen", "getsockname",
        "getpeername", "socketpair", "setsockopt", "getsockopt",
        "clone", "fork", "vfork", "execve", "exit", "wait4",
        "kill", "uname", "semget", "semop", "semctl",
        "shmdt", "msgget", "msgsnd", "msgrcv", "msgctl",
        "fcntl", "flock", "fsync", "fdatasync", "truncate",
        "ftruncate", "getdents", "getcwd", "chdir", "fchdir",
        "rename", "mkdir", "rmdir", "creat", "link", "unlink",
        "symlink", "readlink", "chmod", "fchmod", "chown",
        "fchown", "lchown", "umask", "gettimeofday", "getrlimit",
        "getrusage", "sysinfo", "times", "ptrace", "getuid",
        "syslog", "getgid", "setuid", "setgid", "geteuid",
        "getegid", "setpgid", "getppid", "getpgrp", "setsid",
        "setreuid", "setregid", "getgroups", "setgroups",
        "setresuid", "getresuid", "setresgid", "getresgid",
        "getpgid", "setfsuid", "setfsgid", "getsid", "capget",
        "capset", "rt_sigpending", "rt_sigtimedwait", "rt_sigqueueinfo",
        "rt_sigsuspend", "sigaltstack", "utime", "mknod", "uselib",
        "personality", "ustat", "statfs", "fstatfs", "sysfs",
        "getpriority", "setpriority", "sched_setparam", "sched_getparam",
        "sched_setscheduler", "sched_getscheduler", "sched_get_priority_max",
        "sched_get_priority_min", "sched_rr_get_interval", "mlock",
        "munlock", "mlockall", "munlockall", "vhangup", "modify_ldt",
        "pivot_root", "_sysctl", "prctl", "arch_prctl", "adjtimex",
        "setrlimit", "chroot", "sync", "acct", "settimeofday",
        "mount", "umount2", "swapon", "swapoff", "reboot", "sethostname",
        "setdomainname", "iopl", "ioperm", "create_module", "init_module",
        "delete_module", "get_kernel_syms", "query_module", "quotactl",
        "nfsservctl", "getpmsg", "putpmsg", "afs_syscall", "tuxcall",
        "security", "gettid", "readahead", "setxattr", "lsetxattr",
        "fsetxattr", "getxattr", "lgetxattr", "fgetxattr", "listxattr",
        "llistxattr", "flistxattr", "removexattr", "lremovexattr",
        "fremovexattr", "tkill", "time", "futex", "sched_setaffinity",
        "sched_getaffinity", "set_thread_area", "io_setup", "io_destroy",
        "io_getevents", "io_submit", "io_cancel", "get_thread_area",
        "lookup_dcookie", "epoll_create", "epoll_ctl_old", "epoll_wait_old",
        "remap_file_pages", "getdents64", "set_tid_address", "restart_syscall",
        "semtimedop", "fadvise64", "timer_create", "timer_settime",
        "timer_gettime", "timer_getoverrun", "timer_delete", "clock_settime",
        "clock_gettime", "clock_getres", "clock_nanosleep", "exit_group",
        "epoll_wait", "epoll_ctl", "tgkill", "utimes", "vserver",
        "mbind", "set_mempolicy", "get_mempolicy", "mq_open", "mq_unlink",
        "mq_timedsend", "mq_timedreceive", "mq_notify", "mq_getsetattr",
        "kexec_load", "waitid", "add_key", "request_key", "keyctl",
        "ioprio_set", "ioprio_get", "inotify_init", "inotify_add_watch",
        "inotify_rm_watch", "migrate_pages", "openat", "mkdirat",
        "mknodat", "fchownat", "futimesat", "newfstatat", "unlinkat",
        "renameat", "linkat", "symlinkat", "readlinkat", "fchmodat",
        "faccessat", "pselect6", "ppoll", "unshare", "set_robust_list",
        "get_robust_list", "splice", "tee", "sync_file_range", "vmsplice",
        "move_pages", "utimensat", "epoll_pwait", "signalfd", "timerfd_create",
        "eventfd", "fallocate", "timerfd_settime", "timerfd_gettime",
        "accept4", "signalfd4", "eventfd2", "epoll_create1", "dup3",
        "pipe2", "inotify_init1", "preadv", "pwritev", "rt_tgsigqueueinfo",
        "perf_event_open", "recvmmsg", "fanotify_init", "fanotify_mark",
        "prlimit64", "name_to_handle_at", "open_by_handle_at", "clock_adjtime",
        "syncfs", "sendmmsg", "setns", "getcpu", "process_vm_readv",
        "process_vm_writev", "kcmp", "finit_module", "sched_setattr",
        "sched_getattr", "renameat2", "seccomp", "getrandom", "memfd_create",
        "kexec_file_load", "bpf", "execveat", "userfaultfd", "membarrier",
        "mlock2", "copy_file_range", "preadv2", "pwritev2", "pkey_mprotect",
        "pkey_alloc", "pkey_free", "statx", "io_pgetevents", "rseq",
        "pidfd_send_signal", "io_uring_setup", "io_uring_enter",
        "io_uring_register", "openat2", "pidfd_open", "clone3",
        "close_range", "openat2", "pidfd_getfd", "faccessat2",
        "process_madvise", "epoll_pwait2", "mount_setattr", "quotactl_fd",
        "landlock_create_ruleset", "landlock_add_rule", "landlock_restrict_self",
        "memfd_secret", "process_mrelease", "futex_waitv", "set_mempolicy_home_node"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Run with --security-opt seccomp=seccomp-agent.json. The profile above allows only the syscalls a typical Python agent needs. It explicitly denies ptrace, process_vm_readv, process_vm_writev, and bpf. If your agent needs additional syscalls โ€” for example, clone3 for threading โ€” add them deliberately. Every allowance is a decision you can audit.

The Egress Path You Left Open

A container with no egress rules can reach the internet, your internal metadata service, and every other workload in the cluster. An agent that gains code execution doesn't need a zero-day โ€” it just curls http://169.254.169.254/latest/meta-data/ or scans the pod CIDR.


## kubernetes/networkpolicy-agent-egress.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: agent-egress-deny-all
  namespace: agents
spec:
  podSelector:
    matchLabels:
      app: llm-agent
  policyTypes:
    - Egress
  egress:
    - to:
      - namespaceSelector:
          matchLabels:
            name: model-inference
      ports:
      - protocol: TCP
        port: 8000
    - to:
      - namespaceSelector:
          matchLabels:
            name: vector-db
      ports:
      - protocol: TCP
        port: 6333
    # Explicit DNS if your cluster uses CoreDNS
    - to:
      - namespaceSelector:
          matchLabels:
            k8s-app: kube-dns
      ports:
      - protocol: UDP
        port: 53
      - protocol: TCP
        port: 53

This policy allows egress only to the model inference service and the vector database, both identified by namespace labels. DNS is permitted to the cluster resolver. Everything else โ€” metadata endpoints, other namespaces, the internet โ€” is dropped. Apply it before you deploy the agent. Test with kubectl exec -it <agent-pod> -- curl -s http://169.254.169.254 and verify it hangs or returns connection refused.

The Filesystem Escape Via Mount Propagation

You mounted a host directory into the agent container so it can read uploaded files. You used propagation: Bidirectional or HostToContainer because a tutorial said it makes "file watching work." Now the agent can create a mount namespace, bind-mount /host/etc into it, and read /etc/shadow on the host.


## kubernetes/pod-agent.yaml

apiVersion: v1
kind: Pod
metadata:
  name: llm-agent
  namespace: agents
  labels:
    app: llm-agent
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: Localhost
      localhostProfile: seccomp-agent.json
  containers:
  - name: agent
    image: your-registry/agent:latest
    volumeMounts:
    - name: uploads
      mountPath: /data/uploads
      readOnly: true
      # Critical: no mountPropagation field means private (default)
      # Do NOT set mountPropagation: Bidirectional or HostToContainer
  volumes:
  - name: uploads
    persistentVolumeClaim:
      claimName: agent-uploads-pvc

The key line is the absence of mountPropagation. When omitted, the mount is private โ€” changes in the container don't propagate to the host, and host mount events don't propagate into the container. If you need the agent to react to new files, use a sidecar that watches the volume and writes events to a queue the agent consumes. Never share mount propagation with the host.

Monitoring That Catches the Escape

Containment fails. Your monitoring must assume it will. Three signals catch an escape in progress:

  1. Syscall anomalies โ€” Falco or Tetragon rules that alert on ptrace, process_vm_readv, bpf, or unshare from the agent container.
  2. Egress violations โ€” Cilium or Calico flow logs showing denied connections from the agent pod to unexpected CIDRs.
  3. Filesystem anomalies โ€” Auditd rules on the host watching for openat on sensitive paths (/etc/shadow, /proc/*/mem, /sys/kernel) from the agent's container PID namespace.

## falco/rules-agent-escape.yaml

- rule: Agent Ptrace Attempt
  desc: Agent container attempting to ptrace another process
  condition: >
    container.image.repository=your-registry/agent and
    evt.type=ptrace and
    evt.dir=>
  output: >
    Agent ptrace attempt (user=%user.name container=%container.name
    pid=%proc.pid target=%proc.pname)
  priority: CRITICAL
  tags: [container, mitre_execution]

- rule: Agent BPF Load
  desc: Agent container loading eBPF program
  condition: >
    container.image.repository=your-registry/agent and
    evt.type=bpf and
    evt.dir=>
  output: >
    Agent BPF load (user=%user.name container=%container.name
    cmd=%proc.cmdline)
  priority: CRITICAL
  tags: [container, mitre_defense_evasion]

- rule: Agent Unshare Namespace
  desc: Agent container creating new namespaces
  condition: >
    container.image.repository=your-registry/agent and
    evt.type=unshare and
    evt.dir=>
  output: >
    Agent unshare (user=%user.name container=%container.name
    flags=%evt.arg.flags)
  priority: WARNING
  tags: [container, mitre_privilege_escalation]

Deploy these rules to your Falco or Tetragon instance. Tune the container.image.repository match to your actual image. The goal is not to prevent the syscall โ€” seccomp already does that โ€” but to alert when the agent tries. A spike in denied ptrace or unshare calls means something in the agent's logic or a prompt injection is probing the boundary.

Key Takeaways

  • Default container profiles allow syscalls (ptrace, bpf, process_vm_readv) that let an agent inspect or escape its sandbox. Write a minimal seccomp profile and enforce it.
  • No egress policy means the agent can reach metadata services and internal workloads. Default-deny egress with explicit allow-lists for only the services the agent needs.
  • Mount propagation shared with the host is a filesystem escape vector. Keep mounts private; use sidecars for file-watching patterns.
  • Monitoring must assume containment fails. Alert on denied syscalls, blocked egress, and host filesystem access from the agent's PID namespace.
  • Every allowance โ€” syscall, network destination, mount option โ€” is a decision you can audit. Document the reason in the config.

Source

Containment and monitoring failures are to blame for AI hacking reports: experts โ€” Added working seccomp profiles, Kubernetes NetworkPolicy and Pod specs, Falco detection rules, and the practitioner's checklist for each failure mode.

Support this work

These write-ups are researched and published with no paywall, sponsor, or tracking. If one saved you an afternoon, a small tip keeps them coming.

USDT, USDC or USDD ยท TRC-20 (Tron)

TFTNsfyomKrnUutRjBTGVULp19ByW29KbY
๐Ÿ“ฐ Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.