콘텐츠로 이동

샌드박싱과 블래스트 반경 제한

에이전트가 코드를 실행하거나 시스템을 수정하거나 외부 API를 호출할 때, 오작동이나 보안 침해가 발생하면 그 피해가 어디까지 퍼지는가? 이 피해 범위를 블래스트 반경(blast radius) 이라 한다. 핵 개념에서 빌려온 이 용어는 폭발 반경처럼 피해가 중심에서 동심원으로 퍼진다는 직관을 담고 있다.

루프 엔지니어링에서 블래스트 반경 제한은 “실패를 막는 것”이 아니라 “실패가 발생했을 때 피해를 최소화하는 것”이다. 아무리 잘 설계된 에이전트도 예외 없이 실수한다. 중요한 것은 그 실수가 루프 내부에 머물 것인가, 아니면 프로덕션 데이터베이스나 외부 서비스까지 오염시킬 것인가다.

블래스트 반경 제한의 목표
┌──────────────────────────────────────────────────────────┐
│ 외부 세계 (프로덕션 DB, 외부 API, 파일시스템) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 샌드박스 경계 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 에이전트 루프 │ │ │
│ │ │ ┌──────────────────────────────────────────┐│ │ │
│ │ │ │ 도구 실행 (코드, 쉘, 파일 읽기/쓰기) ││ │ │
│ │ │ └──────────────────────────────────────────┘│ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
오류 발생 시 피해가 가장 안쪽 레이어에 갇히도록 설계

블래스트 반경을 제한하는 강력한 수단은 컨테이너 격리다. 에이전트가 실행하는 코드를 호스트와 분리된 환경에서 돌린다. 다만 실제 격리 수준은 제품, 실행 환경, 권한 설정에 따라 다르므로 특정 에이전트가 언제나 컨테이너 격리를 제공한다고 가정하지 말고 현재 문서를 확인해야 한다.

컨테이너 격리가 제공하는 보호:

  • 파일시스템 격리: 컨테이너 내부 파일 수정이 호스트에 영향 없음
  • 네트워크 격리: 인터넷 접근을 허용 목록(allowlist)으로 제한
  • 프로세스 격리: 컨테이너 내 프로세스가 호스트 프로세스에 접근 불가
  • 리소스 제한: CPU, 메모리, 디스크 사용량 캡 설정 가능
# 개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다
import subprocess
def run_in_sandbox(code: str, timeout_seconds: int = 30) -> dict:
"""
코드를 격리된 컨테이너에서 실행.
타임아웃 초과 시 컨테이너 강제 종료.
"""
result = subprocess.run(
[
"docker", "run",
"--rm", # 실행 후 컨테이너 삭제
"--network", "none", # 네트워크 차단
"--memory", "512m", # 메모리 512MB 제한
"--cpus", "1.0", # CPU 1코어 제한
"--read-only", # 루트 파일시스템 읽기 전용
"--tmpfs", "/tmp:size=100m", # /tmp만 쓰기 가능
"python:3.12-slim",
"python", "-c", code
],
capture_output=True,
text=True,
timeout=timeout_seconds # 프로세스 수준 타임아웃
)
return {
"stdout": result.stdout,
"stderr": result.stderr,
"returncode": result.returncode,
"timed_out": False
}

샌드박싱과 함께 반드시 설계해야 하는 것이 계층적 타임아웃이다. 타임아웃이 없으면 단 하나의 도구 호출이 루프 전체를 무한정 블로킹할 수 있다.

3단 타임아웃 계층
┌────────────────────────────────────────────────────────┐
│ 레벨 3: 샌드박스 전체 타임아웃 (예: 10분) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 레벨 2: 루프 전체 타임아웃 (예: 5분) │ │
│ │ ┌────────────────────────────────────────────┐ │ │
│ │ │ 레벨 1: 개별 도구 타임아웃 (예: 30초) │ │ │
│ │ └────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
안쪽부터 바깥쪽으로 순서대로 발동.
레벨 1이 발동하면 도구만 종료.
레벨 3이 발동하면 샌드박스 전체 강제 종료.
# 개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다
import asyncio
TOOL_TIMEOUT = 30 # 개별 도구 30초
LOOP_TIMEOUT = 300 # 루프 전체 5분
SANDBOX_TIMEOUT = 600 # 샌드박스 10분
async def run_tool_with_timeout(tool_fn, *args, **kwargs):
"""레벨 1: 개별 도구 타임아웃."""
try:
return await asyncio.wait_for(
tool_fn(*args, **kwargs),
timeout=TOOL_TIMEOUT
)
except asyncio.TimeoutError:
return {"error": f"도구가 {TOOL_TIMEOUT}초 내에 완료되지 않았습니다"}
async def run_agent_loop(task):
"""레벨 2: 루프 전체 타임아웃."""
try:
return await asyncio.wait_for(
_inner_loop(task),
timeout=LOOP_TIMEOUT
)
except asyncio.TimeoutError:
return {"error": f"루프가 {LOOP_TIMEOUT}초 내에 완료되지 않았습니다"}

각 타임아웃은 독립적으로 동작한다. 도구 타임아웃이 발동하면 해당 도구만 종료되고 루프는 계속된다. 루프 타임아웃이 발동하면 진행 중인 루프가 종료되고 체크포인트가 저장된다. 샌드박스 타임아웃은 최후의 보루로, 무엇이든 강제로 종료한다.

블래스트 반경을 줄이는 또 다른 핵심 원칙은 최소 권한(principle of least privilege) 이다. 에이전트에게 작업 수행에 필요한 최소한의 권한만 부여한다.

┌──────────────────────────────────────────────────────────────┐
│ 도구 권한 설계 원칙 │
├──────────────────────────────────────────────────────────────┤
│ ❌ 과도한 권한 에이전트가 전체 파일시스템 읽기/쓰기 가능 │
│ ✓ 최소 권한 에이전트가 작업 디렉터리만 읽기/쓰기 가능 │
│ │
│ ❌ 과도한 권한 데이터베이스 전체 읽기/쓰기 권한 │
│ ✓ 최소 권한 특정 테이블의 읽기 권한만 │
│ │
│ ❌ 과도한 권한 모든 외부 API 호출 가능 │
│ ✓ 최소 권한 허용 목록의 API 엔드포인트만 │
└──────────────────────────────────────────────────────────────┘

Anthropic의 도구 설계 가이드는 에이전트에게 “필요할 수도 있는” 권한을 미리 부여하지 말라고 강조한다. 필요한 순간에 필요한 권한을 동적으로 부여하는 방식이 블래스트 반경을 크게 줄인다.

샌드박싱 철학의 마지막 요소는 가역성(reversibility) 우선이다. 에이전트가 수행하는 모든 행동을 되돌릴 수 있도록 설계한다. 가역적 행동은 실수해도 복구할 수 있지만, 비가역적 행동은 한 번의 실수가 영구적 피해를 남긴다.

가역성 설계 예시:

  • 파일 삭제 → 실제 삭제 대신 .trash/ 폴더로 이동
  • 데이터베이스 업데이트 → 트랜잭션으로 감싸고 스냅샷 저장
  • 이메일 발송 → 검토 큐에 넣고 인간 승인 후 발송
  • API 호출 → 드라이런(dry-run) 모드로 먼저 시뮬레이션

루프 엔지니어링의 원칙인 “행동하기 전에 실수를 허용하는 설계를 갖춰라”는 샌드박싱과 가역성에서 구체화된다.


퀴즈를 불러오는 중…

참고 자료