LLM 기반 시스템의 인증 아키텍처, 에이전트 도구 접근 제어, 최소 권한 원칙, API 키 관리, 그리고 Human-in-the-Loop 패턴을 실전 중심으로 다룹니다.
5장에서 콘텐츠 안전성을 다뤘습니다. 이 장에서는 LLM 애플리케이션의 인증(Authentication)과 권한 관리(Authorization) 를 다룹니다. 특히 에이전트 시스템에서 LLM이 외부 도구를 호출할 때의 접근 제어와, OWASP LLM06(과도한 에이전시)에 대한 방어를 중점적으로 살펴봅니다.
OWASP LLM Top 10의 LLM06은 LLM에 불필요하게 넓은 권한이 부여되는 위험을 다룹니다.
위험 시나리오:
1. 에이전트에 데이터베이스 전체 접근 권한 부여
2. 프롬프트 인젝션으로 에이전트가 DELETE 쿼리 실행
3. 전체 데이터 삭제
| 측면 | 설명 | 예시 |
|---|---|---|
| 과도한 기능 | 불필요한 도구/플러그인 접근 | 읽기만 필요한데 쓰기 권한 부여 |
| 과도한 권한 | 도구의 권한이 필요 이상 | SELECT만 필요한데 DELETE 권한 |
| 과도한 자율성 | 사람 승인 없이 위험한 작업 수행 | 결제 처리를 자동 승인 |
from dataclasses import dataclass
from typing import Literal
@dataclass
class ToolPermission:
name: str
operations: list[Literal["read", "write", "delete", "execute"]]
scope: str # "own_data", "team_data", "all_data"
requires_approval: bool = False
rate_limit: int | None = None # 분당 호출 횟수
# 역할별 권한 정의
ROLE_PERMISSIONS = {
"customer_service": [
ToolPermission("search_orders", ["read"], "own_data"),
ToolPermission("update_ticket", ["read", "write"], "own_data"),
# delete 권한 없음, 전체 데이터 접근 없음
],
"admin_assistant": [
ToolPermission("search_database", ["read"], "all_data"),
ToolPermission("modify_config", ["read", "write"], "team_data",
requires_approval=True),
ToolPermission("delete_records", ["delete"], "team_data",
requires_approval=True, rate_limit=5),
],
}class ToolAuthorizationMiddleware:
def __init__(self, permissions: list[ToolPermission]):
self.permissions = {p.name: p for p in permissions}
self.call_counts: dict[str, int] = {}
async def authorize(
self, tool_name: str, operation: str, user_context: dict
) -> tuple[bool, str | None]:
"""도구 호출 권한 검증"""
permission = self.permissions.get(tool_name)
if not permission:
return False, f"허가되지 않은 도구: {tool_name}"
if operation not in permission.operations:
return False, f"{tool_name}에 대한 {operation} 권한 없음"
# 속도 제한 검사
if permission.rate_limit:
count = self.call_counts.get(tool_name, 0)
if count >= permission.rate_limit:
return False, f"{tool_name} 호출 한도 초과"
self.call_counts[tool_name] = count + 1
# 승인 필요 여부
if permission.requires_approval:
approved = await self._request_human_approval(
tool_name, operation, user_context
)
if not approved:
return False, "관리자 승인이 거부되었습니다"
return True, None위험도가 높은 작업에는 인간 승인을 요구합니다.
from enum import Enum
class RiskLevel(Enum):
LOW = "low" # 자동 승인
MEDIUM = "medium" # 로그 기록 + 사후 검토
HIGH = "high" # 사전 인간 승인 필요
CRITICAL = "critical" # 이중 승인 필요
TOOL_RISK_LEVELS = {
"search_knowledge_base": RiskLevel.LOW,
"send_email": RiskLevel.MEDIUM,
"update_database": RiskLevel.HIGH,
"process_payment": RiskLevel.CRITICAL,
"delete_account": RiskLevel.CRITICAL,
}
class HumanInTheLoop:
async def check_and_approve(
self, tool_name: str, params: dict, context: dict
) -> tuple[bool, str]:
risk = TOOL_RISK_LEVELS.get(tool_name, RiskLevel.HIGH)
if risk == RiskLevel.LOW:
return True, "자동 승인"
elif risk == RiskLevel.MEDIUM:
# 실행 후 감사 로그
await self._log_action(tool_name, params, context)
return True, "실행 (감사 로그 기록)"
elif risk == RiskLevel.HIGH:
# 인간 승인 요청
approved = await self._request_approval(
approver="team_lead",
action=f"{tool_name}({params})",
context=context,
)
return approved, "인간 승인" if approved else "승인 거부"
else: # CRITICAL
# 이중 승인
approved1 = await self._request_approval("team_lead", ...)
approved2 = await self._request_approval("security_admin", ...)
return approved1 and approved2, "이중 승인"HITL의 핵심은 모든 것에 인간 승인을 요구하지 않는 것입니다. 위험도에 따라 자동 승인(LOW), 사후 감사(MEDIUM), 사전 승인(HIGH), 이중 승인(CRITICAL)으로 등급을 나누세요. 모든 작업에 승인을 요구하면 사용자 경험이 크게 저하되고, 결국 "승인 피로"로 무의미해집니다.
# 나쁜 예: API 키를 LLM 컨텍스트에 포함
def bad_tool(api_key: str, query: str):
"""LLM이 api_key를 볼 수 있음 — 유출 위험"""
return requests.get(f"https://api.example.com?key={api_key}&q={query}")
# 좋은 예: 키를 서버 측에서만 관리
class SecureTool:
def __init__(self):
self._api_key = os.environ["API_KEY"] # 환경 변수에서 로드
def execute(self, query: str) -> str:
"""LLM은 query만 제공, 키는 내부 처리"""
response = requests.get(
"https://api.example.com",
params={"q": query},
headers={"Authorization": f"Bearer {self._api_key}"},
)
return response.json()["result"]MCP(Model Context Protocol) 서버를 통해 외부 시스템에 접근할 때의 보안입니다.
# MCP 서버 보안 원칙:
# 1. 각 MCP 서버에 최소 필요 권한만 부여
# 2. 읽기 전용 도구와 쓰기 도구 분리
# 3. 민감한 도구에는 승인 워크플로우 적용
# 4. 모든 도구 호출을 감사 로그에 기록
# 5. 속도 제한(Rate Limiting) 적용
mcp_security_config = {
"servers": {
"database": {
"allowed_tools": ["query_readonly"],
"blocked_tools": ["execute_raw_sql", "drop_table"],
"rate_limit": {"calls_per_minute": 30},
"require_approval": False,
},
"email": {
"allowed_tools": ["read_inbox", "search_emails"],
"blocked_tools": ["send_email", "delete_email"],
"rate_limit": {"calls_per_minute": 10},
"require_approval": True, # 이메일 전송은 승인 필요
},
},
}다중 사용자 환경에서 LLM의 대화 컨텍스트가 사용자 간 유출되지 않도록 합니다.
class SecureSessionManager:
def __init__(self):
self.sessions: dict[str, dict] = {}
def get_context(self, session_id: str, user_id: str) -> dict:
"""사용자별 격리된 세션 컨텍스트 반환"""
key = f"{user_id}:{session_id}"
if key not in self.sessions:
self.sessions[key] = {
"messages": [],
"user_id": user_id,
"permissions": self._get_user_permissions(user_id),
"created_at": datetime.now(),
}
return self.sessions[key]
def validate_access(self, session_id: str, user_id: str) -> bool:
"""세션 소유자 검증"""
key = f"{user_id}:{session_id}"
session = self.sessions.get(key)
return session is not None and session["user_id"] == user_idLLM의 대화 컨텍스트에는 민감 정보가 포함될 수 있습니다. 다중 사용자 환경에서는 세션 격리를 철저히 하고, 대화 이력을 저장할 때 암호화를 적용하세요. 특히 공유 LLM 서비스에서 한 사용자의 대화 내용이 다른 사용자에게 유출되지 않도록 주의해야 합니다.
LLM 애플리케이션의 인증과 권한 관리는 전통적인 접근 제어에 에이전트 특화 고려사항을 추가합니다. 최소 권한 원칙으로 도구 접근을 제한하고, HITL 패턴으로 위험한 작업에 인간 승인을 요구하며, API 키를 LLM 컨텍스트에서 격리하는 것이 핵심입니다.
다음 장에서는 이러한 방어 체계의 효과를 검증하는 레드티밍과 보안 테스트를 다룹니다.
이 글이 도움이 되셨나요?
AI 시스템의 레드티밍 방법론, 자동화된 보안 테스트, 프롬프트 인젝션 퍼징, 그리고 지속적 보안 검증 파이프라인 구축을 다룹니다.
LLM의 유해 콘텐츠 생성 방지, 편향 완화, 환각 탐지, 그리고 Constitutional AI와 RLHF의 원리를 다루며 안전한 AI 출력을 위한 다층 전략을 설계합니다.
EU AI Act를 중심으로 글로벌 AI 규제의 핵심 요구사항, 위험 분류 체계, 기술적 컴플라이언스 전략, 그리고 책임 있는 AI 개발 프레임워크를 다룹니다.