AI Sparkup

최신 AI 쉽게 깊게 따라잡기⚡

AI 에이전트가 코드를 실행할 때: 컨테이너만으론 부족한 이유

AI 에이전트가 작업을 하다 보면 결국 같은 요청을 하게 됩니다. “코드 좀 실행시켜줘.” 때로는 무해한 pytest일 수도 있고, 때로는 pip install sketchy-package && python run.py일 수도 있습니다. 어느 쪽이든, AI가 코드를 실행하는 순간 당신이 신경 쓰는 머신에서 신뢰할 수 없는 바이트가 돌아가기 시작하는 겁니다.

사진 출처: Christian Weiss via Unsplash

보안 엔지니어 Luis Cardoso가 AI 샌드박스에 대한 포괄적인 가이드를 발표했습니다. 이 글은 컨테이너, microVM, gVisor, WebAssembly 등 다양한 격리 기술의 차이점과 선택 기준을 상세히 다룹니다. 특히 “Docker 쓰면 되지”라는 흔한 오해를 깨고, AI 코드 실행이라는 특수한 상황에서 실제로 필요한 격리 수준을 어떻게 결정해야 하는지 명확한 프레임워크를 제시합니다.

출처: A field guide to sandboxes for AI – Luis Cardoso

샌드박스를 평가하는 3가지 질문

Luis는 샌드박스를 선택할 때 사람들이 자주 혼동하는 세 가지 개념을 명확히 구분합니다. 이 세 가지를 분리해서 생각하면 훨씬 명확한 결정을 내릴 수 있습니다.

경계(Boundary): 격리가 어디서 강제되는가? 컨테이너는 프로세스를 분리하지만 여전히 하나의 호스트 커널을 공유합니다. gVisor는 syscall을 가로채서 userspace 커널에서 처리하고요. MicroVM은 게스트 커널을 하드웨어 가상화 뒤에 두고, Wasm은 아예 syscall ABI 자체를 제공하지 않습니다. 경계는 공격자가 넘을 수 없다고 당신이 믿는 그 선입니다.

정책(Policy): 경계 안에서 코드가 무엇을 건드릴 수 있는가? 파일시스템 경로, 네트워크 목적지, 프로세스 생성, GPU 접근, CPU/메모리 할당량 등입니다. 약한 경계에서 엄격한 정책을 적용해도 여전히 약한 샌드박스입니다. 강한 경계에 느슨한 정책을 두면 기회를 낭비하는 거고요.

생명주기(Lifecycle): 실행 사이에 무엇이 남는가? Fresh run은 아무것도 남기지 않습니다. 악의적 코드에는 좋지만 “에이전트 작업 공간” UX에는 나쁘죠. 반대로 long-lived workspace는 에이전트에겐 좋지만 비밀이 유출되거나 persistence가 악용될 위험이 있습니다. Snapshot/restore는 VM이나 런타임 상태를 체크포인트해서 빠르게 리셋할 수 있어서 RL 환경이나 “미리 준비된” 에이전트에 유용합니다.

샌드박스를 평가할 때 이 세 가지 질문을 던져보세요. 답할 수 있다면 당신의 샌드박스를 이해하는 겁니다. 답할 수 없다면 추측하고 있는 거고요.

컨테이너의 근본적 한계

많은 사람들이 컨테이너를 보안 경계로 생각하지만, Luis는 명확히 짚어냅니다. “컨테이너는 적대적 코드를 위한 충분한 보안 경계가 아닙니다.”

컨테이너는 네임스페이스, capabilities, cgroups, seccomp 같은 커널 기능들의 조합입니다. 이것들은 프로세스가 볼 수 있는 것(네임스페이스)을 제한하고, 소비할 수 있는 것(cgroups)을 제한하고, 호출할 수 있는 syscall(seccomp)을 제한합니다. 하지만 근본적으로 여전히 같은 호스트 커널입니다.

문제는 여기 있습니다. 프로세스는 무언가 실제 작업을 해야 할 때마다 syscall을 만듭니다. 파일 읽기, 소켓 생성, 메모리 할당, 프로세스 생성. 그 syscall은 커널에 진입합니다. 만약 허용된 syscall 경로, 파일시스템 코드, 네트워킹 코드, ioctl 핸들러 중 어딘가에 버그가 있다면? 로컬 권한 상승이 가능해집니다.

실제로 컨테이너 탈출의 많은 경우는 설정 실수입니다. --privileged 플래그는 대부분의 가드레일을 제거합니다. Docker 소켓(/var/run/docker.sock)을 마운트하면 호스트 Docker 데몬에게 새로운 privileged 컨테이너를 만들어달라고 요청할 수 있죠. 실질적으로 Docker 소켓 접근은 호스트 root 접근입니다.

하지만 제대로 설정된 컨테이너도 여전히 호스트 커널을 공유합니다. Dirty COW, Dirty Pipe, 파일시스템 컨텍스트 오버플로우 같은 커널 버그들이 컨테이너 환경에서 악용되어 왔습니다. Seccomp가 노출을 줄이긴 하지만, 허용하는 syscall들은 여전히 커널 코드입니다.

그리고 AI 시스템에서 특히 중요한 실패 모드가 하나 더 있습니다. 정책 누수(policy leakage)입니다. 많은 “에이전트 샌드박스” 실패는 커널 탈출이 아닙니다. 정책 실패입니다. 샌드박스가 레포지토리를 읽을 수 있고 외부 네트워크 접근이 있다면, 에이전트는 레포를 유출할 수 있습니다. ~/.aws를 읽거나 호스트 볼륨을 마운트할 수 있다면 자격증명을 유출할 수 있고요. 내부 서비스에 도달할 수 있다면 측면 이동 도구가 될 수 있습니다.

더 강한 경계들: gVisor, MicroVM, Wasm

그렇다면 대안은 무엇일까요? Luis는 세 가지 주요 접근법을 소개합니다.

gVisor는 syscall 가로채기(interposition) 방식입니다. 컨테이너의 syscall이 호스트 커널로 바로 가는 대신, Sentry라는 userspace 커널이 먼저 처리합니다. Sentry 자체는 훨씬 작은 호스트 syscall allowlist로 제한됩니다. 한 설정에서는 네트워킹 없이 53개, 네트워킹 포함해서 총 68개의 호스트 syscall만 사용합니다. 컨테이너 워크로드가 만들 수 있는 “모든 syscall”과는 완전히 다른 인터페이스죠.

트레이드오프는 예상 가능합니다. 모든 syscall과 커널 동작이 완전히 동일하지는 않습니다. Syscall이 많은 워크로드는 오버헤드를 더 많이 냅니다. 파일시스템이 많은 워크로드는 중재(mediation)와 추가 복사/IPC 비용을 냅니다. gVisor는 “호환성 매트릭스가 있는 리눅스”를 감내할 수 있고, 표준 컨테이너보다 훨씬 작은 호스트 커널 인터페이스를 원할 때 적합합니다.

MicroVM은 하드웨어 격리를 사용합니다. 하드웨어 가상화(Linux에서는 KVM) 뒤에 게스트 커널을 실행합니다. 호스트 커널은 개별 워크로드 syscall 대신 VM exit과 virtio 장치 I/O를 봅니다. 이것이 “낯선 사람을 위해 임의의 리눅스 코드 실행”의 기본 답인 이유입니다. Syscall ABI를 재구현하지 않고도 완전한 리눅스 의미론을 얻을 수 있으니까요.

Luis는 Firecracker를 자세히 설명합니다. AWS의 미니멀리스트 VMM으로, 멀티 테넌트 서버리스(Lambda, Fargate)를 위해 특별히 제작되었습니다. 아키텍처는 의도적으로 단순합니다. microVM당 하나의 Firecracker 프로세스, 최소한의 virtio 장치 모델(net, block, vsock, console), 그리고 게스트가 실행되기 전에 VMM 프로세스를 위한 격리를 설정하는 “jailer”가 있습니다.

보안 스토리는 VMM을 중심으로 한 심층 방어입니다. Jailer가 chroot + 네임스페이스 + cgroups를 설정하고, 권한을 내리고, VMM을 실행합니다. 엄격한 seccomp 프로필이 VMM을 제한합니다. Firecracker 논문에는 24개 syscall(인자 필터링 포함)과 30개 ioctl의 allowlist가 설명되어 있습니다.

WebAssembly는 완전히 다른 모델입니다. 경계가 런타임 안에 있습니다. 샌드박스된 코드는 호스트의 syscall ABI를 절대 얻지 못합니다. 런타임(과 embedder)이 명시적으로 제공하는 것만 얻을 뿐입니다. Wasm 모듈은 호스트가 imports를 노출하지 않는 한 외부 세계를 건드릴 수 없습니다.

WASI(WebAssembly System Interface)는 capability 지향 API로 이를 확장합니다. 핵심 기능은 preopened directories입니다. 모듈이 임의의 경로를 열도록 하는 대신, “이 하위 트리”를 나타내는 디렉토리 핸들을 주고, 그 안에서만 상대 경로를 해석할 수 있게 합니다. ~/.ssh를 preopen하지 않으면 모듈은 읽을 수 없습니다. 경로 접두사를 확인하는 것을 잊어버리는 “앗” 하는 버그가 없습니다. 왜냐하면 그 capability 자체가 부여되지 않았으니까요.

실전 선택 가이드

Luis는 실용적인 의사결정 표를 제시합니다.

멀티 테넌트 AI 코딩 에이전트(SaaS)라면? 적대적(사용자 제출 코드), 완전한 리눅스/셸/패키지 매니저 필요 → microVM(Firecracker/cloud-hypervisor) 추천.

싱글 테넌트 AI 코딩 에이전트(자체 호스팅)라면? 반신뢰(semi-trusted), 완전한 리눅스 필요 → 강화된 컨테이너 또는 gVisor.

RL 롤아웃(병렬 실행, 많은 리셋)이라면? 대부분 신뢰할 수 있는 코드, 빠른 리셋/스냅샷 필요 → 스냅샷 지원하는 microVM.

코드 인터프리터(상태 없는 스니펫)라면? 적대적, 제한된 capability/셸 없음 → gVisor 또는 런타임 샌드박스(언어가 맞으면).

툴 호출/플러그인이라면? 혼합, 명시적 capability 표면 → Wasm/isolates.

경계를 선택하기 전에 Luis는 최소 실행 가능한 정책(minimum viable policy)을 먼저 작성하라고 조언합니다. 이것들을 강제할 수 없다면 아직 샌드박스가 없는 겁니다.

  • 외부 네트워크는 기본 거부, 그 다음 allowlist (또는 정책 프록시 경유)
  • 샌드박스 안에 장기 자격증명 없음. 짧은 수명의 scoped token 사용
  • 작업공간 전용 파일시스템 접근. 명시적으로 의도한 것 외에 호스트 마운트 금지
  • 리소스 제한: CPU, 메모리, 디스크, 타임아웃, PID
  • 관찰 가능성: 프로세스 트리, 네트워크 egress, 실패 로깅

빠른 경험 법칙을 하나 제시하자면: 셸 + 패키지 매니저가 필요하고 코드를 완전히 신뢰하지 않는다면 → microVM에서 시작하세요. 호환성 매트릭스 안에서 살 수 있어서 오버헤드를 절약하려면 → gVisor 고려. 작업을 capability 범위 작업으로 모델링할 수 있다면 → Wasm/isolate 선호.

그 다음 측정으로 검증하세요. Cold start, 정상 상태 처리량, 운영 복잡성. MicroVM은 규모에서 저렴할 수 있지만, 오케스트레이션이 그에 맞게 구축된 경우에만 그렇습니다.

중요한 것은 승자를 가리는 게 아닙니다. 탈출이 일어나려면 무엇이 실패해야 하는지 아는 것, 그리고 그 현실에 맞는 경계를 선택하는 겁니다.

참고자료:


AI Sparkup 구독하기

최신 게시물 요약과 더 심층적인 정보를 이메일로 받아 보세요! (무료)

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다