OPA로 구축하는 LLM 에이전트 도구 접근 제어 — 런타임 컨텍스트 기반 동적 정책 엔진 설계
"이 도구를 지금 이 사용자가 호출해도 되는가?"
LLM 에이전트가 외부 도구를 자유롭게 호출하는 MCP(Model Context Protocol) 환경에서 이 질문에 답하기 위해, 많은 팀이 OAuth 2.0 스코프를 그대로 적용합니다. MCP는 Anthropic이 2024년 11월 발표한 개방형 표준으로, LLM이 파일 시스템·데이터베이스·외부 API 같은 도구와 통신하는 방식을 표준화합니다. 그런데 표준 명세 자체는 정책 집행(Policy Enforcement)을 애플리케이션 레이어에 위임합니다. 이 구조적 공백 때문에, 정적인 OAuth 스코프만으로는 "야간 00:00~06:00에는 금융 조회를 차단"하거나 "분석가는 민감도 3 미만 데이터만 접근 가능"처럼 현실적인 요건을 충족하지 못합니다. 현재 사용자의 역할, 호출 시각, 데이터 민감도, 에이전트 식별자 같은 런타임 컨텍스트가 정책 결정 과정에 빠져 있기 때문입니다.
같은 구조적 결함에서 더 심각한 위협도 자라납니다. MCP 생태계에서 Tool Squatting(도구 사칭)과 Rug Pull(승인 후 도구 정의 교체) 같은 공격 벡터가 2025년부터 MITRE ATLAS에 공식 등재될 만큼 실질적인 위험으로 부상했습니다. 정적 스코프가 런타임 컨텍스트를 반영하지 못한다는 문제와, 도구 정의 자체가 사칭·교체될 수 있다는 문제는 모두 "검증 책임을 외부 레이어에 넘기지 않는다"는 MCP 명세의 동일한 설계 결정에서 비롯됩니다.
이 글에서는 ETDI(Enhanced Tool Definition Interface)로 도구 정의의 신뢰성을 정적으로 보장하고, OPA(Open Policy Agent)로 런타임 컨텍스트를 동적으로 평가하는 이중 방어 아키텍처를 설계하고 구현하는 방법을 살펴봅니다. 이 글을 읽고 나면 Rego 정책 작성, FastMCP 훅 연동, JWT 기반 도구 서명 검증, 캐싱 전략을 실제 프로젝트에 적용할 수 있습니다.
핵심 개념
MCP 도구 호출의 보안 공백
MCP는 LLM이 외부 세계와 소통하는 "USB 허브" 역할을 합니다. LLM이 query_database 같은 도구를 호출하면, MCP 서버가 실제 데이터베이스와 연결해 결과를 반환합니다. 문제는 "어떤 사용자가 어떤 조건에서 이 도구를 호출할 수 있는가"에 대한 표준화된 답이 MCP 명세에 없다는 점입니다.
기존 OAuth 2.0 스코프 방식의 한계:
| 상황 | OAuth 스코프 방식 | 런타임 컨텍스트 필요 여부 |
|---|---|---|
| 야간 00:00~06:00 금융 조회 차단 | 불가 (시간 정보 없음) | 필요 |
| 분석가가 민감도 3 이상 데이터 차단 | 불가 (데이터 속성 없음) | 필요 |
| 특정 에이전트 버전 이후 도구 접근 차단 | 불가 (에이전트 식별 없음) | 필요 |
| 프롬프트 인젝션 의심 세션 차단 | 불가 (세션 상태 없음) | 필요 |
여기에 더해 ETDI 논문(arXiv:2506.01333)이 지적한 두 가지 구조적 공격이 있습니다.
Tool Squatting: 정상 도구(
query_database)와 동일한 이름을 사용하는 악성 도구를 MCP 서버에 등록해, LLM이 구분하지 못하도록 사칭하는 공격.Rug Pull: 사용자가 초기 승인한 안전한 도구 정의를 배포 후 악의적으로 교체하는 공격. 재승인 없이 동작하므로 탐지가 어렵습니다.
OPA — 정책 결정과 집행의 분리
OPA는 CNCF 졸업 프로젝트로, 애플리케이션 코드에서 정책 로직을 완전히 분리하는 범용 정책 엔진입니다. 핵심 모델은 단순합니다.
Input(JSON) + Policy(Rego) + Data(JSON) → Decision(allow/deny)서비스는 현재 컨텍스트를 JSON으로 OPA에 보내고, OPA는 Rego로 작성된 정책을 평가해 allow/deny를 반환합니다. 코드 재배포 없이 Rego 파일만 교체하면 보안 정책이 즉시 반영됩니다.
Rego 읽는 법: Rego는 SQL처럼 "무엇을 원하는지"를 선언하는 언어입니다.
allow if { 조건들 }은 모든 조건이 참일 때allow가true가 됩니다. 여러allow규칙이 있으면 그 중 하나라도 참이면 허용(OR 결합)됩니다.default allow := false는 조건을 충족하지 못한 모든 경우를 기본값 차단으로 처리합니다.
아래 Rego 예시는 역할·시간·데이터 민감도·에이전트 신뢰 여부를 복합 평가하는 정책입니다.
# policy/mcp_tool_access.rego
package mcp.tool.access
import future.keywords.if
import future.keywords.in
default allow := false
# 기본 허용 조건: 아래 규칙을 모두 충족해야 함 (AND)
allow if {
valid_role
within_working_hours
data_sensitivity_ok
trusted_agent
}
# 역할 검증
valid_role if {
input.principal.role in {"analyst", "engineer", "admin"}
}
# 근무 시간 검증 (UTC 기준 00:00~17:00)
# 주의: env.time은 서버가 UTC로 직접 채워야 합니다.
# 서버 로컬 타임존이 UTC가 아닌 경우 time.now_ns()를 서버에서 직접 주입하는 방식을 권장합니다.
within_working_hours if {
hour := time.clock(time.parse_rfc3339_ns(input.env.time))[0]
hour >= 0
hour < 17
}
# 데이터 민감도 검증 (analyst는 민감도 3 미만만 허용)
data_sensitivity_ok if {
input.principal.role == "analyst"
input.tool.data_sensitivity < 3
}
data_sensitivity_ok if {
input.principal.role in {"engineer", "admin"}
}
# 신뢰할 수 있는 에이전트 검증 (data.trusted_agents는 외부 JSON으로 관리)
trusted_agent if {
input.client.agent_id in data.trusted_agents
}Policy Decision Point(PDP): 정책 "결정"을 담당하는 컴포넌트. 결정을 실제로 "집행"하는 Policy Enforcement Point(PEP)와 분리하면, 정책 로직을 중앙화하고 여러 서비스에서 재사용할 수 있습니다.
ETDI — 도구 정의의 암호화 신뢰 체인
ETDI는 세 가지 기둥으로 Tool Squatting과 Rug Pull을 방어합니다.
1. 암호화 서명: 도구 제공자가 RSA 개인 키로 도구 정의에 서명하고, 클라이언트는 공개 키로 검증합니다. 서명은 JWT(JSON Web Token) 형식으로 전달됩니다.
2. 불변 버전 관리: 도구 정의·코드·권한 변경 시 새 버전 발급과 사용자 재승인이 필수입니다.
3. JWT 기반 권한 범위: 서명된 JWT로 도구의 권한 스코프를 명시합니다.
JWT 서명 구조: JWT는
Header.Payload.Signature세 파트로 구성됩니다. Header(알고리즘 명시)와 Payload(클레임)를 Base64로 인코딩한 뒤, 개인 키로 서명한 값이 세 번째 파트(Signature)로 붙습니다. 서명은 페이로드 안에 별도 필드로 들어가는 것이 아니라 JWT 구조 자체에 포함됩니다.
# ETDI 도구 정의 — JWT 페이로드 (decoded 상태)
{
"tool_id": "query_database_v2",
"provider": "internal-data-team",
"version": "2.1.0",
"permissions": ["db:read", "schema:read"],
"issued_at": "2026-04-14T00:00:00Z"
}
# 실제 전송 형태 (Header.Payload.Signature 포함):
# eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b29sX2lkIjoicXVlcnlfZGF0YWJhc2VfdjIiLC4uLn0.<서명>실전 적용
예시 1~4는 하나의 완성된 아키텍처를 구성합니다. 예시 1(OPA 연동 훅)이 기본 정책 집행 레이어를 만들고, 예시 2(ETDI 검증)가 그 앞단에 도구 신뢰성 검증을 추가하며, 예시 3(캐싱)이 고빈도 환경에서 지연을 줄이고, 예시 4(통합 입력 구조)가 OPA에 전달할 컨텍스트 스키마를 완성합니다.
전체 흐름은 아래와 같습니다.
[LLM Agent]
↓ tool_call 요청
[MCP Host/Broker]
↓ ① ETDI 서명 검증 (정적 레이어)
├─ JWT 서명 유효성 확인
├─ 버전 불변성 확인 (Rug Pull 탐지)
└─ 제공자 신원 확인 (Tool Squatting 탐지)
└─ 검증 실패 → deny + 보안 로그
↓ ② OPA PDP 쿼리 (동적 레이어)
├─ 사용자 역할·속성 평가
├─ 시간·환경 컨텍스트 평가
└─ 에이전트 식별자 평가
└─ deny → 차단 + 감사 로그
↓ allow
[MCP Server] → 도구 실행예시 1: FastMCP + OPA 사이드카 연동
FastMCP의 도구 호출 전 OPA HTTP API를 쿼리하는 패턴입니다. OPA를 사이드카로 동일 호스트에 배포하면 네트워크 지연을 최소화할 수 있습니다.
# mcp_server.py
import httpx
from datetime import datetime, timezone
from fastmcp import FastMCP, Context
from fastmcp.exceptions import ToolError
mcp = FastMCP("secure-data-server")
OPA_URL = "http://localhost:8181/v1/data/mcp/tool/access/allow"
async def check_policy(
principal: dict,
client: dict,
tool_name: str,
tool_metadata: dict,
args: dict,
) -> bool:
"""OPA PDP에 정책 평가 요청을 보냅니다."""
input_payload = {
"input": {
"principal": principal,
"client": client,
"tool": {
"name": tool_name,
**tool_metadata,
},
"args": args,
"env": {
# 환경 정보는 반드시 서버 측에서 직접 채워 넣어야 합니다.
# 클라이언트/에이전트가 제공한 값을 그대로 신뢰하면 정책 우회가 가능합니다.
"time": datetime.now(timezone.utc).isoformat(),
"network": "internal",
},
}
}
async with httpx.AsyncClient(timeout=0.5) as http: # 500ms 타임아웃
try:
resp = await http.post(OPA_URL, json=input_payload)
resp.raise_for_status()
return resp.json().get("result", False)
except (httpx.TimeoutException, httpx.HTTPError):
# OPA 장애 시 허용이 아닌 거부로 폴백 — deny-safe 원칙
return False
@mcp.tool()
async def query_financial_records(
table: str,
filter_expr: str,
ctx: Context, # FastMCP가 Context 객체를 자동 주입합니다
) -> dict:
"""금융 레코드를 조회합니다."""
# Context에서 호출자 정보를 추출하는 방법은 MCP 서버 구현에 따라 다릅니다.
# 아래는 인증 미들웨어에서 ctx에 주입된 값을 사용하는 예시입니다.
principal = getattr(ctx, "principal", {"role": "unknown"})
client_info = getattr(ctx, "client", {"agent_id": "unknown"})
allowed = await check_policy(
principal=principal,
client=client_info,
tool_name="query_financial_records",
tool_metadata={"server": "postgres-mcp", "data_sensitivity": 3},
args={"table": table, "filter": filter_expr},
)
if not allowed:
raise ToolError(
"접근이 거부되었습니다. 현재 컨텍스트에서 이 도구를 사용할 권한이 없습니다.",
code="POLICY_DENIED",
)
# 실제 DB 조회 로직
return {"records": [], "total": 0}# pyproject.toml — 최소 의존성
[project]
dependencies = [
"fastmcp>=2.0",
"httpx>=0.27",
"pyjwt[crypto]>=2.8",
"cryptography>=41.0",
]| 코드 부분 | 설명 |
|---|---|
OPA_URL |
OPA의 특정 정책 경로를 직접 쿼리합니다. /v1/data/{패키지 경로}/allow |
timeout=0.5 |
500ms 초과 시 즉시 실패 처리. 에이전트 응답성 보호 |
return False (폴백) |
OPA 장애 시 허용이 아닌 거부로 폴백 — deny-safe 원칙 |
env.time 서버 직접 주입 |
시간 정보를 서버에서 UTC로 채워 컨텍스트 조작을 방지 |
ctx: Context |
FastMCP의 표준 컨텍스트 주입 방식 (_ctx는 비공개 파라미터로 처리되어 주입 대상에서 제외됨) |
예시 2: ETDI 서명 검증 미들웨어
OPA 쿼리 전 단계에서 도구 JWT의 서명을 검증하는 Python 미들웨어입니다.
RSA 서명 검증: 제공자는 RSA 개인 키로 JWT를 서명하고, 검증자는 해당 제공자의 공개 키(PEM 형식)로 서명이 위조되지 않았는지 확인합니다.
algorithms=["RS256"]은 SHA-256 기반 RSA 서명 알고리즘을 지정합니다. PEM 형식은-----BEGIN PUBLIC KEY-----로 시작하는 텍스트 인코딩 방식입니다.
# etdi_verifier.py
import jwt
from cryptography.hazmat.primitives import serialization
from dataclasses import dataclass
@dataclass
class ETDIToolDefinition:
tool_id: str
provider: str
version: str
permissions: list[str]
raw_token: str # 원본 JWT 문자열 (Header.Payload.Signature)
class ETDIVerificationError(Exception):
pass
class ETDIVerifier:
def __init__(self, trusted_providers: dict[str, str]):
"""
trusted_providers: {provider_id: PEM 형식 공개 키 문자열}
"""
# cryptography 41+ 에서는 backend 인자 없이 직접 호출합니다.
self._keys = {
pid: serialization.load_pem_public_key(pem.encode())
for pid, pem in trusted_providers.items()
}
# 버전 레지스트리: {tool_id: 승인된 버전}
# 프로덕션에서는 DB나 Redis에 영구 저장하고 재시작 시 복원해야 합니다.
self._approved_versions: dict[str, str] = {}
def verify(self, token: str) -> ETDIToolDefinition:
"""JWT 서명을 검증하고 ETDIToolDefinition을 반환합니다."""
# 1단계: 헤더에서 제공자 ID 추출 (서명 검증 전 미리 확인)
unverified = jwt.decode(token, options={"verify_signature": False})
provider_id = unverified.get("provider")
if provider_id not in self._keys:
raise ETDIVerificationError(
f"알 수 없는 제공자: {provider_id}. Tool Squatting 가능성이 있습니다."
)
# 2단계: 공개 키로 서명 검증 (JWT 위조·변조 탐지)
try:
payload = jwt.decode(
token,
key=self._keys[provider_id],
algorithms=["RS256"],
)
except jwt.InvalidSignatureError as e:
raise ETDIVerificationError("도구 서명 검증 실패: Rug Pull 공격 의심") from e
tool_id = payload["tool_id"]
current_version = payload["version"]
# 3단계: 버전 불변성 확인 (Rug Pull 탐지)
if tool_id in self._approved_versions:
approved = self._approved_versions[tool_id]
if approved != current_version:
raise ETDIVerificationError(
f"도구 버전 불일치 — 승인된 버전: {approved}, "
f"현재 버전: {current_version}. 재승인이 필요합니다."
)
return ETDIToolDefinition(
tool_id=tool_id,
provider=provider_id,
version=current_version,
permissions=payload.get("permissions", []),
raw_token=token,
)
def approve_version(self, tool_id: str, version: str) -> None:
"""사용자가 특정 버전을 명시적으로 승인합니다."""
self._approved_versions[tool_id] = version예시 3: OPA 정책 결정 캐싱
고빈도 도구 호출 환경에서 매 호출마다 OPA REST API를 쿼리하면 지연이 누적됩니다. TTL 기반 결정 캐시를 추가해 성능을 개선할 수 있습니다.
# policy_cache.py
import asyncio
import hashlib
import json
import time
from typing import Any
class PolicyDecisionCache:
"""OPA 정책 결정 결과를 TTL 기반으로 캐싱합니다."""
def __init__(self, default_ttl: float = 30.0):
self._cache: dict[str, tuple[bool, float]] = {}
self._default_ttl = default_ttl
self._lock = asyncio.Lock()
def _make_key(self, input_data: dict[str, Any]) -> str:
serialized = json.dumps(input_data, sort_keys=True)
return hashlib.sha256(serialized.encode()).hexdigest()
async def get(self, input_data: dict[str, Any]) -> bool | None:
key = self._make_key(input_data)
async with self._lock:
if key in self._cache:
result, expires_at = self._cache[key]
if time.monotonic() < expires_at:
return result
del self._cache[key]
return None # 캐시 미스
async def set(
self,
input_data: dict[str, Any],
decision: bool,
ttl: float | None = None,
) -> None:
key = self._make_key(input_data)
expires_at = time.monotonic() + (ttl or self._default_ttl)
async with self._lock:
self._cache[key] = (decision, expires_at)
async def invalidate_all(self) -> None:
"""정책 번들 갱신 시 전체 캐시를 무효화합니다."""
async with self._lock:
self._cache.clear()# opa_client.py — 캐시를 활용한 OPA 클라이언트
import httpx
from policy_cache import PolicyDecisionCache
class OPAClient:
def __init__(
self,
opa_url: str = "http://localhost:8181",
cache_ttl: float = 30.0,
):
self._base_url = opa_url
self._cache = PolicyDecisionCache(default_ttl=cache_ttl)
async def decide(
self,
policy_path: str, # 예: "mcp.tool.access" → /v1/data/mcp/tool/access
input_data: dict,
ttl_override: float | None = None,
) -> bool:
cached = await self._cache.get(input_data)
if cached is not None:
return cached
# OPA Rego 패키지명(점 구분)을 URL 경로(슬래시 구분)로 변환합니다.
# "mcp.tool.access" → "/v1/data/mcp/tool/access"
path_segment = policy_path.replace(".", "/")
url = f"{self._base_url}/v1/data/{path_segment}"
async with httpx.AsyncClient(timeout=0.5) as client:
resp = await client.post(url, json={"input": input_data})
resp.raise_for_status()
decision: bool = resp.json().get("result", False)
# deny 결과는 짧은 TTL로 정책 변경이 빠르게 반영되도록 합니다.
effective_ttl = ttl_override or (5.0 if not decision else 30.0)
await self._cache.set(input_data, decision, ttl=effective_ttl)
return decisionDeny 결과 짧은 TTL: "차단" 결정은 5초 만에 만료시켜 정책 변경이 신속히 반영되도록 합니다. "허용" 결정은 30초 TTL을 적용해 고빈도 호출 오버헤드를 줄일 수 있습니다.
예시 4: 통합 정책 입력 구조 설계
OPA가 런타임 컨텍스트를 풍부하게 평가하려면 입력 구조 설계가 중요합니다. 아래는 프로덕션 수준의 입력 스키마 예시입니다.
{
"principal": {
"id": "user-123",
"role": "analyst",
"department": "finance",
"clearance_level": 2
},
"client": {
"agent_id": "claude-agent-v3.1",
"session_id": "sess_abc123",
"session_age_minutes": 15,
"ip_address": "10.0.1.45"
},
"tool": {
"name": "query_financial_records",
"server": "postgres-mcp",
"version": "2.1.0",
"data_sensitivity": 3,
"provider": "internal-data-team"
},
"args": {
"table": "revenue_q1_2026",
"filter": "region = 'APAC'"
},
"env": {
"time": "2026-04-14T10:30:00Z",
"network_zone": "internal",
"request_id": "req_xyz789"
}
}이 입력을 받는 Rego 정책에서 복합 조건 평가가 가능합니다.
# policy/financial_tools.rego
package mcp.tool.financial
import future.keywords.if
import future.keywords.in
default allow := false
# 금융 도구 접근 허용 조건 (모두 충족 필수 — AND)
allow if {
authorized_role
sensitivity_within_clearance
within_business_hours
trusted_agent_version
not suspicious_session
}
authorized_role if {
input.principal.role in {"analyst", "finance_manager", "auditor"}
}
# 사용자 클리어런스 레벨이 도구 민감도 이상이어야 함
sensitivity_within_clearance if {
input.principal.clearance_level >= input.tool.data_sensitivity
}
# 주의: env.time은 서버가 UTC로 직접 채워야 합니다. 클라이언트 제공값 사용 금지.
within_business_hours if {
hour := time.clock(time.parse_rfc3339_ns(input.env.time))[0]
hour >= 9
hour < 18
}
# 신뢰할 수 있는 에이전트 버전 목록 (data.json으로 외부 관리)
trusted_agent_version if {
input.client.agent_id in data.trusted_agent_ids
}
# 30분 이상 된 세션에서의 고민감도 접근은 재인증 필요
suspicious_session if {
input.tool.data_sensitivity >= 4
input.client.session_age_minutes > 30
}장단점 분석
장점
| 항목 | 내용 | 조건/한계 |
|---|---|---|
| 계층적 방어 | ETDI(정적) + OPA(동적) 이중 레이어로 Tool Squatting, Rug Pull, 런타임 권한 남용을 각각 방어 | 두 레이어 모두 구현해야 전체 보호가 완성됨 |
| 정책 분리 | 코드 재배포 없이 Rego 파일만 업데이트해 보안 정책 즉시 반영 | 정책 변경 후 캐시 무효화 훅 구현 필요 |
| 표현력 | RBAC, ABAC, 시간 기반, 위치 기반, 민감도 기반 등 복합 조건 조합 지원 | 복잡한 Rego 정책은 단위 테스트 없이 유지보수가 어려워짐 |
| 생태계 성숙도 | OPA는 Netflix, Goldman Sachs, Google Cloud, T-Mobile 등 대형 프로덕션에서 검증된 CNCF 졸업 프로젝트 | 2025년 OPA 창시자팀이 Apple로 이직 — 커뮤니티 방향성 모니터링 권장 |
| 감사 추적 | 모든 도구 호출 전 정책 평가 결과 로깅으로 SOC2, ISO27001 등 규제 준수 지원 | — |
| 프롬프트 인젝션 방어 | 에이전트 레이어가 아닌 도구 호출 레이어에서 정책 집행 → 에이전트가 속아도 OPA가 차단 | — |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| Rego 진입 장벽 | 선언형 언어 특성상 Python/JS 개발자에게 초기 학습 곡선이 있음 | rego.v1 임포트로 결정론적 평가 강제, opa test 명령으로 정책 단위 테스트 |
| 지연 오버헤드 | 매 도구 호출마다 PDP 쿼리 발생 | TTL 기반 결정 캐시 + OPA 사이드카 배포로 네트워크 지연 최소화 |
| ETDI 생태계 미성숙 | 아직 표준화 진행 중(Python SDK PR #845), 광범위한 MCP 서버 지원 부재 | 자체 JWT 서명·검증 래퍼 구현, 표준화 완료 후 마이그레이션 계획 수립 |
| 번들 동기화 복잡성 | 분산 환경에서 OPA 번들 서버와 각 인스턴스 간 정책 일관성 관리 | OPA Bundle API 활용, 정책 갱신 시 캐시 전체 무효화 훅 구현 |
| 콜드 스타트·폴백 | 번들 소스 장애 시 stale 정책으로 폴백 → 보안 신선도 저하 | deny-safe 원칙 적용: OPA 무응답 시 기본값 deny |
Bundle: OPA에서 정책(.rego)과 데이터(.json)를 묶어 배포하는 패키지 단위. OPA 인스턴스들이 중앙 번들 서버에서 정책을 주기적으로 폴링해 최신 상태를 유지합니다.
실무에서 가장 흔한 실수
- OPA 응답 실패 시
allow=True로 폴백하는 설정 — 가용성을 위해 폴백을 허용으로 설정하면, OPA 장애 자체가 보안 구멍이 됩니다.deny-safe원칙을 기본으로 하고, 가용성이 중요한 도구는 별도의 예외 처리 경로를 명시적으로 설계하는 편이 안전합니다. env.time같은 환경 정보를 클라이언트가 직접 제공하도록 허용 — LLM 에이전트나 클라이언트가time,network_zone같은 값을 채워 보내면, 조작된 컨텍스트로 정책을 우회할 수 있습니다. 서버 측에서 신뢰할 수 있는 소스(시스템 시간, 네트워크 메타데이터)로 직접 채워 넣어야 합니다.- ETDI 버전 레지스트리를 메모리에만 유지 — 서버 재시작 후 승인 버전 정보가 사라지면 Rug Pull 탐지가 무력화됩니다. 승인된 도구 버전은 반드시 영구 저장소(DB, Redis 등)에 기록하고, 재시작 시 복원하는 로직이 필요합니다.
마치며
이 글 첫머리에서 던진 질문, "이 도구를 지금 이 사용자가 호출해도 되는가?"에 대해 OAuth 스코프는 "이 사용자가 이 도구에 접근할 수 있는가"만 답할 수 있습니다. ETDI는 "이 도구가 신뢰할 수 있는 출처에서 변조 없이 왔는가"를, OPA는 "지금 이 순간 이 컨텍스트에서 이 호출이 정책에 부합하는가"를 각각 답하며, 두 레이어가 결합될 때 비로소 완전한 답이 만들어집니다.
지금 바로 시작할 수 있는 3단계:
- 로컬 OPA 실행 → 정책 업로드 → curl로 즉시 테스트 — Docker 한 줄로 OPA를 띄우고, 위의 Rego 정책을 업로드한 뒤 실제 입력으로 평가 결과를 확인할 수 있습니다.
docker run -p 8181:8181 openpolicyagent/opa:latest run --server curl -X PUT localhost:8181/v1/policies/mcp \ -H "Content-Type: text/plain" --data-binary @policy/mcp_tool_access.rego curl -X POST localhost:8181/v1/data/mcp/tool/access/allow \ -H "Content-Type: application/json" \ -d '{"input": {"principal": {"role": "analyst"}, "client": {"agent_id": "test"}, "tool": {"data_sensitivity": 2}, "env": {"time": "2026-04-14T10:00:00Z"}}}'- 기존 MCP 서버에서 가장 민감한 도구 하나에만
check_policy()삽입 — 처음부터 전체 도구에 적용하기보다 파일럿 도구로 지연 오버헤드와 정책 동작을 검증하는 방식을 권장합니다. ETDIVerifier래퍼로 ETDI 검증 레이어 추가 —openssl genrsa -out tool_provider.pem 2048으로 RSA 키 쌍을 생성하고, 승인된 버전을 SQLite나 Redis에 영구 저장하는 방식으로 확장할 수 있습니다. ETDI Python SDK는 현재 PR #845가 진행 중이며, 병합 전에는 본 글의ETDIVerifier래퍼를 그대로 사용할 수 있습니다.
다음 글: Rego 정책을 안전하게 배포하는 OPA Bundle Server 구성과, 분산 MCP 환경에서 정책 일관성을 보장하는 전략을 다룰 예정입니다.
참고 자료
- ETDI: Mitigating Tool Squatting and Rug Pull Attacks in MCP (arXiv:2506.01333)
- ETDI: Enhanced Tool Definition Interface | MCP Security Framework
- ETDI Python SDK PR #845 | modelcontextprotocol/python-sdk
- MCP Access Control: OPA vs Cedar Comparison | Natoma
- Why Open Policy Agent is the Missing Guardrail for Your AI Agents | CodiLime
- Advanced authentication and authorization for MCP Gateway | Red Hat Developer
- IBM ContextForge: Unified Policy Decision Point Feature
- IBM ContextForge AI Gateway
- MCP Security: Implementing Robust Authentication and Authorization | Red Hat
- Implementing Ultra Fine-Grained RBAC/ABAC in MCP Servers with Cerbos and OPA
- Guardrails and Policy Enforcement in Agentic AI Workflows
- Open Policy Agent 공식 문서
- Securing the Model Context Protocol (MCP): Risks, Controls, and Governance (arXiv:2511.20920)
- Authorization for MCP: OAuth 2.1, PRMs, and Best Practices | Oso
- We built the security layer MCP always needed | Trail of Bits