AI 코딩 에이전트를 무력화하지 않으면서 샌드박스(Sandbox)화 하는 방법
요약
AI 코딩 에이전트가 시스템 권한을 오용하여 데이터를 삭제하거나 자격 증명을 탈취하는 위험을 방지하기 위한 샌드박스 구축 방법을 다룹니다. 단순한 Docker 사용을 넘어 Root 권한 제거, 사용자 네임스페이스 설정, Seccomp 필터링을 통한 계층적 방어 전략을 제시합니다.
핵심 포인트
- AI 에이전트에게 과도한 Root 권한을 부여하는 것은 보안상 매우 위험함
- 단순 Docker 컨테이너 사용만으로는 커널 익스플로잇 및 컨테이너 탈출을 막기 부족함
- 비루트(Non-root) 사용자 실행 및 사용자 네임스페이스 재매핑 필수
- Seccomp를 활용한 시스템 호출(Syscall) 화이트리스트 관리 권장
문제점: 당신의 AI 에이전트가 Root 권한을 가지고 있습니다. 몇 달 전, 저는 한 팀이 셀프 호스팅(Self-hosted) AI 코딩 에이전트를 설정하는 것을 도와주고 있었습니다. 일반적인 설정이었습니다. 도구 접근 권한(Tool access)이 있는 LLM이 공유 개발 서버에서 실행되며, 파일을 읽고, 명령어를 실행하고, API를 호출할 수 있는 상태였습니다. 늘 있는 일이죠. 그러던 중 누군가가 신뢰할 수 없는 웹페이지에서 복사한 출력이 포함된 프롬프트(Prompt)를 실행했습니다. 에이전트는 내장된 지침을 충실히 해석했고, 건드릴 이유가 전혀 없는 디렉토리를 rm -rf로 삭제하기 시작했습니다. 다행히 중요한 데이터가 유실되지는 않았습니다. 하지만 유실될 수도 있었던 상황이었습니다. 이것이 코드를 실행하는 에이전트를 운영할 때 숨겨진 위험 요소입니다. 기본적으로 에이전트는 해당 프로세스가 가진 모든 권한으로 실행됩니다. 만약 그 프로세스가 당신의 개발 환경이라면, 에이전트는 당신의 SSH 키, 클라우드 자격 증명(Credentials), git 히스토리에 접근할 수 있습니다. 모든 것에 말이죠. 이제 이러한 것들을 실제로 어떻게 제대로 샌드박스(Sandbox)화 할 수 있는지 설명하겠습니다.
왜 "그냥 Docker를 사용하는 것"만으로는 부족한가
당연한 답은 에이전트를 컨테이너(Container)에 넣는 것입니다. 네, 그것도 시작점은 됩니다. 하지만 단순한 Docker 설정은 여전히 다음과 같은 문제를 가집니다:
- 기본적으로 컨테이너 내부의 Root 권한 (알려진 여러 경로를 통해 탈출 가능)
- 내부 서비스에 대한 전체 네트워크 접근 권한
- 충분히 고민하지 않은 바인드 마운트(Bind mounts)
- 시스템 호출(Syscall) 필터링 부재 — 커널 익스플로잇(Kernel exploits)이 존재함
편의를 위해docker.sock을 마운트한 "샌드박스" 설정을 본 적이 있습니다. 그것은 샌드박스가 아닙니다. 그것은 뜨거운 욕조(Hot tub)일 뿐입니다.
계층적 접근 방식 (The Layered Approach)
제가 이 문제를 생각하게 된 방식은 다음과 같습니다: 심층 방어(Defense in depth). 각 계층은 이전 계층이 우회되었다고 가정합니다.
계층 1: Root 권한 제거
컨테이너는 root로 실행되어서는 안 됩니다. 기본적이지만 끊임없이 간과되는 부분입니다.
FROM ubuntu:22.04
# 전용 사용자, sudo 없음, 셸 권한 상승 불가
RUN useradd -m -s /bin/bash agent
# 앱 설정 전에 전환하여 캐시/파일 소유권을 올바르게 설정
USER agent
WORKDIR /home/agent
COPY --chown=agent:agent ./app /home/agent/app
계층 2: 사용자 네임스페이스 (User namespaces)
컨테이너 내부에서 비루트(Non-root) 사용자라 할지라도, 호스트 상에서 컨테이너의 UID가 재매핑(Remapped)되기를 원할 것입니다.
따라서 에이전트가 어떤 방식으로든 컨테이너 내부에서 루트(root) 권한을 획득하더라도, 외부에서는 권한이 없는 UID(Unprivileged UID)일 뿐입니다. /etc/docker/daemon.json에서 다음과 같이 설정하세요: { "userns-remap" : "default" }. 데몬(daemon)을 재시작하면 컨테이너의 UID가 호스트의 높은 범위로 이동(shifted)됩니다. 내부의 "root" 프로세스는 호스트 파일 시스템에 대해 아무런 권한을 갖지 못하게 됩니다. 전체 설정 방법은 Docker 사용자 네임스페이스(user namespace) 문서를 참조하세요.
계층 3: Seccomp 필터링 (Seccomp filtering)
이것은 대부분의 사람들이 건너뛰는 계층입니다. seccomp를 사용하면 시스템 호출(syscalls)을 화이트리스트(whitelist)로 관리할 수 있습니다. 즉, 에이전트가 컨테이너를 장악하더라도 허용되지 않은 시스템 호출은 수행할 수 없습니다. Docker는 약 40개의 위험한 시스템 호출을 차단하는 기본 seccomp 프로필을 제공합니다. 에이전트 워크로드(workloads)를 위해 저는 이를 더욱 강화합니다:
{ "defaultAction" : "SCMP_ACT_ERRNO" , "syscalls" : [ { "names" : [ "read" , "write" , "open" , "openat" , "close" , "stat" , "fstat" , "lstat" , "mmap" , "brk" , "rt_sigaction" , "execve" , "exit" , "exit_group" , "futex" , "clone" , "fork" , "wait4" ], "action" : "SCMP_ACT_ALLOW" } ] }
다음과 같이 실행하세요:
docker run
--security-opt seccomp=./agent-seccomp.json
--security-opt no-new-privileges
--cap-drop=ALL
agent-image
--cap-drop=ALL은 모든 리눅스 기능(Linux capability)을 제거합니다. --no-new-privileges는 setuid 바이너리가 권한을 상승시키는 것을 차단합니다. 이들이 결합되어 컨테이너 내부의 공격 표면(attack surface)을 거의 제로에 가깝게 축소합니다.
계층 4: 네트워크 송신 제어 (Network egress control)
에이전트는 HTTP 호출을 수행해야 합니다. 하지만 귀하의 내부 네트워크를 스캔할 필요는 없습니다. 제가 발견한 가장 깔끔한 패턴은 목적지를 화이트리스트로 관리하는 프록시(proxy)를 통해 컨테이너를 라우팅하는 것입니다:
docker-compose.yml
services:
agent:
image: agent-image
# 에이전트가 프록시의 네트워크 네임스페이스(network namespace)를 공유합니다 — 직접적인 송신(egress)은 불가능합니다.
network_mode: "service:proxy"
proxy:
image: nginx:alpine
volumes:
- ./proxy.conf:/etc/nginx/nginx.conf:ro
networks:
- egress
networks:
egress:
driver: bridge
프록시는 에이전트가 정당하게 필요로 하는 엔드포인트(endpoints)만 허용합니다.
에이전트는 자체적인 네트워크 인터페이스를 갖지 않습니다. 모든 패킷은 호스트를 인식해야 하는 nginx를 거쳐야만 합니다.
Layer 5: 파일 시스템 격리 (Filesystem isolation)
마운트 지점(Mount points)은 제가 가장 많은 실수를 목격하는 부분입니다. 에이전트는 코드 위에서 작업해야 하지만, 원칙은 다음과 같습니다: 정확히 필요한 것만 마운트하고, 가능한 경우 읽기 전용(read-only)으로 설정하며, 민감한 정보는 절대 포함하지 마십시오.
docker run \
--read-only \ # 루트 파일 시스템(Root FS)은 불변(immutable) 상태로 유지
--tmpfs /tmp:size=100M \ # 용량이 제한된 임시 공간(Scratch space)
-v "$PROJECT_DIR:/workspace:rw" \ # 실제 작업 디렉토리
-v "$PROMPT_FILE:/input/prompt:ro" \ # 읽기 전용 입력값
agent-image
마운트되지 않은 항목들에 주목하십시오: ~/.ssh, ~/.aws, docker.sock, 그리고 우연히 .env 파일을 포함하고 있을지도 모를 상위 디렉토리들이 모두 제외되었습니다.
멀티 세션 워크로드 처리 (Handling Multi-Session Workloads)
여러 개발자가 에이전트 인프라를 공유하는 경우, 세션 간 격리(isolation) 자체가 문제가 됩니다. 해결책은 간단합니다. 세션당 하나의 컨테이너를 할당하고, 라이프사이클(lifecycle)을 세션에 종속시키는 것입니다.
import uuid
import subprocess
def start_agent_session(user_id: str, project_path: str) -> str:
session_id = str(uuid.uuid4())
container_name = f"agent-{user_id}-{session_id}"
subprocess.run([
"docker", "run", "-d",
"--name", container_name,
"--rm", # 중지 시 자동 정리
"--memory", "2g", # 하드 메모리 제한
"--cpus", "1.0", # CPU 할당량(quota)
"--pids-limit", "100", # 포크 폭탄(fork bombs) 방지
"-v", f"{project_path}:/workspace",
"agent-image",
], check=True)
return session_id
cgroup 제한(--memory, --cpus, --pids-limit)은 숨은 영웅들입니다. 이 제한이 없다면, 폭주하는 에이전트 하나가 호스트 전체를 다운시킬 수 있습니다. 저는 에이전트가 서브프로세스(subprocess)를 생성하는 루프에 빠졌을 때 이 사실을 뼈저리게 배웠습니다.
예방 팁 (Prevention Tips)
처음에는 명확하지 않았지만, 제가 배운 몇 가지 사항입니다:
- 에이전트의 환경을 신뢰할 수 없는(untrusted) 것으로 취급하십시오. 파일 시스템이나 환경 변수(env vars)에 있는 모든 것은 프롬프트 인젝션(prompt injection)을 통해 유출될 수 있습니다.
- 마운트 설정을 매번 감사(Audit)하십시오. 바인드 마운트(Bind mounts)는 제가 실제로 목격한 탈출(escapes) 사례 중 가장 빈번한 원인입니다.
에이전트가 실행하는 모든 명령어를 기록(Log)하십시오. 무언가 잘못되었을 때, 당신에게는 그 흔적이 필요할 것입니다. 모든 작업에 타임아웃(Timeout)을 설정하십시오. 30초 내에 끝나야 할 에이전트가 때로는 30시간 동안 실행되려고 시도할 수도 있습니다. 즉시 종료하십시오. 휘발성 컨테이너(Ephemeral containers)를 사용하십시오. 세션 전반에 걸쳐 동일한 컨테이너를 재사용하는 것은 상태 오염(State pollution)과 사용자 간의 자격 증명 유출(Credential leakage)을 초래합니다. 저에게 도움이 되었던 사고의 전환은 다음과 같습니다. 에이전트를 "내가 신뢰하는 인프라에서 실행되는, 내가 신뢰하는 코드"라고 생각하는 것을 멈추십시오. 대신, 당신이 터미널을 건네준 낯선 사람이라고 생각하십시오. 그리고 그에 맞춰 설계하십시오. 상위 계층(Layers)들이 에이전트를 무적(Invulnerable)으로 만들어주지는 않을 것입니다. 하지만 단 한 번의 잘못된 프롬프트(Prompt)가 대재앙이 아닌, 하나의 각주(Footnote) 정도로 끝나게 만들어 줄 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기