블래스트 반경(Blast Radius)이 신뢰의 단위가 되어야 한다
요약
AI 에이전트의 보안 취약점은 권한 자체가 아니라, 임무 완료 후에도 남아있는 과도한 권한에서 발생한다. 따라서 작업(task) 단위로 컨테이너를 사용하고 호스트 마운트를 금지하여 '블래스트 반경'을 최소화하는 것이 핵심 해결책이다.
핵심 포인트
- 작업당 하나의 컨테이너를 사용하여 임시적이고 격리된 환경을 구축해야 한다.
- 컨테이너는 작업 완료 후 완전히 제거되어 권한 누출이 없도록 해야 한다.
- 호스트 마운트나 Docker 소켓 접근은 root 권한과 같으므로 절대 사용해서는 안 된다.
- 입력/출력 데이터는 `docker cp` 또는 이름 지정 볼륨을 통해 안전하게 전달해야 한다.
문제는 권한 자체가 아닙니다. 문제는 임무가 끝났음에도 불구하고 남아있는 권한입니다.
이번 주에 제 피드에서 같은 형태의 세 가지 사례를 접했습니다. 프로덕션 데이터베이스를 삭제한 에이전트, 돈이 바닥날 때까지 실행된 자동화 시스템, 멈출 타이밍을 모르는 무언가에 의해 평평하게 만들어진 미러입니다. 스택은 다르고, 국가는 다르지만 근본 원인은 같습니다. 한 가지 작업을 위해 발급되었고 취소되지 않은 자격 증명(credential), 마운트(mount), 또는 네트워크 경로였습니다. 아무도 잘못된 프롬프트를 작성하지 않았습니다. 그저 배달이 끝난 후 문을 열어둔 것뿐입니다.
저는 이 문제의 피해를 입은 쪽이었습니다. 샌드박스를 구축하는 것보다 빠르다고 에이전트에게 제 호스트에 대한 셸(shell) 접근 권한을 주었고, 그 결과 주말 내내 어떤 파일들이 수정되었는지 알아내는 데 시간을 보냈습니다. 해결책은 더 똑똑한 모델이나 더 엄격한 시스템 프롬프트가 아니었습니다. 실수는 지루할 정도로 사소하게 만드는 만큼의 작은 블래스트 반경(blast radius)을 만드는 것이었습니다.
따라서, 작업당 하나의 컨테이너를 사용해야 합니다. 호스트 마운트는 금지합니다. 네트워크는 기본적으로 비활성화합니다. 컨테이너가 작업을 마치면 사라집니다. 이것이 핵심입니다.
왜 '작업당 하나의 컨테이너'이고 '에이전트당 하나의 컨테이너'가 아닌가?
그 이유는 에이전트는 장기간 지속되는 반면, 작업은 그렇지 않기 때문입니다. 만약 에이전트 프로세스가 일주일 동안 파일 시스템, 토큰, 네트워크 경로를 보유하고 있다면, 그 에이전트가 실행하는 모든 작업은 일주일간 축적된 권한을 물려받게 됩니다. 설정 파일을 읽어야 하는 작업과 빌드 디렉토리에서 rm 명령어를 실행해야 하는 작업이 같은 도달 범위(reach)를 공유하게 되는 것입니다.
이를 분리하면, 데이터베이스를 삭제할 수 있는 것은 작업을 수행하는 데 걸리는 90초 동안만 존재하고 그 후에는 사라집니다. '취소'되는 것이 아니라 '사라지는' 것입니다. 컨테이너는 제거되고, 볼륨은 제거되며, 토큰은 만료되어 누출될 것이 아무것도 남지 않습니다.
저는 다음과 같이 실행합니다:
docker run --rm \
--name
다시 읽어보세요. `--network none`은 컨테이너가 인터넷, 내 로컬 네트워크(LAN), 또는 메타데이터 엔드포인트에 접근할 수 없다는 의미입니다. `--read-only`는 루트 파일 시스템에 쓰기가 불가능하며, 유일하게 쓸 수 있는 표면적은 프로세스와 함께 사라지는 256MB의 tmpfs라는 뜻입니다. `--cap-drop ALL`와 `no-new-privileges`를 사용하면 내부에서 무언가가 손상되더라도 권한 상승할 경로가 없다는 의미입니다. `--pids-limit`는 포크 폭탄(fork bomb)이 호스트 전체를 장악하는 대신 죽게 만듭니다.
그리고 결정적으로: 절대 `-v /var/run/docker.sock`을 사용하지 않습니다. Docker 소켓에 접근할 수 있는 에이전트는 추가적인 단계를 거치면 호스트에서 root 권한과 같습니다. 저는 올해 세 개의 '샌드박스' 에이전트 프레임워크에서 이것을 목격했습니다. 그것은 샌드박스가 아니라 단순한 제안일 뿐입니다.
## 호스트를 마운트하지 않고 데이터를 넣고 빼는 방법
사람들이 반대하는 부분이 바로 이 부분이라, 제가 실제로 하는 방법을 알려드리겠습니다. 입력 데이터는 실행 전에 `docker cp`로 들어가거나, 해당 태스크만 볼 수 있는 이름 지정 볼륨(named volume)을 통해 들어옵니다:
docker volume create "in-${TASK_ID}"
docker cp ./task-input/. "task-${TASK_ID}:/task"
출력 데이터도 같은 방식으로 나오고, 그 후에 제가 호스트에서 검증합니다. 에이전트는 제 홈 디렉터리나 SSH 키, 또는 `.env` 파일을 가리키는 경로를 절대 얻지 못합니다. 태스크가 정확히 필요로 하는 것의 복사본을, 단 한 번 실행되는 동안만 존재하는 디렉터리에 받습니다.
네, 복사(copying)하는 것이 마운트(mounting)하는 것보다 느립니다. 그게 요점입니다. 이 복사가 경계선 역할을 합니다.
## 네트워크 문제와 이를 해결하는 프록시
솔직히 말해서 걸리는 부분이 있습니다: 에이전트는 모델 API를 호출해야 하는데, `--network none`은 그것마저 차단합니다. 두 가지 옵션이 있으며 저는 둘 다 사용해봤습니다.
첫 번째는 모델 호출 자체를 샌드박스 외부로 완전히 빼내는 것입니다. 호스트의 오케스트레이터가 LLM과 통신하고, 도구 호출(tool call)을 결정한 다음, 그 단일 호출을 네트워크 연결이 없는 컨테이너로 전송합니다. 이 경우 샌드박스는 stdin 외에는 아무것도 하지 않기 때문에 아웃바운드 연결(egress)이 필요하지 않습니다.
두 번째 방법은 작업이 실제로 네트워크를 필요로 하는 경우에 사용되는, allowlist가 적용된 프록시 사이드카(proxy sidecar)입니다. 샌드박스는 `--network container:proxy`를 받게 되고, 이 프록시는 리스트에 있는 두세 개의 호스트로만 포워딩하며, 그 외의 모든 요청은 403 에러와 로그 라인을 반환합니다. 저는 allowlist를 코드처럼 취급하여 파일에 보관하는데, 왜냐하면 그것 자체가 일종의 코드를 포함하고 있기 때문입니다.
어느 쪽이든 기본값은 꺼져 있습니다. 아웃바운드 연결(Egress)은 에이전트 자체의 속성이 아니라 작업별로 결정해야 하는 문제입니다.
## 자격 증명은 컨테이너보다 먼저 만료되어야 한다
만약 작업에 토큰이 필요하다면, 디스패치 시점에 토큰을 발행하고(mint), 해당 리소스 하나에 한정하여 범위를 지정하며(scope), TTL(Time To Live)을 컨테이너의 수명 주기보다 짧게 설정해야 합니다. 저는 컨테이너는 15분을 사용하고 토큰은 10분을 사용합니다. 만약 작업이 10분이 지났는데도 여전히 실행 중이라면, 그것은 토큰을 연장할 이유가 아니라 버그입니다.
이것이 컨테이너보다 더 중요한 이유는 다음과 같습니다. 컨테이너는 `docker ps`에서 볼 수 있지만, 로그 파일에 유출된 토큰은 그렇지 않습니다. 저는 CI 로그나 충돌 덤프(crash dumps), 심지어 6개월 전 디버깅 세션의 Slack 메시지에서 장기 실행 에이전트 토큰을 발견한 적이 있습니다. 범위 지정(Scope)과 만료 기한(Expiry)만이 우리가 통제할 수 없는 곳에 복사되는 것을 막아주는 유일한 두 가지 제어 장치입니다.
## Docker가 이것을 제품화하는 것이 신호다
Docker는 현재 에이전트를 위해 일회용의 격리된 환경인 [Sandboxes 제품](https://www.docker.com/products/docker-sandboxes/)을 출시했습니다. 저는 아직 이를 극한으로 사용해 본 적은 없기 때문에, 벤치마크를 가지고 있다고 가장하지 않겠습니다. 하지만 컨테이너 회사가 에이전트 샌드박스를 제품화하고 있다는 사실 자체가 업계가 위험을 어디에 두고 생각하는지를 알려줍니다. 그것은 모델 가중치(model weights)에 있는 것이 아닙니다. 그것은 새벽 3시에 데이터베이스 자격 증명을 가지고 있는 프로세스라는 과정(process)에 있습니다.
만약 이 위에 구축하고 있다면, 동일한 규칙이 적용됩니다. 호스트 마운트 금지, 네트워크 비활성화, 샌드박스당 하나의 작업, 그리고 TTL보다 오래된 모든 것을 종료하는 리퍼(reaper)가 필요합니다. 저는 `docker ps --filter label=agent.ttl --format '{{.ID}} {{.Label
## 이것이 당신에게 비용으로 작용하는 것들
콜드 스타트(Cold start). 캐시된 작은 이미지는 제 컴퓨터에서 1초도 안 걸리지만, 만약 러너 이미지(runner image)가 CUDA로 4GB라면 체감할 수 있을 겁니다. 슬림한 러너를 만들고 무거운 작업은 호스트에 두세요.
상태(State). 외부의 어딘가에 기록하지 않으면 '에이전트가 어제 뭘 했는지 기억한다'는 편리함을 잃게 됩니다. 의도적으로 그렇게 하세요. 사용자가 통제하는 메모리 파일이 에이전트가 낙서할 수 있는 파일 시스템보다 낫습니다.
디버깅(Debugging). `--network none` 안에서 무언가가 실패하면, 단순히 `curl`로 답을 찾을 수 없습니다. 디버그 플래그를 추가해서 한 번 실행 동안 네트워크를 열고, 그걸 닫는 것을 잊게 될 겁니다. 그 플래그를 오직 당신의 노트북에만 존재하는 환경 변수(env var) 뒤에 두세요.
이 모든 것이 공짜가 아닙니다. 하지만 대안은 이번 주 세 사람에게 일어난 일입니다: 부여된 임무보다 오래 지속된 권한, 그리고 매우 나쁜 아침을 맞이하는 것입니다.
블래스트 반경(blast radius)을 신뢰의 단위로 만드세요. 그 외 모든 것은 프롬프트일 뿐입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기