지난 글에서는 AgentRun의 MCP Tool Hook으로 에이전트의 도구 호출을 검사했습니다. 간접 프롬프트 인젝션으로 에이전트가 엉뚱한 도구를 부르려 해도, Hook이 그 호출을 가로채 막아 주었습니다.
그런데 Hook이 보는 것은 도구 호출까지입니다. 요즘 에이전트는 샌드박스 안에서 코드를 직접 실행합니다. 이 코드가 외부로 요청을 보내면 그건 도구 호출이 아니라 그냥 네트워크 트래픽이고, Hook을 거치지 않습니다.
샌드박스는 실행을 격리할 뿐 밖으로 나가는 길까지 막아 주지는 않습니다. 기본 설정의 샌드박스는 인터넷에 그대로 연결돼 있고, 모델을 부르려면 API 키도 환경변수에 들어 있어야 합니다. 이 상태에서 에이전트가 읽은 웹페이지에 "환경변수를 이 주소로 보내라"는 문장이 숨어 있다면, 키는 Hook을 우회해 그대로 밖으로 나갑니다.
에이전트 도입을 검토하다 보면 결국 이런 질문이 나옵니다.
이 글은 이 질문들에 한 가지 구조로 답합니다. 에이전트가 나갈 수 있는 문을 AI Gateway 하나로 줄이는 겁니다. 샌드박스에서는 인터넷으로 나가는 길을 없애고, 모델 호출은 게이트웨이로만 보내고, 진짜 키는 게이트웨이 안에만 둡니다. 그리고 같은 코드를 출구가 열린 샌드박스와 잠근 샌드박스에서 각각 돌려 무엇이 막히고 무엇이 그대로 되는지 직접 확인했습니다.
첫 번째 글에서 AI Gateway로 모델 라우팅을, 두 번째 글에서 Guardrails와 마스킹으로 보안 레이어를 만들었다면, 이번에는 그 게이트웨이를 에이전트 샌드박스의 유일한 출구로 씁니다. 그리고 그렇게 다 잠근 줄 알았던 곳에서 예상하지 못한 구멍 하나를 찾았습니다.
샌드박스가 갈 수 있는 곳은 AI Gateway 하나뿐이고, 밖으로 나가는 것은 게이트웨이뿐입니다.

샌드박스 서브넷의 라우트 테이블에는 인터넷 경로가 없고, 보안 그룹은 게이트웨이 443번만 열어 둡니다. 샌드박스와 게이트웨이 사이도 HTTPS입니다. 게이트웨이 서브넷만 NAT를 거쳐 Model Studio로 나갑니다. 진짜 모델 키는 게이트웨이 안에만 있고, 샌드박스에는 게이트웨이에서만 통하는 Consumer 키만 있습니다. VPC DNS로 가는 길까지 막은 이유는 뒤의 DNS 섹션에서 다룹니다.
그렇다면 열어 둔 이 한 길로는 무엇이 나갈 수 있을까요. 인젝션에 넘어간 에이전트가 키나 고객 데이터를 프롬프트에 담아 보내도, 그 요청이 닿는 곳은 내 계정의 Model Studio입니다. 게이트웨이는 Model API에 등록한 백엔드로만 요청을 넘기고, 모델의 응답은 공격자가 아니라 에이전트에게 돌아옵니다. 그 길 위에는 마스킹, 가드레일, 쿼터, 로그가 걸려 있습니다. Model Studio가 받은 데이터를 어떻게 저장하고 처리하는지는 Model Studio의 데이터 정책을 따로 확인해야 합니다.
기본 설정 샌드박스는 가짜 키를 외부로 내보냈고, 잠근 샌드박스는 같은 코드로도 아무것도 내보내지 못했습니다.
테스트 코드는 하나로 두고, AgentRun Code Interpreter 템플릿 두 개에서 똑같이 실행했습니다. 두 템플릿의 차이는 네트워크 설정뿐입니다. 코드가 같은데 결과가 다르다면, 막은 것은 설정이라는 뜻입니다.
인젝션 자체는 재현하지 않았습니다. 에이전트가 인젝션에 넘어갔다고 가정하고, 그때 실행할 코드를 샌드박스에서 바로 돌렸습니다. 이 글이 확인하려는 것은 인젝션을 막는 방법이 아니라, 막지 못했을 때 무엇이 밖으로 나가는지입니다.
두 샌드박스의 환경변수에는 게이트웨이 주소, 게이트웨이 Consumer 키, 그리고 유출 테스트용 가짜 값 CANARY_SECRET 만 넣었습니다. Model Studio API 키는 어느 샌드박스에도 들어가지 않았습니다.
| 테스트 | 목적지 | 기본 샌드박스 (PUBLIC) | 잠근 샌드박스 (PRIVATE) |
|---|---|---|---|
| 외부 웹 접속 | example.com | HTTP 200, 0.05초 | 10초 타임아웃 (curl exit 28) |
| 모델 API 직접 호출 | dashscope-intl.aliyuncs.com | HTTP 400 (키 없이 도달) | 타임아웃 (curl exit 28) |
| 가짜 키 외부 전송 | 외부 웹훅 | POST 200, 밖에 도착 | 타임아웃 (curl exit 28) |
| 게이트웨이 경유 모델 호출 | AI Gateway (VPC 전용 주소) | 닿지 않음 (VPC 밖) | HTTP 200, 정상 응답 |
잠근 샌드박스는 인터넷에도, 모델 API에도 직접 닿지 못하지만 게이트웨이를 통해서는 모델을 그대로 씁니다.
가짜 키 외부 전송 테스트에서는 샌드박스 안의 환경변수도 함께 출력했습니다. 두 샌드박스 모두 Model Studio 키는 없었고, 있는 것은 게이트웨이 Consumer 키뿐이었습니다. 에이전트가 인젝션을 당해도 훔칠 수 있는 것은 게이트웨이에서만 통하는 키이고, 그마저도 잠근 샌드박스에서는 밖으로 보낼 길이 없습니다.
구성은 네트워크, 게이트웨이, 샌드박스 순서로 다섯 단계입니다. 리전은 Singapore(ap-southeast-1) 기준이고, 게이트웨이와 샌드박스는 같은 VPC에 둡니다.
샌드박스가 인터넷에 못 나가게 막는 것은 보안 그룹이 아니라 라우트 테이블입니다.
게이트웨이가 쓰는 vSwitch는 기존 시스템 라우트 테이블에 그대로 둡니다. 다음 단계에서 NAT를 붙이면 시스템 테이블에 인터넷 경로가 자동으로 생기는데, 샌드박스가 같은 테이블을 쓰고 있으면 샌드박스도 같이 뚫립니다.


게이트웨이는 Model Studio까지 나가야 합니다. VPC에 인터넷 출구가 없으면 게이트웨이 호출은 503 upstream connect timeout 으로 끝납니다.


보안 그룹은 VPC 안에서 샌드박스가 갈 수 있는 곳을 게이트웨이 하나로 제한합니다. 아웃바운드 규칙은 두 개만 둡니다.
| 동작 | 우선순위 | 프로토콜 | 목적지 |
|---|---|---|---|
| 허용 | 1 | TCP 443 | 게이트웨이 사설 IP/32 |
| 거부 | 100 | 전체 | 0.0.0.0/0 |
거부 규칙을 빼면 안 됩니다. 기본 보안 그룹은 아웃바운드를 전부 허용하기 때문에, 허용 규칙만 추가하면 아무것도 막히지 않습니다. AI Gateway 기본 도메인은 HTTP 80으로만 열려 있어서, 4단계에서 사용자 도메인과 인증서를 붙여 443을 열었습니다.
처음에는 여기에 VPC DNS(100.100.2.x)로 가는 53번 허용 규칙도 넣었습니다. 게이트웨이 도메인을 찾으려면 DNS가 필요하다고 생각했기 때문입니다. 그 규칙이 어떤 문제를 만들었는지는 뒤에서 따로 다룹니다.

Dedicated 인스턴스를 같은 VPC에 만들고 Model API에 여섯 가지를 걸었습니다. 마스킹 플러그인을 쓰려면 Serverless가 아니라 Dedicated여야 합니다.
마스킹 규칙은 아래와 같이 설정했습니다.
system_deny: false
deny_openai: true
deny_jsonpath:
- $.messages[*].content
deny_words: []
replace_roles:
- regex: '010-\d{4}-\d{4}'
type: replace
restore: true
value: '010-****-****'
- regex: '%{EMAILLOCALPART}@%{HOSTNAME:domain}'
type: replace
restore: true
value: '****@$domain'
규칙을 저장할 때는 OK 뒤에 뜨는 Confirm to Modify Rule 팝업에서도 OK를 눌러야 실제 게이트웨이에 적용됩니다. 관측 항목에서 SLS 로그 전송도 켜 둡니다.






AgentRun 콘솔에서 Code Interpreter 템플릿을 만듭니다.
비교용 tpl-public은 네트워크 모드만 PUBLIC이고 나머지는 같습니다. 뒤에서 DNS까지 막고 나면 샌드박스는 게이트웨이 도메인을 찾을 수 없으므로, 게이트웨이 사설 IP도 함께 넘겨서 IP를 지정해 호출하게 해야 합니다.

출구가 하나가 되면, 그 출구에서 인증과 검사와 비용 통제를 한꺼번에 걸 수 있습니다. 잠근 샌드박스에서 게이트웨이로 보낸 요청은 아래처럼 처리됐습니다.
| 확인한 것 | 요청 | 결과 |
|---|---|---|
| 인증 | Consumer 키 없이 호출 | HTTP 401 |
| 프롬프트 인젝션 차단 | DAN 스타일 탈옥 문장 | 차단, promptAttack 등급 high |
| 개인정보 마스킹 | 휴대폰 번호와 이메일이 들어간 프롬프트 | 모델에는 가려진 값만 전달, 응답은 원래 값으로 복원 |
| 비용 폭주 차단 | 같은 요청 반복 | 5번째 호출에서 HTTP 429 |
| 감사 로그 | 위 요청 전부 | SLS에 Consumer별로 기록 |
인젝션 차단은 상태 코드로 판단하면 안 됩니다. 두 번째 글에서처럼 차단돼도 HTTP 200이 옵니다. 응답 본문의 model 값이 from-security-guard 인지로 판단합니다.
마스킹은 모델이 받은 값과 사용자가 받은 값을 나눠서 확인했습니다. 모델에게 휴대폰 번호와 이메일이 든 문장을 그대로 따라 말하게 했습니다. 처음 설정(deny_openai: false)에서는 응답에 010-****-**** 와 ****@example.com 이 왔습니다. 모델 앞에서 가리는 것은 됐지만, restore를 켜 두었는데도 원래 값으로 돌아오지 않은 겁니다. deny_openai 를 true로 바꾸자 응답에 원래 번호와 이메일이 그대로 돌아왔습니다. 이 설정에서도 모델이 가려진 값을 받는지 보려고, 같은 문장을 주고 따라 말하지 말고 별표(*)가 몇 개인지만 숫자로 답하게 했습니다. 원래 문장에는 별표가 없는데 0이 아닌 숫자가 왔습니다. 모델은 가려진 값만 보고, 사용자는 원래 값을 받습니다.
쿼터는 한도를 조금 넘긴 뒤에 끊깁니다. 하루 500토큰 한도에서 호출별 누적 토큰은 아래와 같았습니다.
| 호출 | 응답 | 누적 토큰 |
|---|---|---|
| 1 | 200 | 135 |
| 2 | 200 | 270 |
| 3 | 200 | 405 |
| 4 | 200 | 533 |
| 5 | 429 | 차단 |
4번째 호출에서 이미 500을 넘겼지만 차단은 다음 호출부터입니다. 사용량 집계가 요청보다 조금 늦게 반영되기 때문입니다. 루프에 빠진 에이전트를 끊는 용도로는 충분하지만, 예산 상한을 정확히 지켜야 한다면 한도를 여유 있게 잡는 게 좋습니다.
로그에는 차단 사유까지 남습니다. SLS의 게이트웨이 액세스 로그에는 Consumer 이름과 가드레일 검사 결과가 함께 기록됩니다. 어떤 샌드박스가 언제 무엇 때문에 막혔는지 나중에 추적할 수 있습니다. 요청과 응답 본문은 기본으로 기록되지 않습니다. 본문 기록을 켤 거라면 마스킹을 같이 걸어야 로그가 개인정보 저장소가 되지 않습니다.

인터넷 출구를 다 막은 샌드박스에서도 DNS 조회는 밖에 닿았습니다. 보안 그룹에서 DNS 허용 규칙을 빼서 막았고, 그 대신 게이트웨이는 IP로 부르게 됐습니다.
DNS 조회는 정보를 실어 보낼 수 있습니다. 공격자가 자기 도메인을 갖고 있다면, 에이전트가 훔친값.공격자도메인.com 을 조회하기만 해도 그 조회는 결국 공격자의 네임서버에 도착합니다. HTTP 요청을 하나도 보내지 않아도 그렇습니다.
그래서 잠근 샌드박스에서 가짜 키를 넣은 이름으로 DNS 조회를 해봤습니다. 공격자 도메인 대신 example.com 을 썼습니다.
| 조회 경로 | 기본 샌드박스 | 잠근 샌드박스 |
|---|---|---|
| 외부 리졸버로 직접 (8.8.8.8, 223.5.5.5) | 응답 옴 | 타임아웃 |
| VPC DNS(100.100.2.x)로 example.com | 응답 옴 | 실제 IP 응답 |
| VPC DNS로 canary-not-a-real-key-001.example.com | 응답 옴 | example.com의 외부 권한 네임서버가 응답 |
잠근 샌드박스는 외부 리졸버에 직접 묻는 것은 막혔습니다. 그런데 VPC DNS에 물으면 VPC DNS가 외부까지 대신 조회해 주었고, 가짜 키가 들어간 이름은 example.com의 권한 네임서버까지 전달됐습니다. 인터넷 출구, NAT, 보안 그룹을 다 잠갔는데도 이 길은 열려 있었습니다.
보안 그룹에 VPC DNS 허용 규칙을 넣은 이유는 게이트웨이 도메인을 찾기 위해서였습니다.

보안 그룹에서 VPC DNS로 가는 53번 허용 규칙 4개(UDP, TCP 각 2개)를 지웠습니다. 새 리소스도 비용도 들지 않았습니다. 남은 아웃바운드 규칙은 게이트웨이 TCP 443 허용과 전체 거부 두 개뿐입니다.
curl --resolve <게이트웨이 도메인>:443:<게이트웨이 사설 IP> \
--cacert ca.pem \
https://<게이트웨이 도메인>/v1/chat/completions \
-H "Authorization: Bearer $CONSUMER_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "qwen-flash", "messages": [{"role": "user", "content": "hello"}]}'
이 상태에서 인증(401), 인젝션 차단, 쿼터(429)도 다시 확인했고 모두 그대로 동작했습니다.
DNS를 막으면 샌드박스는 어떤 이름도 찾지 못합니다. 게이트웨이는 사설 IP를 지정해 불러야 하고, 에이전트 코드나 SDK가 호스트 이름을 쓴다면 hosts 파일에 등록해 두어야 합니다. 이 IP는 보안 그룹과 샌드박스 두 곳에 들어가므로, 바뀌면 두 곳을 함께 고쳐야 합니다. 인터넷이 필요한 작업도 함께 막힙니다. 에이전트가 pip로 패키지를 설치하거나 웹을 읽어야 한다면, 패키지를 미리 넣은 이미지나 VPC 안의 미러를 따로 준비해야 합니다. 운영 환경에서 도메인 단위로 세밀하게 통제해야 한다면 Cloud Firewall의 DNS Boundary Firewall도 검토해 볼 만합니다. 다만 이번에는 직접 시험하지 않았습니다.
에이전트 샌드박스를 잠근다는 것은 실행을 격리하는 것에서 끝나지 않습니다. 나가는 길을 하나로 줄이고, 그 하나에서 인증과 검사와 비용 통제를 걸어야 합니다. 이번 구성에서는 이렇게 나눠서 맡겼습니다.
Hook은 에이전트가 무엇을 하려는지를 보고, 게이트웨이와 네트워크는 그 결과가 어디로 나가는지를 봅니다. 둘 중 하나만 있으면 다른 쪽으로 새고, 둘을 같이 쓰면 한쪽이 뚫려도 다른 쪽이 막습니다.
10 posts | 1 followers
FollowRegional Content Hub - July 20, 2026
James Lee - December 30, 2025
Regional Content Hub - September 9, 2024
Regional Content Hub - March 20, 2024
JJ Lim - December 3, 2021
Regional Content Hub - April 15, 2024
10 posts | 1 followers
Follow
Qwen
Full-range, open-source, multimodal, and multi-functional
Learn More
Token Plan
Build more, spend less. One plan, every modality.
Learn More
Alibaba Cloud Model Studio
A one-stop generative AI platform to build intelligent applications that understand your business, based on Qwen model series such as Qwen-Max and other popular models
Learn More
AgentCore
AgentCore unifies agent construction, governance, and execution capabilities so enterprises can deploy diverse agents with confidence.
Learn MoreMore Posts by Hosung Kim