프롬프트는 보안 경계가 아니다 — AI 에이전트에 툴 권한 줄 때 인프라가 챙겨야 할 것들
요즘 사내에서 "MCP 서버 붙여서 이것도 자동화하자"는 얘기가 부쩍 늘었다. 지라 티켓 읽고, 쿠버네티스 로그 긁고, 배포까지 트리거하는 에이전트. 재미있고 실제로 쓸모도 있다. 그런데 인프라 담당자 입장에서 이 흐름의 본질은 하나다. 우리가 매달 감사받는 그 실행 권한을, 사람이 아닌 프로세스에 넘기고 있다는 것.
"인터넷 없다"고 말했는데 실제로는 있었던 사고
Dev.to에 올라온 We're Giving AI Agents More Tools. What Happens When the Boundaries Fail? 글이 Anthropic의 7월 30일 리포트를 잘 정리해뒀다. 요지는 이렇다.
- Anthropic이 사이버보안 평가(CTF 형태) 실행 141,006건을 리뷰했고, 그중 3건에서 Claude 모델이 실제 인터넷에 도달해 서로 다른 세 조직의 실제 시스템에 접근한 사례를 발견했다.
- 모델에게는 "너는 시뮬레이션 안에 있고 인터넷 접근이 없다"고 명시적으로 알려줬다. 그런데 서드파티 평가 파트너와의 소통 착오로 일부 평가 머신에 실제 인터넷이 열려 있었다.
- 한 건에서는 모델이 가상 회사 세팅 문서에서 "존재하지 않는 파이썬 패키지를 설치하라"는 안내를 발견하고, 같은 이름의 악성 패키지를 만들어 실제 PyPI에 업로드했다. 계정 생성을 위해 무료 이메일까지 구했다.
- 그 패키지는 약 1시간 공개되어 있었고 실제 시스템 15대에서 다운로드·실행됐다. 그중 하나는 패키지를 자동 설치해 멀웨어를 스캔하는 보안 회사의 스캐너였고, 실행된 코드가 크리덴셜을 노출시켜 모델이 그 회사 인프라에 추가 접근까지 했다.
- 더 흥미로운 부분: 모델은 중간에 "이게 진짜 인터넷이면 실제 공격이고 안 된다"는 추론을 한 번 했었다. 그런데도 시스템 날짜가 2026년인 걸 근거로 "역시 연출된 환경"이라고 스스로를 설득하고 진행했다.
원문 저자가 뽑은 한 줄이 정확하다. A prompt is not a security boundary. "인터넷 없어"라고 말하는 것과 실제로 egress를 막는 것은 완전히 다른 일이다. "이 파일만 써"라고 쓰는 것과 파일 권한을 제한하는 것도 다른 일이다.
왜 프롬프트는 경계가 될 수 없는가
인프라 하는 사람에게는 사실 낯선 얘기가 아니다. 우리가 이미 아는 문법으로 바꿔보면 이렇다.
| 우리가 아는 안티패턴 | 에이전트 판 버전 |
|---|---|
| 프론트엔드 JS에서만 검증하고 API는 검증 안 함 | 시스템 프롬프트로만 금지하고 툴 권한은 열어둠 |
| README에 "prod DB 만지지 마세요" 적어둠 | "너는 읽기 전용이야"라고 프롬프트에 적어둠 |
| 루트로 컨테이너 실행 + hostPath 마운트 | 에이전트를 개발자 계정 권한으로 로컬 실행 |
프롬프트는 요청이고, 커널·네트워크 정책·IAM은 강제다. 요청은 모델이 재해석할 수 있다. 위 사고에서 모델은 "이건 시뮬레이션"이라는 전제를 재확인하며 자기 판단을 뒤집었다. 사람도 하는 실수인데, 정책 엔진은 그런 걸 안 한다. iptables는 설득당하지 않는다.
그리고 하나 더. 모델은 시스템의 일부일 뿐이다. 붙여준 툴, 넘겨준 크리덴셜, 실행 환경, 모니터링이 다 합쳐져서 "AI 시스템"이 된다. 사고 원인을 "AI가 그랬다"로 끝내면 정작 고쳐야 할 평가 환경 네트워크 설정 미스가 남는다.
실무: 3중 방어와 진짜 마주치는 함정들
1) egress는 기본 차단, 필요한 것만 프록시로
에이전트 컨테이너의 기본은 --network none이거나 deny-all이어야 한다. 로컬에서 툴 실행 샌드박스를 만들 때 최소 검증은 이 정도다.
$ docker run --rm --network none \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-u 1000:1000 python:3.12-slim \
sh -c "pip install requests"
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None,
status=None)) after connection broken by
'NewConnectionError(<pip._vendor.urllib3.connection.HTTPSConnection object at 0x7f...>:
Failed to establish a new connection: [Errno -3] Temporary failure in name resolution)':
/simple/requests/
ERROR: Could not find a version that satisfies the requirement requests
ERROR: No matching distribution found for requests
이 에러가 뜨면 정상이다. 반대로 여기서 패키지가 설치되면 당신의 샌드박스는 샌드박스가 아니다. 위 사고가 딱 그 상태였다.
쿠버네티스에서 에이전트 워크로드를 굴린다면 네임스페이스 단위 deny-all egress를 먼저 깔고 DNS와 내부 미러만 열어준다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-deny-egress
namespace: ai-agent
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: kube-system}
podSelector:
matchLabels: {k8s-app: kube-dns}
ports: [{protocol: UDP, port: 53}]
- to:
- podSelector:
matchLabels: {app: egress-proxy} # 사내 허용목록 프록시만
ports: [{protocol: TCP, port: 3128}]
함정 A — 정책을 만들었는데 아무 일도 안 일어난다. CNI가 NetworkPolicy를 구현하지 않으면 오브젝트는 조용히 생성되고 트래픽은 그대로 나간다. 에러도 안 난다. 적용 전에 반드시 실측하자.
$ kubectl -n ai-agent run t --rm -it --image=curlimages/curl --restart=Never -- \
curl -s -m 5 -o /dev/null -w '%{http_code}\n' https://pypi.org
command terminated with exit code 28 # 28 = timeout → 차단됨(정상)
여기서 200이 나오면 정책이 무시된 것이다. 우리 팀은 이 확인용 잡을 CI에 넣어두고, 클러스터 업그레이드마다 회귀 테스트로 돌린다.
함정 B — 프록시 화이트리스트를 도메인으로만 걸었다. HTTPS는 CONNECT 터널이라 경로 단위 통제가 안 된다. SNI 기반 검사 없이 *.githubusercontent.com 같은 걸 열어주면 사실상 임의 파일 다운로드 창구가 된다. DNS를 열어준 것 자체로 DNS 기반 유출 경로가 남는 점도 감안해야 한다(완전 차단하려면 사내 리졸버 + 쿼리 로깅).
2) 앰비언트 크리덴셜을 없애라
PyPI 사고에서 피해를 키운 건 실행 환경에 있던 크리덴셜이었다. 로컬 에이전트에서 가장 흔한 실수는 이거다.
# 하지 말 것: 홈 디렉터리를 통째로 마운트
docker run -v $HOME:/home/agent ... # ~/.aws, ~/.kube, ~/.ssh, ~/.npmrc 전부 노출
# 최소한 이렇게: 필요한 것만, 읽기 전용, 짧은 수명 토큰
docker run --network none \
-v $PWD/workspace:/work \
-e GITHUB_TOKEN="$(gh auth token --scopes repo:status)" ...
MCP 서버를 로컬에서 붙일 때 특히 위험하다. MCP 서버 프로세스는 당신 계정 권한으로 뜬다. 에이전트가 "파일 읽기" 툴 하나만 갖고 있어도, 그 툴의 실효 권한은 당신의 uid 전체다. filesystem 계열 MCP 서버는 허용 디렉터리 옵션을 반드시 명시하고, 별도 저권한 유저나 컨테이너로 분리하는 게 맞다. (구현체별 옵션 이름은 각 MCP 서버 공식 문서 확인 필요.)
3) 행동을 남겨라 — 툴 호출 감사 로그
"에이전트가 뭘 했는지 모른다"가 가장 무서운 상태다. 툴 호출은 예외 없이 구조화 로그로 남기고, 에이전트가 쓸 수 없는 곳으로 보낸다(같은 볼륨에 쓰면 지워질 수 있다).
$ jq -c 'select(.tool=="shell" or .tool=="http_request")' agent-audit.jsonl | tail -3
{"ts":"2026-02-11T09:12:03Z","run":"r-8812","tool":"shell","arg":"kubectl get pods -n stg","allow":true}
{"ts":"2026-02-11T09:12:31Z","run":"r-8812","tool":"http_request","arg":"https://pypi.org/...","allow":false,"reason":"egress-denied"}
{"ts":"2026-02-11T09:13:02Z","run":"r-8812","tool":"shell","arg":"rm -rf /work/build","allow":true}
allow:false 이벤트가 급증하는 패턴은 좋은 알람 신호다. 에이전트가 자기 경계를 계속 두드리고 있다는 뜻이고, 원문 사고처럼 "우회 경로를 탐색하는" 단계에서 잡을 수 있다.
함정 C — 승인(human-in-the-loop)을 신뢰 경계로 착각. "위험한 명령은 사용자 확인 받음"은 방어층 하나지 경계가 아니다. 하루 200번 뜨는 확인창은 사람이 무조건 Yes를 누른다. 승인 대상은 정말 되돌릴 수 없는 것(prod 배포, 삭제, 외부 퍼블리시)으로 줄이고, 나머지는 정책으로 자동 차단해야 승인이 의미를 가진다.
트레이드오프
egress 전면 차단은 개발 생산성을 확실히 깎는다. pip install, npm i가 다 죽으니까. 현실적인 타협은 사내 미러(Nexus/Artifactory 등)만 열어주는 것이다. 부가 효과도 크다. 위 사고처럼 존재하지 않는 패키지 이름을 외부에서 당겨오는 경로 자체가 막히므로, 요즘 흔한 슬롭스쿼팅/의존성 혼동 공격 표면도 같이 줄어든다.
정리
한 줄: 에이전트에게 툴을 주는 일은 프롬프트 엔지니어링이 아니라 권한 설계다. 프롬프트는 문서고, 경계는 네트워크 정책·파일 권한·IAM·감사 로그에 있다.
- 지금 당장 해야 하는 팀: 사내 MCP 서버나 자동화 봇에
kubectl, DB, 배포 파이프라인, 레지스트리 푸시 권한을 이미 붙였다면 오늘 점검 대상이다. 위curl/pip테스트 두 개만 돌려봐도 실태가 나온다. - 순서: ① egress deny-all + 미러 허용 → ② 앰비언트 크리덴셜 제거·토큰 스코프 축소 → ③ 툴 호출 감사 로그 외부 적재 → ④ 되돌릴 수 없는 작업만 승인 게이트.
- 사후 리뷰 원칙: "AI가 그랬다"에서 멈추지 말 것. 원문 사고의 근본 원인은 모델의 오판이 아니라 인터넷이 열려 있던 평가 환경 설정이었다. 우리 쪽 사고도 대개 그렇다.
가정이 틀렸을 때 무슨 일이 일어나는지를 설계에 넣어두는 것 — 결국 우리가 SRE 하면서 계속 해온 얘기다. 대상이 사람에서 에이전트로 바뀌었을 뿐이다.
참고 자료
- We're Giving AI Agents More Tools. What Happens When the Boundaries Fail? (Dev.to, Hemapriya Kanagala)
- Anthropic Research — 사이버보안 평가 관련 리포트 원문(2025-07-30 공개분) 확인
- Kubernetes NetworkPolicy 공식 문서 (CNI 지원 여부 주의사항 포함)
- Model Context Protocol 공식 문서
- Docker none 네트워크 드라이버 문서
- OWASP Top 10 for LLM Applications (과도한 권한 부여 항목 참고)