MCP 멀티에이전트 위임 패턴: RFC 8693 Token Exchange와 감사 로그로 에이전트 체인 보안 설계하기
AI 에이전트가 단독으로 작동하는 시대는 지났습니다. 오늘날 실제 프로덕션 환경에서는 에이전트가 또 다른 에이전트를 호출하고, 그 에이전트가 다시 서드파티 도구를 실행하는 복잡한 체인이 일상적으로 동작합니다. MCP(Model Context Protocol)는 A2A(Agent-to-Agent), ACP 같은 경쟁 프로토콜이 공존하는 가운데서도 가장 빠르게 채택되고 있는 에이전트-도구 통신 프로토콜이지만, 이 체인 위에서 "누가 무엇을 허가했는가"를 추적하는 문제는 여전히 새로운 설계 과제로 남아 있습니다.
이 글은 멀티에이전트 위임(Delegation) 패턴의 핵심인 토큰 전파(Token Propagation)와 책임 추적(Responsibility Tracking)을 RFC 8693 Token Exchange·OIDC-A·A2A 프로토콜 등 최신 표준을 중심으로 설계하는 방법을 다룹니다. 단순한 개념 소개에 그치지 않고, 실제 구현 예시와 함께 보안 리스크 대응 방안까지 함께 살펴봅니다. 이 글은 OAuth 2.0 기본 개념(Authorization Code Grant 흐름 정도)을 알고 있는 백엔드·보안 개발자를 주요 대상으로 합니다.
2025~2026년에 걸쳐 MCP 인가 스펙이 세 차례 주요 업데이트를 거쳤습니다. 특히 2026년 3월 스펙(2026-03-15)부터 RFC 8707 호환 resource 파라미터 검증이 의무화되어, 이전 구현은 호환성을 잃을 수 있는 상황입니다. Microsoft Entra ID에서 발견된 CVE-2025-55241은 에이전트의 actor 토큰 검증 미흡으로 인해 서로 다른 위임 컨텍스트의 토큰을 조합해 STS(Security Token Service, 보안 토큰 발급 서버)를 속이는 '위임 체인 접합(Delegation Chain Splicing)' 공격이 실제로 가능함을 확인한 사례로, 업계 전반의 표준화를 가속시키고 있습니다.
핵심 개념
위임 vs 가장: 이 구분이 모든 것의 출발점입니다
멀티에이전트 시스템에서 가장 먼저 명확히 해야 할 개념적 구분이 있습니다.
| 구분 | 위임(Delegation) | 가장(Impersonation) |
|---|---|---|
| 행위자 구분 | 대리 에이전트와 원래 사용자가 분리되어 식별 가능 | 행위자가 원래 사용자인 것처럼 완전히 대체 |
| 감사 추적 | 체인 전체가 로그에 기록됨 | 원래 주체만 기록, 대리 행위자 불투명 |
| 보안 수준 | 최소 권한 원칙 적용 가능 | 권한 범위 통제 어려움 |
| MCP 권장 여부 | 권장 | 지양 |
핵심 원칙 멀티에이전트 환경에서 가장(Impersonation)은 "누가 실제로 행동했는가"를 숨기기 때문에, 사고 발생 시 원인 추적이 불가능해집니다. 추적 가능성을 위해 위임 방식을 사용하는 것을 권장합니다.
RFC 8693: 토큰 전파의 핵심 표준
OAuth 2.0 Token Exchange(RFC 8693)는 에이전트 체인을 따라 인증·인가 정보가 흘러야 할 때의 표준 방식입니다. STS(Security Token Service, 보안 토큰 발급 서버)가 각 홉(hop, 에이전트 간 이동 단계)마다 새로운 위임 토큰을 발급하며, 이 스펙의 핵심은 act(actor) 클레임을 통한 중첩 위임 구조입니다.
RFC 8693 Section 4.1에 따르면, act 클레임에서 바깥쪽 sub이 가장 직접적인(최종) 행위자에 해당하고, 안쪽으로 중첩될수록 더 이전 단계의 행위자를 나타냅니다. 오케스트레이터 에이전트가 서브 에이전트에게 위임하는 3단계 체인을 예로 들면 다음과 같습니다.
{
"sub": "user-alice",
"act": {
"sub": "sub-agent",
"act": {
"sub": "orchestrator-agent"
}
}
}이 구조는 "sub-agent가 가장 직접적인 최종 행위자로서, orchestrator-agent를 거쳐 user-alice를 대신해 행동하고 있다"는 사실을 토큰 자체에 내포합니다. 위임의 시간적 흐름은 user-alice → orchestrator-agent → sub-agent 순이며, 가장 바깥쪽 act.sub이 현재 최종 행위자입니다.
Token Exchange 요청의 기본 형태는 다음과 같습니다.
POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=<user_access_token>
&subject_token_type=urn:ietf:params:oauth:token-type:access_token
&actor_token=<orchestrator_agent_token>
&actor_token_type=urn:ietf:params:oauth:token-type:access_token
&scope=read:orders
&resource=https://api.example.com/ordersresource 파라미터는 2026년 3월 MCP 스펙 업데이트에서 의무화된 RFC 8707 리소스 인디케이터입니다. 이를 통해 특정 리소스용으로 발급된 토큰이 다른 리소스에 재사용되는 토큰 오용(mis-redemption) 공격을 방어합니다.
OIDC-A 1.0: 에이전트 전용 신원 클레임 (표준화 진행 중인 제안)
OIDC-A(OpenID Connect for Agents)는 기존 OIDC Core 1.0을 에이전트 환경에 맞게 확장하는 방향으로 제안된 스펙입니다. 2026년 4월 현재 공식 OIDC Foundation 표준으로 확정되지 않은 초안 수준의 제안이지만, 다수의 구현체에서 실험적으로 채택하고 있어 향후 표준화 가능성이 높은 방향입니다. 에이전트 세션 시작 시 발급되는 ID Token에는 다음과 같은 클레임들이 포함될 수 있습니다.
{
"sub": "agent-instance-xyz",
"agent_type": "orchestrator",
"agent_model": "claude-sonnet-4-6",
"agent_instance_id": "sess-abc123",
"delegator_sub": "user-alice",
"delegation_chain": [
"user-alice",
"orchestrator-agent",
"sub-agent-b"
]
}용어 정의
delegation_chain클레임은 이 에이전트가 거쳐온 위임 경로를 시간 순서대로 기록한 배열입니다. 감사 시스템이 이 값을 파싱해 어느 단계에서 이상 행동이 발생했는지 추적할 수 있습니다.
A2A 프로토콜: 에이전트 간 협력을 위한 보완 표준
Google이 주도하는 A2A(Agent-to-Agent) 프로토콜은 MCP와 상호보완적인 역할을 합니다. MCP가 에이전트와 도구 사이의 통신을 표준화한다면, A2A는 에이전트 간 피어(peer) 협업, 태스크 협상, 위임 흐름 자체를 표준화합니다. CrewAI는 2026년 A2A 태스크 위임을 네이티브 지원으로 추가했으며, 복잡한 멀티에이전트 파이프라인에서는 MCP와 A2A를 함께 사용하는 구성이 점차 확산되고 있습니다.
책임 추적을 위한 감사 로그 구조
책임 추적은 에이전트가 수행한 모든 행동·결정·위임 이벤트의 연대순 로그입니다. IETF에 제출된 초안(draft-sharif-agent-audit-trail-00)이 제안하는 로그 항목 구조는 다음과 같습니다.
interface AuditLogEntry {
timestamp: string; // ISO 8601
trace_id: string; // 전체 요청 체인을 관통하는 ID
action_type:
| "tool_call"
| "tool_response"
| "decision"
| "delegation" // 에이전트 → 에이전트
| "escalation" // 에이전트 → 인간
| "error";
actor: {
sub: string; // 현재 행위 에이전트
delegation_chain: string[]; // 위임 계보 (시간순)
};
resource: string; // 접근 대상 리소스
policy_evaluation: {
decision: "allow" | "deny";
matched_policy: string;
};
payload_hash: string; // 불변성 검증용 해시
}이 구조에서 trace_id는 사용자 요청 시작부터 최하위 에이전트 응답까지 전체 체인을 하나로 묶는 핵심 필드입니다.
실전 적용
핵심 개념들을 실제 코드로 어떻게 구현하는지 세 가지 시나리오를 통해 살펴봅니다. 각 예시는 위임 패턴의 서로 다른 레이어(검증, 협상, 취소)를 다루어 전체 생애주기를 보여줍니다.
예시 1: Identity-Aware Gateway를 통한 3중 신원 검증
실제 엔터프라이즈 환경에서 가장 많이 채택되는 패턴은 모든 MCP 호출이 API 게이트웨이를 통과하도록 강제하는 방식입니다. Gravitee 4.11이 구현한 Trusted On-Behalf-Of(OBO) 패턴을 기반으로 한 예시입니다.
# mcp_gateway_middleware.py
import jwt
import os
from typing import TypedDict
from dataclasses import dataclass
from cryptography.hazmat.primitives.serialization import load_pem_public_key
# 공개키는 환경 변수에서 로드합니다. 하드코딩은 지양해야 합니다.
PUBLIC_KEY = load_pem_public_key(os.environ["JWT_PUBLIC_KEY"].encode())
# 위임 깊이 제한 (일반적으로 3~5 수준을 권장합니다)
# 너무 낮으면 복잡한 위임 시나리오를 처리할 수 없고,
# 너무 높으면 체인 전체의 보안 검토가 어려워집니다.
MAX_DELEGATION_DEPTH = 5
@dataclass
class PolicyResult:
allowed: bool
matched_policy: str
reason: str | None = None
async def evaluate_policy(
principal: str,
resource: str,
action: str,
context: dict,
) -> PolicyResult:
"""
외부 정책 엔진(Cerbos, OPA 등)과 연동해 RBAC 결정을 수행합니다.
Cerbos 연동 예시:
from cerbos.sdk.client import CerbosClient
client = CerbosClient(host=os.environ["CERBOS_HOST"])
resp = client.check_resource(
principal=Principal(id=principal),
resource=Resource(id=resource, kind="mcp_tool"),
actions=[action],
context=context,
)
return PolicyResult(
allowed=resp.is_allowed(action),
matched_policy=resp.outputs[0].policy_version if resp.outputs else "",
)
"""
raise NotImplementedError("정책 엔진 연동 구현이 필요합니다")
class TripleIdentityContext(TypedDict):
user_sub: str
agent_sub: str
tool_id: str
delegation_chain: list[str]
scope: list[str]
async def verify_triple_identity(
user_token: str,
agent_token: str,
tool_id: str,
resource: str,
) -> TripleIdentityContext:
"""
사용자 → 에이전트 → 도구 세 신원을 동시에 검증합니다.
RFC 8693 act 클레임 체인을 통해 위임 맥락을 보존합니다.
"""
# 1. 사용자 토큰 검증 (공개키 필수 — key 없이는 DecodeError 발생)
user_claims = jwt.decode(
user_token,
key=PUBLIC_KEY,
algorithms=["RS256"],
)
# 2. 에이전트 토큰 검증 및 act 클레임에서 위임 체인 추출
agent_claims = jwt.decode(
agent_token,
key=PUBLIC_KEY,
algorithms=["RS256"],
)
delegation_chain = extract_delegation_chain(agent_claims)
# 3. 위임 깊이 제한 검증
if len(delegation_chain) > MAX_DELEGATION_DEPTH:
raise ValueError(
f"Delegation depth {len(delegation_chain)} exceeds limit {MAX_DELEGATION_DEPTH}"
)
# 4. RFC 8707 리소스 인디케이터로 토큰 오용 방지
if agent_claims.get("aud") != resource:
raise ValueError(
f"Token audience mismatch: expected {resource}, "
f"got {agent_claims.get('aud')}"
)
# 5. 정책 엔진(Cerbos 등)으로 RBAC 평가
policy_result = await evaluate_policy(
principal=user_claims["sub"],
resource=resource,
action="mcp_tool_call",
context={"tool_id": tool_id, "agent": agent_claims["sub"]},
)
if not policy_result.allowed:
raise PermissionError(
f"Policy denied: {policy_result.matched_policy}"
)
return TripleIdentityContext(
user_sub=user_claims["sub"],
agent_sub=agent_claims["sub"],
tool_id=tool_id,
delegation_chain=delegation_chain,
scope=agent_claims.get("scope", "").split(),
)
def extract_delegation_chain(claims: dict) -> list[str]:
"""
act 클레임을 순회해 위임 계보를 시간순 리스트로 변환합니다.
RFC 8693에서 바깥쪽 act.sub이 가장 직접적인(최종) 행위자이므로,
순회 후 역순 정렬해 [원래 주체, 첫 위임자, ..., 최종 행위자] 형태로 반환합니다.
"""
act_chain = []
act = claims.get("act")
while act:
act_chain.append(act["sub"]) # 바깥쪽(최근) → 안쪽(이전) 순으로 추가
act = act.get("act")
act_chain.reverse() # 이전 → 최근 순으로 정렬
return [claims["sub"]] + act_chain # [원래 주체, ..., 최종 행위자] 순예시 2: 위임 요청 협상 워크플로
에이전트 A가 에이전트 B에게 작업을 위임할 때, 수신 에이전트가 요청된 권한 범위를 수락·거절·역제안할 수 있는 협상 패턴입니다.
// delegation-negotiation.ts
// 위임 깊이 제한 (일반적으로 3~5를 권장합니다)
const MAX_DELEGATION_DEPTH = 5;
interface DelegationRequest {
task_id: string;
scope: string[];
deadline: string; // ISO 8601
expected_artifact: string;
sensitivity_level: "public" | "internal" | "confidential" | "restricted";
delegation_chain: string[]; // 현재까지의 위임 경로
}
interface DelegationResponse {
status: "accepted" | "rejected" | "counter_proposed";
accepted_scope?: string[]; // counter_proposed 시 수락 가능한 축소 범위
reason?: string;
}
async function requestDelegation(
fromAgent: string,
toAgent: string,
request: DelegationRequest,
): Promise<DelegationResponse> {
// 무한 위임 방지: 체인 깊이가 한계에 도달하면 거절
if (request.delegation_chain.length >= MAX_DELEGATION_DEPTH) {
return { status: "rejected", reason: "delegation_depth_exceeded" };
}
// 수신 에이전트의 권한 범위 내에서 처리 가능한지 확인
const agentCapabilities = await getAgentCapabilities(toAgent);
const feasibleScope = request.scope.filter(
(s) => agentCapabilities.includes(s),
);
if (feasibleScope.length === 0) {
return { status: "rejected", reason: "no_matching_capabilities" };
}
if (feasibleScope.length < request.scope.length) {
// 일부 범위만 처리 가능한 경우 역제안
return {
status: "counter_proposed",
accepted_scope: feasibleScope,
reason: "partial_capability_match",
};
}
// 위임 이벤트를 감사 로그에 기록
await auditLog({
action_type: "delegation",
actor: { sub: fromAgent, delegation_chain: request.delegation_chain },
resource: request.task_id,
timestamp: new Date().toISOString(),
});
return { status: "accepted", accepted_scope: request.scope };
}예시 3: OpenID Shared Signals를 활용한 취소 이벤트 전파
사용자가 최상위 에이전트 권한을 취소했을 때, 이미 발급된 하위 체인의 토큰까지 실시간으로 무효화하는 패턴입니다. 이 패턴의 핵심은 find_delegation_descendants 함수가 어떤 저장소에서 위임 계보를 조회하는지에 있습니다.
# revocation_propagation.py
import asyncio
from dataclasses import dataclass
# find_delegation_descendants 구현을 위해 필요한 데이터 구조:
# 각 Token Exchange 시점마다 위임 관계를 별도 저장소에 기록해야 합니다.
#
# Redis 예시:
# KEY: delegation_tree:{parent_agent_id}
# VALUE: SET of "{child_agent_id}:{issued_at}:{token_id}"
#
# PostgreSQL 예시:
# CREATE TABLE delegation_tree (
# parent_agent_id TEXT,
# child_agent_id TEXT,
# issued_at TIMESTAMPTZ,
# token_id TEXT PRIMARY KEY
# );
#
# 토큰 발급 시마다 이 테이블에 레코드를 삽입해야 취소 전파가 가능합니다.
@dataclass
class RevocationEvent:
subject: str # 취소된 주체 (사용자 또는 에이전트)
reason: str
trace_id: str
issued_before: str # 이 시각 이전 발급 토큰 모두 무효화 (ISO 8601)
async def propagate_revocation(event: RevocationEvent) -> None:
"""
OpenID Shared Signals Framework를 활용해
취소 이벤트를 체인 하위로 전파합니다.
"""
# 취소된 주체의 위임 체인에 포함된 모든 에이전트 조회
# (delegation_tree 저장소에서 BFS/DFS로 하위 노드를 순회합니다)
affected_agents = await find_delegation_descendants(
event.subject, event.issued_before
)
# 병렬로 모든 하위 에이전트 토큰 무효화
revocation_tasks = [
invalidate_agent_token(agent_id, event.trace_id)
for agent_id in affected_agents
]
results = await asyncio.gather(*revocation_tasks, return_exceptions=True)
# 실패한 취소 작업은 별도 큐에 저장해 재시도
failed = [
affected_agents[i]
for i, r in enumerate(results)
if isinstance(r, Exception)
]
if failed:
await queue_revocation_retry(failed, event)장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 최소 권한 원칙 | 각 에이전트에게 해당 작업에 필요한 최소 권한만 위임하고, 범위를 scope 파라미터로 명시적 제한 가능 |
| 완전한 감사 추적 | act 클레임 체인으로 "누가, 무엇을, 누구 대신, 언제" 실행했는지 토큰 자체에서 추적 가능 |
| 선택적 신뢰 강등 | 홉이 늘어날수록 신뢰 수준과 권한 범위를 점진적으로 축소하는 정책 설계 가능 |
| 취소 전파 | 상위 에이전트 접근 취소 시 파생 권한도 정책 엔진을 통해 연쇄 폐기 가능 |
| 표준 기반 상호운용성 | RFC 8693 같은 공개 표준 기반으로 서로 다른 벤더 에이전트 간 통합 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 위임 체인 접합 공격 | 손상된 중간 에이전트가 서로 다른 컨텍스트의 토큰을 조합해 STS를 속임 (CVE-2025-55241로 확인됨) | RFC 8707 리소스 인디케이터 의무화, 체인 바인딩 검증 |
| 오프라인 토큰 취소 실패 | 이미 배포된 오프라인 토큰은 실시간 무효화 불가 | OpenID Shared Signals로 취소 이벤트 전파, 토큰 TTL 단축 |
| 혼동된 대리인 문제 | 에이전트가 합법적 자격증명을 보유하더라도 프롬프트 인젝션으로 공격자 명령을 실행 | 도구 입력값의 신뢰 경계 구분, Human-in-the-Loop 게이트 추가 |
| 도구 설명 오염 | MCP 서버의 도구 메타데이터에 숨겨진 명령 삽입 가능 | 도구 설명을 신뢰할 수 없는 외부 입력으로 취급, 레지스트리 서명 검증 |
| 감사의 어려움 | 명시적 로깅 없이는 에이전트 의사결정 과정이 블랙박스 | IETF 초안 draft-sharif-agent-audit-trail에 따른 구조화 로그 의무화 |
용어 보충 혼동된 대리인(Confused Deputy): 에이전트가 합법적인 권한을 가지고 있음에도, 악의적인 프롬프트나 도구 입력을 통해 의도치 않은 권한 있는 행동을 수행하게 되는 취약점입니다. 에이전트 권한이 넓을수록 피해 범위가 커집니다.
실무에서 가장 흔한 실수
- API 키를 그대로 체인에 전달하는 방식 유지: 에이전트에게 서드파티 API 키를 직접 전달하면 키 노출 시 모든 에이전트가 위험에 처합니다. Auth0 Token Vault 같은 토큰 교환 서비스를 통해 실제 자격증명이 에이전트 코드에 닿지 않도록 하는 것을 권장합니다.
- 위임 깊이 제한 없이 재귀 위임 허용: 에이전트 A → B → C → A처럼 순환 위임이나 무한 깊이 위임이 가능하면 시스템 자원 고갈 및 보안 우회로 악용될 수 있습니다. 일반적으로
MAX_DELEGATION_DEPTH를 3~5 수준으로 설정하는 것이 좋습니다. - 취소 이벤트를 동기 처리로만 가정: 오프라인 에이전트나 캐시된 토큰이 있을 때 동기 취소만으로는 충분하지 않습니다. 취소 실패를 별도 큐에 기록하고 재시도하는 비동기 전파 메커니즘을 함께 설계하는 것이 안전합니다.
마치며
멀티에이전트 위임 패턴이 해결하는 핵심 문제는 하나입니다: "누가 누구를 대신해서 무엇을 했는가"를 에이전트 체인의 모든 계층에서 추적 가능하게 만드는 것. 보안 사고는 기술이 부족해서가 아니라, 이 추적 가능성이 설계 단계에서 빠졌을 때 발생합니다.
지금 바로 시작해볼 수 있는 3단계:
- 기존 에이전트 코드에서 자격증명 직접 전달 방식을 확인해보세요.
grep -r "api_key\|bearer_token" ./agents명령으로 현재 코드베이스에서 자격증명이 에이전트에 직접 노출되는 지점을 파악하고, RFC 8693 Token Exchange로 대체할 수 있는 우선순위를 정리해보세요. - 감사 로그에
trace_id와delegation_chain필드를 추가해보세요. 현재 로깅 시스템에 앞서 제시한AuditLogEntry구조의 핵심 필드 두 가지만 먼저 추가해도, 사고 발생 시 원인 추적 능력이 크게 향상됩니다. - MCP 2026-03-15 스펙의 리소스 인디케이터 요구사항을 점검해보세요. 공식 MCP 인가 문서에서 현재 스펙을 확인하고, 자신의 MCP 서버가 OAuth 2.1 리소스 서버로서
resource파라미터를 올바르게 검증하고 있는지 살펴보시면 좋습니다.
다음 글: 프롬프트 인젝션과 도구 설명 오염 공격으로부터 MCP 에이전트를 방어하는 실전 보안 하드닝 가이드
참고 자료
표준 문서
- OAuth 2.0 Token Exchange RFC 8693 | IETF
- Understanding Authorization in MCP | 공식 MCP 문서
- The New MCP Authorization Specification 2026-03-15 | dasroot.net
- What's New In The 2025-11-25 MCP Authorization Spec | Den Delimarsky
- draft-sharif-agent-audit-trail-00: Agent Audit Trail Standard | IETF Datatracker
- Delegation Chain Splicing Security Consideration | OAUTH-WG
- A Survey of Agent Interoperability Protocols: MCP, ACP, A2A, ANP | arXiv
구현 사례 및 도구
- Trusted On-Behalf-Of: Agent Delegation in Gravitee 4.11 | Gravitee
- Securing MCP with OIDC & OIDC-A: Identity-Aware API Gateways | Subramanya N
- Token Delegation and MCP server orchestration for multi-user AI systems | DEV Community
- End-User Identity Propagation in Agents | Salesforce Agentforce
- OAuth for MCP - Emerging Enterprise Patterns for Agent Authorization | GitGuardian
- AI Agents, the Model Context Protocol, and the Future of Authorization Guardrails | Cerbos
- Delegation in a Multi-Actor World: It's Not Just OAuth Anymore | Spherical Cow Consulting