MCP 게이트웨이를 Zero Trust PEP로 설계하는 법 — OAuth 2.1·OPA·에피머럴 토큰으로 최소 권한(Least Privilege) 구현하기
이 글의 전제 조건: JWT 인증과 REST API 개발 경험이 있는 분을 대상으로 합니다. OAuth 2.0 기본 플로우를 알고 있다면 더 수월하게 읽을 수 있습니다.
AI 에이전트가 데이터베이스를 조회하고, 파일을 수정하고, 외부 API를 호출하는 시대가 왔습니다. 실제로 어떤 일이 벌어지는지 생각해 보면 이미 답이 나옵니다. 2025년 중반, Privileged service-role 권한으로 실행되던 Supabase Cursor 에이전트가 지원 티켓에 삽입된 SQL 명령을 그대로 실행해 민감한 통합 토큰이 유출됐습니다. 최소 권한 원칙 없이 운용된 에이전트의 직접적인 결과였습니다. arXiv 2504.08623 연구에서 2,500개 이상의 실제 MCP 플러그인을 분석한 결과, 테스트된 구현체의 43%에서 커맨드 인젝션 결함이, 30%에서 무제한 URL 패칭이 발견됐습니다.
MCP(Model Context Protocol)는 2025년 Claude, OpenAI(2026년 3월 Sam Altman이 전면 지원 공식 발표), Google Gemini까지 채택하며 AI 에이전트 인프라의 사실상 표준으로 자리잡았습니다. 에코시스템이 확산될수록 보안 설계를 나중으로 미루면, 그만큼 더 많은 에이전트와 시스템이 취약한 상태로 운용됩니다.
이 글에서는 MCP 게이트웨이를 정책 집행 포인트(PEP, Policy Enforcement Point) 로 활용해 Zero Trust 최소 권한 아키텍처를 단계별로 구축하는 방법을 살펴봅니다. 아래가 이 글을 통해 완성하게 될 아키텍처의 핵심 흐름입니다.
[에이전트] → [MCP 게이트웨이 (PEP)]
↓ JWT 검증 → OPA 정책 평가 → 에피머럴 토큰 발급
→ [MCP Server A: DB]
→ [MCP Server B: File]
→ [MCP Server C: API]이 글을 읽고 나면 "에이전트가 필요한 도구만, 필요한 순간에만, 최소 범위로 사용하도록 강제하는" 게이트웨이를 직접 설계할 수 있게 됩니다.
핵심 개념
MCP 아키텍처의 세 계층 — Host와 Client는 왜 분리될까
MCP(Model Context Protocol)는 Anthropic이 2024년 오픈소스로 공개한 프로토콜입니다. AI 모델과 외부 시스템(데이터베이스, API, 파일시스템 등)을 표준화된 방식으로 연결하는 역할을 하며, "AI를 위한 USB-C 포트"로 자주 비유됩니다.
| 계층 | 역할 | 예시 |
|---|---|---|
| MCP Host | 사용자와 직접 상호작용하는 애플리케이션 | Claude Desktop, VS Code, Cursor |
| MCP Client | 호스트 내부에서 서버와 1:1 세션을 유지 | 내장 프로토콜 클라이언트 |
| MCP Server | 특정 도구나 데이터를 노출하는 경량 서비스 | DB 커넥터, 파일 서버, API 래퍼 |
Host와 Client가 분리되는 이유를 이해하는 것이 중요합니다. Host는 사용자 인터페이스와 전체 애플리케이션 생명주기를 담당하고, Client는 각 MCP 서버와 독립된 JSON-RPC 스테이트풀 세션을 유지합니다. Claude Desktop이 DB 서버·File 서버·API 서버 세 곳에 동시 연결한다면, 그 안에 Client 인스턴스가 세 개 존재하는 구조입니다. 이 1:1 세션 구조는 게이트웨이 수평 스케일링 설계에서 핵심 난제가 됩니다(스테이트풀 세션 문제는 장단점 분석에서 자세히 다룹니다).
프로토콜은 세 가지 원시 타입으로 LLM에 컨텍스트를 제공합니다. Tools(실행 가능한 함수), Resources(읽기 가능한 데이터), Prompts(재사용 가능한 템플릿)가 그것입니다.
MCP 게이트웨이가 필요한 이유
클라이언트가 MCP 서버 10개를 직접 연결한다면 각 서버마다 인증 방식이 다르고, 감사 로그는 흩어지며, 정책 변경 시 10곳을 모두 수정해야 합니다. MCP 게이트웨이는 AI 클라이언트와 여러 MCP 서버 사이에 위치하는 중간 계층으로, 모든 트래픽이 단일 진입점을 통과하도록 강제하고 라우팅·인증·정책 집행·관찰가능성을 중앙에서 처리합니다.
Zero Trust 최소 권한의 세 원칙
Zero Trust 정의: "절대 신뢰하지 말고, 항상 검증하라(Never Trust, Always Verify)". 네트워크 경계 내부에 있다는 이유만으로 신뢰를 부여하지 않는 보안 모델입니다.
AI 에이전트에 Zero Trust를 적용할 때는 세 가지 원칙으로 구체화됩니다.
- 신원 기반 검증: 모든 에이전트·사용자·서비스는 매 요청마다 신원을 증명해야 합니다
- 최소 권한(Least Privilege): 현재 작업에 필요한 최소 권한만 부여하고, 가능하면 시간 제한을 붙입니다
- 마이크로 세그멘테이션: 에이전트가 접근할 수 있는 도구와 데이터의 범위를 개별 태스크 단위로 좁힙니다. 실전 적용 예시 4의 동적 도구 필터링이 바로 이 원칙의 구현 패턴입니다
OAuth 2.1과 MCP 인증 표준(2025년 6월 확정)
2025년 6월, MCP 공식 스펙은 MCP 서버를 OAuth 2.0 Resource Server로 공식 분류했습니다.
| 스펙 | 내용 |
|---|---|
| RFC 8707 (Resource Indicators) | resource 파라미터에 동적 서버 URL을 담아 클라이언트가 전송. 이 토큰은 해당 MCP 서버에서만 유효하므로, 에이전트가 침해되어도 토큰이 다른 서버에서 재사용될 수 없습니다 |
| RFC 8693 (Token Exchange) | 광범위한 토큰을 좁은 스코프 토큰으로 교환하는 위임 패턴 |
| RFC 9728 (Protected Resource Metadata) | MCP 서버가 Authorization Server 위치를 자동으로 광고 |
에피머럴 토큰(Ephemeral Token) 정의: 단일 작업 또는 짧은 시간(보통 60~300초) 동안만 유효한 일회성 토큰입니다. 에이전트가 침해되더라도 피해 범위를 특정 작업·특정 시간으로 제한하는 핵심 수단입니다.
실전 적용
예시 1~4는 하나의 게이트웨이를 점진적으로 구성하는 레이어입니다. 예시 1의 PEP 미들웨어 위에 예시 2의 정책 파일을 올리고, 예시 3의 토큰 교환 로직을 인증 레이어에 통합하며, 예시 4의 동적 필터링이 도구 노출을 제어합니다.
[요청 진입]
↓
[예시 1] JWT 검증 + OPA 정책 평가 + 에피머럴 토큰 발급 ← 예시 2의 Rego 파일이 평가됨
↓
[예시 3] RFC 8693 토큰 교환 → 서버별 좁은 스코프 토큰 획득
↓
[예시 4] 사용자 역할 기반 도구 목록 필터링 (마이크로 세그멘테이션)
↓
[MCP Server로 프록시]예시 1: 인증·인가 설계 — PEP 미들웨어와 Fail-Closed 구현
모든 도구 호출 요청이 게이트웨이를 통과하며, OPA에 정책을 질의한 뒤 허용·거부를 결정합니다. OPA 질의 실패 시 반드시 기본 거부(Fail-Closed)로 처리하는 것이 Zero Trust의 핵심 원칙입니다.
먼저 verifyAgentJWT를 jsonwebtoken 라이브러리를 사용한 실제 구현으로 살펴봅니다.
// gateway/middleware/auth.ts
import jwt from 'jsonwebtoken';
interface AgentIdentity {
agentId: string;
role: string;
userId: string;
}
async function verifyAgentJWT(
authHeader: string | undefined,
): Promise<AgentIdentity | null> {
if (!authHeader?.startsWith('Bearer ')) {
return null;
}
const token = authHeader.slice(7);
const publicKey = process.env.AGENT_JWT_PUBLIC_KEY;
if (!publicKey) {
// 환경 변수 누락 시에도 Fail-Closed
console.error('AGENT_JWT_PUBLIC_KEY not configured');
return null;
}
try {
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: process.env.JWT_ISSUER,
}) as AgentIdentity & jwt.JwtPayload;
return {
agentId: decoded.agentId,
role: decoded.role,
userId: decoded.userId,
};
} catch {
// 토큰 위조·만료·서명 불일치 모두 null 반환 → 401
return null;
}
}이어서 OPA 질의 함수와 PEP 미들웨어입니다.
// gateway/middleware/policy-enforcement.ts
import { Request, Response, NextFunction } from 'express';
interface MCPToolCallRequest {
agentId: string;
userId: string;
toolName: string;
params: Record<string, unknown>;
}
interface PolicyDecision {
allowed: boolean;
reason?: string;
}
async function queryOPA(input: {
agent: AgentIdentity;
user: string;
tool: string;
params: Record<string, unknown>;
timestamp: number;
}): Promise<PolicyDecision> {
try {
const response = await fetch(
`${process.env.OPA_URL}/v1/data/mcp/tool_access`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input }),
},
);
if (!response.ok) {
// OPA 서버 오류 시 Fail-Closed: 정책 엔진 불가 → 기본 거부
console.error(`OPA returned ${response.status}`);
return { allowed: false, reason: 'policy engine unavailable' };
}
const data = await response.json();
return {
allowed: data.result?.allow ?? false,
reason: data.result?.deny_reason,
};
} catch (err) {
// 네트워크 오류도 Fail-Closed
console.error('OPA connection error:', err);
return { allowed: false, reason: 'policy engine unreachable' };
}
}
async function enforceMCPPolicy(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
const toolCall = req.body as MCPToolCallRequest;
// 1단계: 에이전트 신원 검증
const agentIdentity = await verifyAgentJWT(req.headers.authorization);
if (!agentIdentity) {
res.status(401).json({ error: 'Agent identity verification failed' });
return;
}
// 2단계: OPA 정책 질의 (Fail-Closed 포함)
const decision = await queryOPA({
agent: agentIdentity,
user: toolCall.userId,
tool: toolCall.toolName,
params: toolCall.params,
timestamp: Date.now(),
});
if (!decision.allowed) {
await auditLog({
event: 'tool_call_denied',
agentId: toolCall.agentId,
tool: toolCall.toolName,
reason: decision.reason,
});
res.status(403).json({ error: `Access denied: ${decision.reason}` });
return;
}
// 3단계: 에피머럴 토큰 발급 (단일 작업용, 60초 만료)
req.ephemeralToken = await mintEphemeralToken({
scope: `tool:${toolCall.toolName}`,
subject: toolCall.agentId,
expiresIn: '60s',
});
next();
}| 코드 위치 | 역할 |
|---|---|
verifyAgentJWT |
RS256 서명 검증. 위조·만료·설정 오류 모두 null 반환 |
queryOPA |
Fail-Closed: OPA 장애 시에도 기본 거부로 처리 |
mintEphemeralToken |
허가된 경우에만 단일 작업용 토큰 발급, 60초 후 만료 |
auditLog |
거부 이벤트도 구조화 로깅 — SOC 2·HIPAA 감사 증적의 기반 |
예시 2: 정책 설계 — OPA Rego로 최소 권한 표현
앞의 PEP 미들웨어의 queryOPA()가 이 정책 파일을 평가합니다. 정책을 코드로 정의(Policy-as-Code)하면 Git 버전 관리와 코드 리뷰가 가능해집니다.
# policies/mcp/tool_access.rego
package mcp.tool_access
import future.keywords.if
import future.keywords.in
default allow = false
# 읽기 전용 역할은 읽기 도구만 허용
allow if {
input.agent.role == "readonly"
input.tool in readonly_tools
}
# 쓰기 역할은 읽기 + 쓰기 도구 허용 (관리 도구는 제외)
allow if {
input.agent.role == "readwrite"
input.tool in allowed_write_tools
not is_admin_tool(input.tool)
}
# 시간 기반 제한: 업무 시간(KST 09:00-18:00)에만 쓰기 허용
allow if {
input.agent.role == "readwrite"
business_hours_kst
}
readonly_tools := {
"database_query",
"file_read",
"metrics_fetch",
}
allowed_write_tools := readonly_tools | {
"file_write",
"database_insert",
}
is_admin_tool(tool) if {
tool in {"database_drop", "user_delete", "config_override"}
}
business_hours_kst if {
# time.now_ns()는 UTC 기준 나노초를 반환합니다
# KST(UTC+9) 변환: UTC 시(hour)에 9를 더해 24로 나눕니다
utc_hour := time.clock(time.now_ns())[0]
kst_hour := (utc_hour + 9) % 24
kst_hour >= 9
kst_hour < 18
}
# 거부 사유를 함께 반환
deny_reason := "readonly role cannot use write tools" if {
input.agent.role == "readonly"
input.tool in allowed_write_tools
}
deny_reason := "admin tools require explicit approval" if {
is_admin_tool(input.tool)
}
deny_reason := "write tools only available during business hours (KST 09-18)" if {
input.agent.role == "readwrite"
not business_hours_kst
}타임존 주의:
time.now_ns()는 UTC를 반환합니다. 한국 업무 시간 기준으로 정책을 작성할 때 KST(UTC+9) 변환을 빠뜨리면, 실제 09:00 KST에도utc_hour는 0이 되어 정책이 조용히 오작동합니다.(utc_hour + 9) % 24변환은 필수입니다.
예시 3: 토큰 관리 — RFC 8693 스코프 좁히기
앞의 PEP 미들웨어에서 신원 검증 후, 인증 레이어는 광범위한 액세스 토큰을 특정 MCP 서버·특정 작업에만 유효한 토큰으로 교환합니다. 토큰을 좁혀두면 에이전트가 침해되어도 피해 범위가 해당 서버와 도구로만 제한됩니다.
이 예시에서 Python을 사용하는 이유는, Red Hat·Auth0 등 주요 인증 서버 SDK가 Python 예제 위주로 제공되어 인증 레이어 통합에 Python이 흔하게 쓰이기 때문입니다. 게이트웨이의 HTTP 레이어(TypeScript)와 인증·시크릿 레이어(Python)를 분리하는 것은 각 생태계의 라이브러리 성숙도 차이를 반영한 현실적인 선택입니다.
# gateway/auth/token_exchange.py
import os
import httpx
from dataclasses import dataclass
@dataclass
class NarrowedToken:
access_token: str
scope: str
expires_in: int
target_server: str
async def exchange_for_narrow_token(
broad_token: str,
target_mcp_server: str,
required_tool: str,
) -> NarrowedToken:
"""
RFC 8693 Token Exchange: 광범위한 토큰 → 특정 서버·도구 전용 토큰
RFC 8707: resource 파라미터에 동적 서버 URL을 담아 전송하므로,
이 토큰은 target_mcp_server 외 다른 서버에서 사용될 수 없습니다
resource 값을 하드코딩하면 이 바인딩 보장이 깨지므로 주의하세요
"""
auth_server_url = os.environ["AUTH_SERVER_URL"]
async with httpx.AsyncClient() as client:
try:
response = await client.post(
url=f"{auth_server_url}/oauth/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": broad_token,
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
# RFC 8707: 동적 서버 URL 바인딩 — 절대 하드코딩 금지
"resource": f"https://mcp.example.com/servers/{target_mcp_server}",
"scope": f"tool:{required_tool}",
"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
},
timeout=5.0,
)
response.raise_for_status()
except httpx.HTTPStatusError as e:
# 토큰 교환 실패도 Fail-Closed
raise RuntimeError(
f"Token exchange failed: {e.response.status_code}"
) from e
except httpx.RequestError as e:
raise RuntimeError(f"Auth server unreachable: {e}") from e
data = response.json()
return NarrowedToken(
access_token=data["access_token"],
scope=data["scope"],
expires_in=data["expires_in"], # 보통 60~300초
target_server=target_mcp_server,
)
async def get_secret_for_legacy_server(
server_name: str,
vault_token: str, # 전역 변수 대신 함수 파라미터로 전달 — 보안 필수
) -> str:
"""
OAuth2를 지원하지 않는 레거시 MCP 서버용:
HashiCorp Vault에서 동적으로 단기 시크릿을 검색합니다
"""
vault_url = os.environ["VAULT_URL"]
async with httpx.AsyncClient() as client:
try:
response = await client.get(
url=f"{vault_url}/v1/secret/mcp/{server_name}",
headers={"X-Vault-Token": vault_token},
timeout=3.0,
)
response.raise_for_status()
except httpx.HTTPStatusError as e:
raise RuntimeError(
f"Vault lookup failed: {e.response.status_code}"
) from e
return response.json()["data"]["api_key"]예시 4: 동적 도구 필터링 — 역할 기반 마이크로 세그멘테이션
앞의 PEP 미들웨어와 정책 레이어 위에서, 에이전트가 세션을 시작할 때 허가된 도구 목록을 동적으로 결정합니다. 이것이 Zero Trust의 마이크로 세그멘테이션 원칙을 도구 레이어에서 구현하는 방식입니다. 이 계층은 게이트웨이의 메인 HTTP 레이어(예시 1과 동일한 Express 미들웨어 스택)이므로 TypeScript를 사용합니다.
// gateway/tools/dynamic-filter.ts
interface UserContext {
userId: string;
roles: string[];
department: string;
}
interface MCPTool {
name: string;
description: string;
requiredRoles: string[];
sensitivityLevel: 'low' | 'medium' | 'high';
}
const ALL_MCP_TOOLS: MCPTool[] = [
{
name: 'database_query',
description: 'Read-only SQL query',
requiredRoles: ['analyst', 'engineer'],
sensitivityLevel: 'low',
},
{
name: 'database_insert',
description: 'Insert rows into database',
requiredRoles: ['engineer'],
sensitivityLevel: 'medium',
},
{
name: 'payment_process',
description: 'Process payment transaction',
requiredRoles: ['payment_admin'],
sensitivityLevel: 'high',
},
{
name: 'user_delete',
description: 'Delete user account',
requiredRoles: ['super_admin'],
sensitivityLevel: 'high',
},
];
async function checkHighSensitivityApproval(
toolName: string,
userContext: UserContext,
): Promise<boolean> {
// 고위험 도구별 허용 부서 목록 (마이크로 세그멘테이션)
const sensitiveToolDepartments: Record<string, string[]> = {
payment_process: ['finance', 'treasury'],
user_delete: ['platform-ops'],
};
const allowedDepts = sensitiveToolDepartments[toolName];
if (allowedDepts && !allowedDepts.includes(userContext.department)) {
return false;
}
// 고위험 도구는 KST 업무 시간(09:00-18:00) 외 차단
const kstHour = (new Date().getUTCHours() + 9) % 24;
if (kstHour < 9 || kstHour >= 18) {
return false;
}
return true;
}
async function getPermittedTools(userContext: UserContext): Promise<MCPTool[]> {
// 세션 시작 시 기본 활성 도구 = 없음 (Zero Trust 원칙)
const permittedTools: MCPTool[] = [];
for (const tool of ALL_MCP_TOOLS) {
const hasRequiredRole = tool.requiredRoles.some((role) =>
userContext.roles.includes(role),
);
if (!hasRequiredRole) continue;
if (tool.sensitivityLevel === 'high') {
const approved = await checkHighSensitivityApproval(tool.name, userContext);
if (!approved) continue;
}
permittedTools.push(tool);
}
// 허가된 도구만 반환 — 존재하지 않는 도구는 에이전트에 보이지 않음
return permittedTools;
}도구 필터링의 보안 원칙: 단순히 호출을 막는 것이 아니라, 허가되지 않은 도구는 목록에서 아예 제거하는 것이 중요합니다. LLM은 도구 이름만 보고도 추측 기반 호출을 시도할 수 있기 때문입니다. 이것이 마이크로 세그멘테이션의 핵심입니다.
도구 포이즈닝(Tool Poisoning) 정의: 악의적인 MCP 서버가 도구 설명(description) 필드에 숨겨진 명령어를 삽입하는 공격입니다. LLM은 도구 메타데이터를 신뢰할 수 있는 지시어로 간주하기 때문에, 서버에서 받은 도구 메타데이터를 클라이언트 측에서 반드시 검증해야 합니다. 클라이언트 측 메타데이터 검증이 MCP 스펙에서 의무가 아닌 것이 현재 가장 큰 구조적 약점입니다.
방어를 위한 최소한의 메타데이터 검증 패턴입니다. 프로덕션에서는 이 규칙 기반 검사에 더해 LLM 기반 인젝션 탐지 모델을 함께 통합하는 것이 좋습니다.
// gateway/tools/metadata-validation.ts
function validateToolMetadata(tool: MCPTool): boolean {
const suspiciousPatterns = [
/ignore previous instructions/i,
/system:/i,
/<\|.*\|>/, // 특수 토큰 패턴
/\[INST\]/i, // 인젝션 마커
];
return !suspiciousPatterns.some((pattern) =>
pattern.test(tool.description),
);
}장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 중앙화된 정책 관리 | 모든 MCP 트래픽이 단일 PEP를 통과하므로, 보안 정책 변경이 즉시 전체 에이전트에 적용됩니다 |
| 완전한 감사 추적 | 어떤 에이전트가, 어떤 도구를, 어떤 파라미터로 호출했는지 타임스탬프·결과까지 구조화 로깅이 가능합니다. SOC 2 Type II의 접근 제어 증적, GDPR의 처리 활동 기록(Art. 30), HIPAA의 접근 감사 로그 요건을 이 구조화 로그로 충족할 수 있습니다 |
| 폭발 반경 제한 | 에피머럴 스코프 토큰으로 에이전트 침해 시 피해 범위를 특정 작업·특정 시간으로 제한합니다 |
| 동적 도구 필터링 | 사용자 컨텍스트·역할·정책에 따라 런타임에 노출 도구 집합을 변경할 수 있습니다 |
| 프로토콜 표준화 | 새 MCP 서버는 게이트웨이에 등록만 하면 보안 정책을 자동으로 상속받습니다 |
| 낮은 레이턴시 오버헤드 | AWS 측정 기준 평균 4.47ms 추가 지연으로, 운영 부담이 미미합니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 단일 장애점(SPOF) | 게이트웨이가 다운되면 모든 에이전트 기능이 중단됩니다 | 고가용성(HA) 배포, 서킷 브레이커 패턴, 재시도 로직 적용 |
| 스테이트풀 세션 복잡성 | MCP는 JSON-RPC 기반 스테이트풀 세션을 사용합니다. SSE(Server-Sent Events) 전송 방식은 HTTP 연결을 장시간 유지하므로, 로드 밸런서가 요청을 다른 인스턴스로 분산하면 세션이 끊깁니다. HTTP Streamable 방식으로 전환하면 일부 완화되지만 Sticky Session 또는 외부 세션 저장소가 필요합니다 | Session Affinity(Sticky Session) 또는 Redis 기반 외부 세션 저장소 설계 |
| 공급망 위험 | MCP 서버 자체가 신뢰할 수 없는 코드일 수 있습니다(CVE-2025-6514: mcp-remote OS 명령 인젝션, 다운로드 43만 건 이상) | 서버 서명 검증, SCA(Software Composition Analysis) 파이프라인 도입 |
| 프롬프트 인젝션·도구 포이즈닝 | 악의적인 서버가 도구 설명에 숨겨진 명령을 삽입할 수 있습니다. 클라이언트 측 메타데이터 검증이 스펙에서 의무가 아닌 것이 현재 가장 큰 구조적 약점입니다 | 메타데이터 검증(예시 4 참조), LLM 기반 인젝션 탐지 모델 통합 |
| 구현 복잡도 | OAuth 2.1 + Resource Indicators + 동적 토큰 교환의 조합은 학습 곡선이 가파릅니다 | 레거시 서버는 HashiCorp Vault로 PAT 관리, 단계적 마이그레이션 권장 |
실무에서 가장 흔한 실수
- 장기 API 키를 에이전트에 직접 주입하는 것: 가장 흔하고 치명적인 실수입니다. 2025년 Supabase Cursor 에이전트 침해 사고의 직접적 원인이었습니다. 에이전트에는 작업 단위 에피머럴 토큰만 전달하고, 장기 시크릿은 Vault에서 게이트웨이가 중개하는 방식이 좋습니다.
- 정책 엔진만으로 시맨틱 위협을 막으려는 것: OPA/Cedar는 구조화된 권한 결정에는 강력하지만, 프롬프트 인젝션·PII 유출 같은 시맨틱 공격은 감지하지 못합니다. 게이트웨이 레이어에 ML 기반 인젝션 탐지와 DLP(Data Loss Prevention)를 함께 통합하는 것이 좋습니다.
- 정책을 지나치게 세분화하여 운영 복잡도를 폭발시키는 것: 도구마다, 환경마다, 사용자마다 별도 정책을 만들면 관리가 불가능해집니다. 역할 기반 정책 템플릿으로 추상화하고, 예외는 오버라이드 방식으로 처리하는 구조가 현실적입니다.
마치며
MCP 게이트웨이에 Zero Trust를 적용한다는 것은 단순한 보안 레이어 추가가 아니라, 에이전트가 "무엇을 할 수 있는지"의 경계를 코드로 명시하는 아키텍처적 결정입니다.
지금 바로 시작해볼 수 있는 3단계입니다.
- 감사 로그부터 활성화하는 것이 좋습니다 (1주일 이내 가능):
agentgateway또는mcp-oauth-gateway를 기존 스택 앞단에 배치하고, 모든 도구 호출을 구조화된 형식으로 수집해 보시면 됩니다. "어떤 에이전트가 무엇을 얼마나 호출하는가"를 파악하는 것이 Zero Trust 설계의 출발점이 됩니다. - OPA 또는 Cerbos로 최소 권한 정책을 코드로 표현해 보실 수 있습니다 (첫 버전 하루면 충분합니다): "read-only 역할은 read 도구만" 같은 가장 단순한 정책부터 시작해, 로그에서 실제 호출 패턴을 보며 정교화해 가는 접근이 효과적입니다. 정책 파일은 Git으로 관리해 리뷰와 변경 이력을 남기는 것을 권장합니다.
- 장기 API 키를 에피머럴 토큰으로 교체하는 마이그레이션 계획을 세워보실 수 있습니다: OAuth 2.1을 지원하는 서버는 RFC 8693 토큰 교환 패턴으로, 레거시 서버는 HashiCorp Vault의 동적 시크릿 기능으로 단계적으로 전환할 수 있습니다. 한 번에 전부 바꾸기보다, 가장 민감한 도구부터 우선순위를 두어 진행하는 것이 현실적입니다.
이 시리즈를 계속 받아보시려면 블로그를 구독해 두시면 새 글이 올라올 때 알림을 받으실 수 있습니다.
다음 글: MCP 멀티에이전트 환경에서의 위임(Delegation) 패턴 — 에이전트가 다른 에이전트에게 권한을 넘길 때 토큰 전파와 책임 추적을 어떻게 설계할지 살펴봅니다.
참고 자료
입문용
- Model Context Protocol 공식 사이트 - Security Best Practices
- MCP and Zero Trust: Securing AI Agents With Identity and Policy | Cerbos
- The Agentic Trust Framework: Zero Trust Governance for AI Agents | CSA
- New tools and guidance: Announcing Zero Trust for AI | Microsoft Security Blog
구현 레퍼런스
- Advanced authentication and authorization for MCP Gateway | Red Hat Developer
- MCP Spec Updates from June 2025 - All About Auth | Auth0
- MCP Authorization the Easy Way – agentgateway
- agentgateway GitHub
- mcp-oauth-gateway GitHub
보안 심화
- Enterprise-Grade Security for the Model Context Protocol | arXiv 2504.08623
- MCP Security Vulnerabilities: How to Prevent Prompt Injection and Tool Poisoning Attacks | Practical DevSecOps
- OWASP GenAI - A Practical Guide for Securely Using Third-Party MCP Servers
- Building Zero-Trust Tooling for MCP: Interceptors, Scopes, and Policy-as-Code | Medium
- Zero Trust for autonomous agentic AI systems | Red Hat Emerging Technologies