LLM 기반 자율 에이전트가 데이터베이스 쿼리를 직접 실행하고, 사내 ERP API를 호출하며, 운영 인프라에 배포 스크립트를 전달하는 단계로 진화하면서 엔터프라이즈 보안의 중심축이 ‘프롬프트 필터링’에서 ‘행위 감사(Action Auditing)’로 급격히 전환되고 있습니다.
그러나 많은 개발 현장에서 여전히 사용자와 LLM 간에 오간 원시 대화 내용(Chat History)이나 모델의 내부 생각 과정(Chain of Thought)을 데이터베이스에 단순 덤프해 두는 것을 ‘감사 로그’로 간주하는 실수를 범합니다. 비정형 자연어 텍스트는 시스템의 상태 변화를 구조적으로 입증할 수 없으며, 금융·의료·엔터프라이즈 컴플라이언스 기준(ISMS-P, SOC 2, ISO 27001)에서 요구하는 법적 증적 요건을 충족하지 못합니다.
이번 글에서는 OWASP Agentic Security 표준과 Anthropic·OpenAI의 시스템 안전성 평가 기준을 기반으로, 단순 대화 기록의 한계를 짚고 프로덕션 환경에 즉시 적용 가능한 도구 호출(Tool Call) 전용 감사 스키마와 실무 인터셉터 아키텍처를 정리합니다.
1. 대화 기록과 Chain of Thought가 감사 로그가 될 수 없는 이유
모델의 원시 텍스트 로그는 디버깅에는 유용할 수 있으나, 보안 감사의 관점에서는 세 가지 치명적인 결함을 가집니다:
- 비결정성(Non-deterministic Text)과 증적 불능: 모델이 생성한 ‘출력 텍스트’는 실제 백엔드 샌드박스나 OS 커널에서 실행된 ‘명령어’와 일치하지 않을 수 있습니다. 감사의 핵심은 모델이 무엇을 ‘말했는가’가 아니라 어떤 인자(Parameter)를 사용해 어떤 엔드포인트를 ‘실행했는가’입니다.
- 민감정보(PII/Secret) 통제 부재: 원시 프롬프트를 무차별 저장하면 사용자가 무심코 입력한 개인정보나 시스템 프롬프트의 기밀 토큰이 로그 저장소에 암호화 없이 영구 잔류하게 됩니다.
- 사전 권한 검증(Pre-execution Authorization) 이력 누락: 대상 액션이 읽기(Read)인지 쓰기/삭제(Mutation)인지, 사전에 관리자 승인(Human-in-the-loop)이나 역할 기반 접근 제어(RBAC) 검증을 거쳤는지에 대한 구조화된 메타데이터가 존재하지 않습니다.
2. 실무 AI 도구 호출 감사 스키마 4대 핵심 계층
OWASP의 ‘Insecure Tool Use’ 방어 지침에 따르면, 에이전트의 도구 호출은 다음 4가지 계층이 구조화된 JSON 형태로 반드시 기록되어야 합니다:
- 컨텍스트 및 주체 식별(Identity & Context): 세션 ID, 호출 사용자 식별자, 에이전트 인스턴스 ID, 클라이언트 IP.
- 도구 실행 명세(Tool Specification): 호출된 도구의 고유 명칭, 함수 버전, Pydantic 등으로 엄격히 검증된 입력 파라미터.
- 보안 및 승인 메타데이터(Security & Authorization): 쓰기 위험 등급(Risk Level), 인간 승인(Human-in-the-loop) 토큰, 서명된 JWT 클레임.
- 실행 결과 및 사이드 이펙트(Execution Result & Impact): 반환 상태 코드(Success/Fail), 마스킹 처리된 반환값 요약, 실행 소요 시간(Latency), 영향받은 리소스 ID.
3. Pydantic 기반 표준 감사 로그 스키마 구현
다음은 엔터프라이즈 환경에서 SIEM(Security Information and Event Management) 시스템이나 Elasticsearch/OpenSearch로 스트리밍하기에 적합한 표준 Pydantic 감사 모델 예시입니다.
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, Optional
from pydantic import BaseModel, Field
class ActionRiskLevel(str, Enum):
READ_ONLY = "READ_ONLY" # 단순 조회
MUTATION = "MUTATION" # 데이터 생성/수정
CRITICAL = "CRITICAL" # 데이터 삭제/인프라 제어
class ToolExecutionStatus(str, Enum):
SUCCESS = "SUCCESS"
FAILED = "FAILED"
DENIED = "DENIED" # 권한 부족으로 차단됨
class AgentToolAuditRecord(BaseModel):
# 1. 식별 및 감사 추적 계층
audit_id: str = Field(..., description="UUID v4 기반 고유 감사 레코드 식별자")
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
session_id: str = Field(..., description="상위 대화 및 태스크 트레이스 ID")
actor_id: str = Field(..., description="호출을 유발한 인증된 사용자 식별자")
agent_name: str = Field(..., description="실행 에이전트 모듈 명칭")
# 2. 도구 호출 명세 계층
tool_name: str = Field(..., description="호출된 도구 함수명 (예: execute_sql_query)")
tool_version: str = Field("1.0.0", description="도구 스키마 버전")
input_parameters: Dict[str, Any] = Field(..., description="검증 완료된 입력 파라미터 (Secret 마스킹 완료)")
# 3. 인가 및 거버넌스 계층
risk_level: ActionRiskLevel
is_human_approved: bool = Field(False, description="비가역 작업 시 인간 승인 여부")
approver_id: Optional[str] = Field(None, description="승인자 식별자")
# 4. 실행 결과 계층
execution_status: ToolExecutionStatus
execution_duration_ms: float = Field(..., description="실행 소요 시간(ms)")
affected_resource_id: Optional[str] = Field(None, description="수정/영향을 받은 외부 리소스 식별자")
error_message: Optional[str] = Field(None, description="실패 시 원인 메시지")
4. 파이썬 도구 호출 인터셉터(Interceptor) 패턴
실제 도구 함수에 감사 로깅을 강제하기 위해서는 모델이 도구를 호출하는 진입점에 데코레이터 기반의 인터셉터를 배치하여 비즈니스 로직과 감사를 분리해야 합니다.
import time
import uuid
import functools
def audit_logged_tool(risk_level: ActionRiskLevel):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
audit_id = str(uuid.uuid4())
start_time = time.perf_counter()
status = ToolExecutionStatus.SUCCESS
err_msg = None
# 민감정보 마스킹 처리된 파라미터 복사
sanitized_params = {k: ("***REDACTED***" if "token" in k or "key" in k else v) for k, v in kwargs.items()}
try:
# 실제 도구 로직 실행
result = func(*args, **kwargs)
return result
except Exception as e:
status = ToolExecutionStatus.FAILED
err_msg = str(e)
raise e
finally:
duration_ms = (time.perf_counter() - start_time) * 1000
audit_entry = AgentToolAuditRecord(
audit_id=audit_id,
session_id=kwargs.get("session_id", "anonymous-session"),
actor_id=kwargs.get("actor_id", "system"),
agent_name="production-backend-agent",
tool_name=func.__name__,
input_parameters=sanitized_params,
risk_level=risk_level,
is_human_approved=kwargs.get("is_approved", False),
execution_status=status,
execution_duration_ms=round(duration_ms, 2),
error_message=err_msg
)
# 비동기 로거(CloudWatch, Kafka, WORM 스토리지)로 격리 전송
emit_audit_event(audit_entry.model_dump())
return wrapper
return decorator
5. 프로덕션 감사 파이프라인 체크리스트
- WORM(Write Once, Read Many) 저장소 분리: 감사 로그는 에이전트 런타임이나 일반 관리자가 임의로 수정·삭제할 수 없는 격리된 S3 Object Lock 또는 전용 감사 DB에 적재해야 합니다.
- 위험 등급 기반 Human-in-the-loop 게이트:
ActionRiskLevel.CRITICAL(테이블 드롭, 금융 거래, 권한 변경) 레벨의 도구는 유효한 승인 토큰이 인자로 전달되지 않으면 인터셉터 단계에서 즉시 실행을 차단(DENIED)해야 합니다. - 파라미터 변조 검증: LLM이 환각으로 존재하지 않는 옵션이나 SQL 인젝션 패턴을 주입하지 못하도록 Pydantic DTO의
extra='forbid'옵션을 필수로 활성화하십시오.