AI 에이전트 인증·인가 완전 가이드: 프로덕션에서 OAuth, 툴 호출 권한, 보안을 제대로 하는 법
AI 에이전트가 고객 14,000명한테 슬랙 메시지를 보냈어요. 주문 조회용 프로덕션 서포트 에이전트가 프롬프트 인젝션을 당해서 대량 메시징 API를 호출한 거예요. 자격증명도 있었고, 권한도 있었고, 아무도 승인하지 않았어요. 에이전트는 기술상 문제없는 범위에서 움직인 거예요. 그냥 그걸 하면 안 됐을 뿐이죠.
이게 AI 보안의 새로운 전선이에요. 그동안 우리는 웹 애플리케이션을 SQL 인젝션, XSS, CSRF로부터 방어하는 데 수년을 쏟았죠. 지금은 API 키, OAuth 토큰, 데이터베이스 자격증명을 들고 다니면서 어떤 툴을 호출하고 어떤 데이터에 접근할지 알아서 결정하는 자율 시스템을 배포하고 있어요. 공격 표면은 모델 가중치가 아니에요. 실행 레이어예요. 프롬프트 인젝션, 목표 하이재킹, 혹은 단순한 설정 실수로 조작될 수 있는 자격증명, 권한, 툴 접근권이 진짜 위협이거든요.
이 가이드에서는 프로덕션 AI 에이전트의 보안 아키텍처 전체를 다뤄요. 아이덴티티 관리, OAuth 2.1 위임 인증, 세분화된 툴 권한, MCP 게이트웨이 적용, 휴먼인더루프 패턴, 그리고 안전한 에이전트 배포와 사고 대기 상태를 가르는 심층 방어 전략까지.
기존 인증 체계가 AI 에이전트에서 안 통하는 이유
인턴한테 AWS root 자격증명을 주면서 "알아서 잘 써"라고 하지는 않잖아요. 근데 많은 팀이 AI 에이전트한테 정확히 그걸 하고 있어요. 기존 서비스 계정 모델이 왜 무너지는지 살펴보죠.
에이전트는 비결정적 행위자
기존 마이크로서비스는 매번 같은 API를 호출해요. 소스코드를 읽으면 행동을 감사할 수 있죠. AI 에이전트는 근본적으로 달라요.
기존 서비스:
입력: "주문 #12345 조회"
→ 항상 호출: GET /api/orders/12345
→ 예측 가능, 감사 가능
AI 에이전트:
입력: "이 고객 주문 관련 도와줘"
→ GET /api/orders/12345를 호출할 수도
→ POST /api/refunds를 호출할 수도
→ PUT /api/customer/email을 호출할 수도
→ DELETE /api/orders/12345를 호출할 수도
→ 비결정적, 컨텍스트 의존
에이전트는 런타임에서 추론에 따라 어떤 툴을 호출할지 결정해요. 마이크로서비스용으로 잘 동작했던 정적 RBAC 정책으로는 이걸 처리할 수 없어요. 동적인 컨텍스트 기반 인가가 필요하죠.
폭발 반경 문제
기존 서비스가 침해당하면 피해 범위는 고정된 기능으로 한정돼요. AI 에이전트가 침해당하면(또는 조작당하면) 폭발 반경은 부여된 전체 권한 세트와 같아요.
| 요소 | 마이크로서비스 | AI 에이전트 |
|---|---|---|
| 행동 | 고정, 사전 정의 | 동적, 모델이 결정 |
| 공격 벡터 | 코드 익스플로잇 | 프롬프트 인젝션, 목표 하이재킹 |
| 폭발 반경 | 단일 기능 | 부여된 모든 권한 |
| 감사 추적 | 결정론적 로그 | 추론 트레이스 필요 |
| 접근 패턴 | 예측 가능 | 컨텍스트 의존 |
넓은 권한을 가진 에이전트는 범용 공격 표면이 돼요. 에이전트가 접근 가능한 모든 툴은 공격자가 에이전트를 통해 호출할 수 있는 툴이니까요.
자격증명 수명 불일치
대부분의 서비스 계정은 장기 자격증명을 써요. 분기마다 교체하는 API 키, 만료 없는 서비스 토큰. 결정론적 서비스에는 (어느 정도) 괜찮아요. 하지만 실시간으로 조작될 수 있는 에이전트에는?
// ❌ 대부분의 팀이 에이전트를 배포하는 방식 const agent = new Agent({ openaiKey: process.env.OPENAI_API_KEY, stripeKey: process.env.STRIPE_SECRET_KEY, // 전체 접근 dbConnection: process.env.DATABASE_URL, // 읽기 + 쓰기 slackToken: process.env.SLACK_BOT_TOKEN, // 전체 채널 awsCredentials: { accessKeyId: process.env.AWS_ACCESS_KEY, // IAM 관리자?? secretAccessKey: process.env.AWS_SECRET_KEY, }, }); // 이 에이전트가 성 전체의 열쇠를 쥐고 있어요
에이전트가 프롬프트 인젝션을 당하면, 이 자격증명 전부가 작동 가능한 상태가 돼요.
에이전트 아이덴티티 모델
에이전트 보안의 첫 단계는 에이전트를 **넌-휴먼 아이덴티티(NHI)**로 취급하는 거예요. 사용자 계정의 연장선도 아니고, 공유 서비스 계정도 아닌, 독립적인 아이덴티티 주체로요.
고유 에이전트 아이덴티티
모든 에이전트 인스턴스는 다른 에이전트나 서비스와 자격증명을 공유하지 않는 고유한 아이덴티티를 가져야 해요.
interface AgentIdentity { agentId: string; // 고유 식별자 agentType: string; // 예: 'customer-support', 'data-analyst' version: string; // 감사용 에이전트 버전 deploymentEnv: string; // 'production' | 'staging' | 'development' owner: string; // 담당 팀 createdAt: Date; expiresAt: Date; // 필수 만료일 maxConcurrentSessions: number; allowedTools: string[]; // 허용된 툴 화이트리스트 deniedTools: string[]; // 명시적 블랙리스트 } // 배포 시 에이전트 아이덴티티 등록 const identity = await identityProvider.register({ agentType: 'customer-support', version: '2.4.1', owner: 'support-team', expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24시간 allowedTools: [ 'lookup_order', 'check_shipping_status', 'create_support_ticket', ], deniedTools: [ 'issue_refund', // 사람 승인 필요 'delete_account', // 절대 자동화 금지 'bulk_message', // 절대 자동화 금지 ], });
단기, 범위 제한 자격증명
정적 API 키는 과감히 없애야 해요. 모든 에이전트 세션은 다음 조건의 자격증명을 써야 해요.
- 시간 제한: 몇 달이 아니라 몇 분이나 몇 시간 후 만료
- 범위 제한: 필요한 특정 툴에만 접근 허가
- 세션 바인딩: 에이전트 타입이 아닌 특정 세션에 바인딩
class AgentCredentialManager { async getSessionCredentials( agentIdentity: AgentIdentity, sessionContext: SessionContext ): Promise<ScopedCredentials> { // 아이덴티티 프로바이더에서 단기 토큰 발급 const token = await this.idp.issueToken({ subject: agentIdentity.agentId, audience: 'tool-gateway', scopes: this.resolveScopes(agentIdentity, sessionContext), expiresIn: '15m', // 15분 세션 sessionId: sessionContext.id, constraints: { maxToolCalls: 50, // 세션당 하드 리밋 allowedIPs: ['10.0.0.0/8'], // 네트워크 제한 rateLimit: '100/minute', }, }); return { token, refreshToken: null, // 리프레시 없음, 새 세션 발급 expiresAt: token.expiresAt, }; } private resolveScopes( identity: AgentIdentity, context: SessionContext ): string[] { // 컨텍스트 기반 동적 스코프 해석 const baseScopes = identity.allowedTools.map( (t) => `tool:${t}:execute` ); // 사용자 등급에 따른 권한 상승 또는 제한 if (context.userTier === 'enterprise') { baseScopes.push('tool:priority_support:execute'); } // 시간 기반 제한 const hour = new Date().getHours(); if (hour < 6 || hour > 22) { // 업무 외 시간: 읽기 전용 모드 return baseScopes.filter((s) => !s.includes('write')); } return baseScopes; } }
OAuth 2.1로 에이전트 위임 인가 구현하기
AI 에이전트가 사용자를 대신해서 행동할 때는 위임 인가가 필요해요. 사용자가 에이전트에게 제한된, 시간 한정 접근을 명시적으로 부여하는 거죠. OAuth 2.1이 정확히 이 목적으로 설계됐어요.
에이전트용 OAuth 플로우
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 사용자 │ │ 에이전트 │ │ 인증 │ │ 리소스 │
│ │ │ 게이트웨이│ │ 서버 │ │ 서버 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ "주문 관련 │ │ │
│ 도와줘" │ │ │
│──────────────>│ │ │
│ │ │ │
│ 인증 필요 │ │ │
│<──────────────│ │ │
│ │ │ │
│ 로그인 + 범위 │ │ │
│ 제한 동의 │ │ │
│──────────────────────────────>│ │
│ │ │ │
│ │ 범위 제한 │ │
│ │ 토큰 발급 │ │
│ │<──────────────│ │
│ │ │ │
│ │ 토큰으로 │ │
│ │ API 호출 │ │
│ │──────────────────────────────>│
│ │ │ │
│ │ 응답 수신 │ │
│ │<──────────────────────────────│
│ │ │ │
│ 결과만 │ │ │
│ 전달 │ │ │
│<──────────────│ │ │
구현: OAuth 2.1 + PKCE
import { AuthorizationCode } from 'simple-oauth2'; class AgentOAuthManager { private client: AuthorizationCode; constructor() { this.client = new AuthorizationCode({ client: { id: process.env.AGENT_CLIENT_ID!, secret: '', // 퍼블릭 클라이언트(시크릿 없음) }, auth: { tokenHost: process.env.AUTH_SERVER_URL!, authorizePath: '/authorize', tokenPath: '/token', }, }); } async initiateUserConsent( userId: string, requiredScopes: string[] ): Promise<ConsentRequest> { // PKCE 챌린지 생성 const codeVerifier = crypto.randomBytes(32) .toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); const authorizationUrl = this.client.authorizeURL({ redirect_uri: process.env.AGENT_CALLBACK_URL, scope: requiredScopes.join(' '), state: crypto.randomUUID(), code_challenge: codeChallenge, code_challenge_method: 'S256', }); // 토큰 교환을 위해 verifier 저장 await this.storeSession(userId, { codeVerifier }); return { consentUrl: authorizationUrl, scopes: requiredScopes, expiresIn: 300, // 5분 내 완료 필요 }; } async exchangeCode( userId: string, authorizationCode: string ): Promise<AgentToken> { const session = await this.getSession(userId); const tokenResponse = await this.client.getToken({ code: authorizationCode, redirect_uri: process.env.AGENT_CALLBACK_URL, code_verifier: session.codeVerifier, }); return { accessToken: tokenResponse.token.access_token, expiresAt: new Date(tokenResponse.token.expires_at), scopes: tokenResponse.token.scope.split(' '), // 리프레시 토큰 없음: 에이전트가 재동의 요청해야 함 }; } }
세밀한 스코프 설계
최소 권한 원칙을 적용할 만큼 좁은 스코프를 설계하세요.
// ❌ BAD: 너무 넓은 스코프 const scopes = ['orders:full', 'customers:full', 'payments:full']; // ✅ GOOD: 세밀한, 행동별 스코프 const scopes = [ 'orders:read', // 주문 조회만 가능 'orders:status:read', // 배송 상태 조회만 가능 'tickets:create', // 지원 티켓 생성만 가능 // 포함되지 않음: // 'orders:write' // 주문 수정 불가 // 'refunds:create' // 환불 발행 불가 // 'customers:delete' // 계정 삭제 불가 ]; // 더 좋은 방법: 리소스별 스코프 const scopes = [ 'orders:read:user:usr_abc123', // 이 사용자의 주문만 'tickets:create:org:org_xyz789', // 이 조직의 티켓만 ];
툴 게이트웨이: 모든 에이전트 행동을 가로채기
가장 핵심적인 보안 레이어는 에이전트와 모든 호출 가능한 툴 사이에 위치하는 레이어예요. 에이전트 행동의 방화벽이라고 생각하세요.
아키텍처
┌─────────────┐ ┌─────────────────────────────────────┐
│ │ │ 툴 게이트웨이 │
│ 에이전트 │ │ │
│ 런타임 │───>│ ┌──────────┐ ┌───────────────┐ │
│ │ │ │ 인가 │ │ 레이트 │ │
│ │ │ │ 엔진 │ │ 리미터 │ │
└─────────────┘ │ └────┬─────┘ └───────┬───────┘ │
│ │ │ │
│ ┌────▼────────────────▼───────┐ │
│ │ 정책 적용 포인트 (PEP) │ │
│ └────┬─────────────────────────┘ │
│ │ │
│ ┌────▼─────────────────────────┐ │
│ │ 감사 로거 │ │
│ └────┬─────────────────────────┘ │
└───────┼─────────────────────────────┘
│
┌────────────┼────────────────┐
│ │ │
┌─────▼────┐ ┌────▼─────┐ ┌──────▼──────┐
│ Stripe │ │ DB │ │ Slack API │
│ API │ │ │ │ │
└──────────┘ └──────────┘ └─────────────┘
구현
interface ToolCallRequest { agentId: string; sessionId: string; toolName: string; parameters: Record<string, unknown>; reasoning: string; // 에이전트가 이 툴을 호출하려는 이유 traceId: string; } interface PolicyDecision { allowed: boolean; reason: string; requiresApproval: boolean; modifiedParams?: Record<string, unknown>; } class ToolGateway { private authzEngine: AuthorizationEngine; private rateLimiter: RateLimiter; private auditLog: AuditLogger; private approvalQueue: ApprovalQueue; async executeToolCall( request: ToolCallRequest ): Promise<ToolCallResult> { // Step 1: 에이전트 아이덴티티와 세션 검증 const session = await this.verifySession(request); if (!session.valid) { throw new UnauthorizedError('유효하지 않거나 만료된 세션'); } // Step 2: 레이트 리밋 확인 const rateLimitOk = await this.rateLimiter.check( request.agentId, request.toolName ); if (!rateLimitOk) { await this.auditLog.log({ event: 'RATE_LIMIT_EXCEEDED', ...request, }); throw new RateLimitError('툴 호출 레이트 리밋 초과'); } // Step 3: 인가 정책 평가 const decision = await this.authzEngine.evaluate({ subject: request.agentId, action: request.toolName, resource: request.parameters, context: { sessionId: request.sessionId, time: new Date(), reasoning: request.reasoning, }, }); // Step 4: 정책 결정 처리 if (!decision.allowed) { await this.auditLog.log({ event: 'TOOL_CALL_DENIED', reason: decision.reason, ...request, }); throw new ForbiddenError(decision.reason); } // Step 5: 필요시 사람 승인 if (decision.requiresApproval) { const approved = await this.requestHumanApproval(request); if (!approved) { throw new ForbiddenError('사람 승인 거부됨'); } } // Step 6: 파라미터 정제 후 실행 const sanitizedParams = decision.modifiedParams || this.sanitizeParams(request.parameters); // Step 7: 감사 로깅과 함께 실행 const result = await this.executeWithAudit( request.toolName, sanitizedParams, request ); return result; } private sanitizeParams( params: Record<string, unknown> ): Record<string, unknown> { const sanitized = { ...params }; // 잠재적 인젝션 패턴 제거 for (const [key, value] of Object.entries(sanitized)) { if (typeof value === 'string') { sanitized[key] = value .replace(/ignore previous instructions/gi, '') .replace(/system:/gi, '') .replace(/\bsudo\b/gi, '') .replace(/;\s*(rm|drop|delete|truncate)\b/gi, ''); } } return sanitized; } }
MCP를 표준 게이트웨이로 쓰기
MCP(Model Context Protocol)가 에이전트-툴 통신의 사실상 표준이 됐어요. MCP 서버를 보안 경계로 활용하세요.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; const server = new McpServer({ name: 'secure-tools', version: '1.0.0', }); // 보안이 내장된 툴 등록 (Zod 검증) server.registerTool( 'lookup_order', { description: '주문 ID로 주문 상세 조회', inputSchema: z.object({ orderId: z.string() .regex(/^ORD-[A-Z0-9]{8}$/) // 엄격한 포맷 검증 .describe('조회할 주문 ID'), }), }, async ({ orderId }, { meta }) => { // 호출자의 필수 스코프 검증 const hasScope = await verifyScope( meta.authToken, 'orders:read' ); if (!hasScope) { return { content: [ { type: 'text', text: '오류: 주문 조회 권한이 부족합니다', }, ], isError: true, }; } // 주문이 요청 사용자의 것인지 확인 const order = await db.orders.findByIdAndUser( orderId, meta.userId ); if (!order) { return { content: [ { type: 'text', text: '주문을 찾을 수 없거나 접근이 거부됐습니다', }, ], isError: true, }; } // 정제된 데이터 반환, 내부 필드 제거 return { content: [ { type: 'text', text: JSON.stringify(sanitizeOrder(order)), }, ], }; } );
휴먼인더루프(HITL) 승인 플로우
모든 행동에 사람 승인을 거는 건 자동화의 의미를 없애요. 하지만 고위험 작업에는 반드시 필요하죠.
위험도 분류 매트릭스
enum RiskLevel { LOW = 'low', // 자동 승인 MEDIUM = 'medium', // 로그 남기고 진행, 비동기 리뷰 HIGH = 'high', // 실시간 사람 승인 필수 CRITICAL = 'critical', // 완전 차단 } const TOOL_RISK_CLASSIFICATION: Record<string, RiskLevel> = { // 낮은 위험: 자동 승인 'lookup_order': RiskLevel.LOW, 'check_shipping': RiskLevel.LOW, 'search_faq': RiskLevel.LOW, // 중간 위험: 진행하되 리뷰 플래그 'create_ticket': RiskLevel.MEDIUM, 'update_customer_preferences': RiskLevel.MEDIUM, 'send_notification': RiskLevel.MEDIUM, // 높은 위험: 실시간 승인 필수 'issue_refund': RiskLevel.HIGH, 'modify_subscription': RiskLevel.HIGH, 'access_pii': RiskLevel.HIGH, 'escalate_to_human': RiskLevel.HIGH, // 치명적: 자동 실행 절대 금지 'delete_account': RiskLevel.CRITICAL, 'bulk_data_export': RiskLevel.CRITICAL, 'modify_permissions': RiskLevel.CRITICAL, 'execute_code': RiskLevel.CRITICAL, };
실시간 승인 구현
class ApprovalQueue { async requestApproval( request: ToolCallRequest ): Promise<boolean> { const risk = TOOL_RISK_CLASSIFICATION[request.toolName]; if (risk === RiskLevel.CRITICAL) { return false; // 항상 거부 } if (risk === RiskLevel.LOW) { return true; // 항상 허용 } if (risk === RiskLevel.MEDIUM) { // 자동 승인하되 비동기 리뷰 플래그 await this.flagForReview(request); return true; } // HIGH 위험: 동기 승인 필수 const approval = await this.createApprovalRequest({ toolName: request.toolName, parameters: request.parameters, reasoning: request.reasoning, agentId: request.agentId, timeout: 300000, // 5분 }); // 슬랙/Teams/PagerDuty로 리뷰어 알림 await this.notifyReviewers(approval); // 결정 대기 const result = await this.waitForDecision( approval.id, approval.timeout ); // 타임아웃 = 거부 (fail-closed) return result?.approved ?? false; } }
심층 방어: 레이어별 보안 아키텍처
단일 보안 레이어로는 부족해요. 프로덕션 에이전트 배포에는 심층 방어가 필수예요.
Layer 1: 입력 필터링 (에이전트 이전)
class AgentInputFilter { private readonly INJECTION_PATTERNS = [ /ignore\s+(all\s+)?previous\s+instructions/i, /you\s+are\s+now\s+a/i, /system\s*:\s*/i, /\bact\s+as\b/i, /forget\s+(everything|all|your)/i, /new\s+instructions?\s*:/i, /admin\s+(mode|access|override)/i, ]; async filterInput(input: string): Promise<FilterResult> { // 패턴 매칭 for (const pattern of this.INJECTION_PATTERNS) { if (pattern.test(input)) { return { safe: false, reason: `잠재적 인젝션 감지: ${pattern.source}`, sanitized: null, }; } } // LLM 기반 콘텐츠 분류 const classification = await this.classifyIntent(input); if (classification.maliciousScore > 0.7) { return { safe: false, reason: `악의적 콘텐츠로 분류됨 (점수: ${classification.maliciousScore})`, sanitized: null, }; } return { safe: true, reason: null, sanitized: input }; } }
Layer 2: 툴 호출 검증 (게이트웨이)
위에서 다룬 툴 게이트웨이. 인가 엔진, 레이트 리미팅, 승인 플로우 통합.
Layer 3: 출력 필터링 (에이전트 이후)
class AgentOutputFilter { async filterOutput( output: string, context: SessionContext ): Promise<FilterResult> { // PII 감지 및 마스킹 const piiCheck = await this.detectPII(output); if (piiCheck.found) { output = this.redactPII(output, piiCheck.entities); } // 사실 주장에 대한 환각 체크 const claims = this.extractFactualClaims(output); for (const claim of claims) { const verified = await this.verifyClaim(claim, context); if (!verified) { output = this.flagUnverifiedClaim(output, claim); } } // 민감 데이터 유출 방지 const secrets = this.detectSecretsInOutput(output); if (secrets.length > 0) { output = '[편집됨: 출력에 민감 데이터가 포함되어 있었습니다]'; await this.alertSecurityTeam({ type: 'SECRET_LEAK_PREVENTED', context, }); } return { safe: true, reason: null, sanitized: output }; } }
Layer 4: 행동 이상 탐지
class AgentBehaviorMonitor { private baselines: Map<string, BehaviorBaseline> = new Map(); async monitorAction( action: AgentAction ): Promise<AnomalyResult> { const baseline = this.baselines.get(action.agentType); if (!baseline) return { anomalous: false }; const anomalies: string[] = []; // 툴 사용 빈도 이상 const toolFreq = await this.getToolFrequency( action.agentId, action.toolName, '1h' ); if (toolFreq > baseline.toolFrequency[action.toolName] * 3) { anomalies.push( `"${action.toolName}" 호출 ${toolFreq}회 (기준: ${baseline.toolFrequency[action.toolName]}회)` ); } // 새로운 툴 접근 패턴 const previousTools = await this.getHistoricalTools( action.agentId, '30d' ); if (!previousTools.includes(action.toolName)) { anomalies.push( `최초 툴 접근: "${action.toolName}"` ); } // 데이터 볼륨 이상 if (action.dataVolume > baseline.avgDataVolume * 5) { anomalies.push( `데이터 볼륨 ${action.dataVolume}B (기준: ${baseline.avgDataVolume}B)` ); } if (anomalies.length > 0) { await this.triggerAlert({ agentId: action.agentId, anomalies, severity: anomalies.length > 2 ? 'critical' : 'warning', }); } return { anomalous: anomalies.length > 0, details: anomalies, }; } }
감사 로깅: 포렌식의 핵심
모든 에이전트 행동은 전체 결정 체인을 재구성할 수 있을 만큼의 컨텍스트와 함께 불변적으로 로깅돼야 해요.
interface AgentAuditEntry { // 아이덴티티 timestamp: Date; traceId: string; agentId: string; agentType: string; sessionId: string; // 행위자 컨텍스트 triggerUserId: string | null; triggerSource: 'user' | 'schedule' | 'event' | 'agent'; // 행동 event: 'TOOL_CALL' | 'TOOL_DENIED' | 'APPROVAL_REQUESTED' | 'APPROVAL_GRANTED' | 'APPROVAL_DENIED' | 'RATE_LIMITED' | 'ANOMALY_DETECTED' | 'SESSION_CREATED' | 'SESSION_EXPIRED'; // 상세 toolName: string; parameters: Record<string, unknown>; // 정제됨 reasoning: string; // 에이전트의 추론 policyDecision: PolicyDecision; result: 'success' | 'failure' | 'denied' | 'timeout'; // 비용 tokensConsumed: number; estimatedCost: number; // 메타데이터 modelUsed: string; latencyMs: number; } // 핵심: 에이전트 행동과 사람 행동을 구분 class AuditLogger { async log(entry: AgentAuditEntry): Promise<void> { // 추가 전용, 불변 저장소 await this.immutableStore.append({ ...entry, actorType: 'AI_AGENT', // 항상 에이전트 행동으로 표시 hash: this.computeHash(entry), // 변조 감지 }); // 모니터링을 위한 실시간 스트리밍 await this.eventStream.publish('agent.audit', entry); } }
프로덕션 안티패턴
안티패턴 1: 공유 API 키
// ❌ 절대 금지: 여러 에이전트가 하나의 자격증명 공유 const agentA = new Agent({ apiKey: SHARED_KEY }); const agentB = new Agent({ apiKey: SHARED_KEY }); // 로그에서 A와 B를 구분 못 함 // 키 하나 폐기하면 모든 에이전트가 죽음 // ✅ 항상: 에이전트별, 세션별 자격증명 const agentA = new Agent({ credential: await issueCredential({ agentId: 'agent-a', sessionId: 'sess-123', scopes: ['orders:read'], expiresIn: '15m', }), });
안티패턴 2: "갓 모드" 권한
// ❌ 절대 금지: DB 전체 접근 가능한 에이전트 const agent = new Agent({ db: new PrismaClient(), // 전체 스키마 접근 tools: ALL_TOOLS, // 모든 툴 사용 가능 }); // ✅ 항상: 최소한의 화이트리스트 기반 접근 const agent = new Agent({ db: new ReadOnlyClient({ allowedTables: ['orders', 'products'], allowedOperations: ['SELECT'], rowLimit: 100, }), tools: SUPPORT_AGENT_TOOLS, // 선별된 서브셋 });
안티패턴 3: 에이전트 추론 신뢰하기
// ❌ 절대 금지: 에이전트가 스스로 권한 상승을 결정 if (agent.reasoning.includes('관리자 접근이 필요합니다')) { grantAdminAccess(agent); // 에이전트가 공손하게 요청했으니까! } // ✅ 항상: 권한 변경은 대역 외 승인 필수 // 권한은 배포 시 정의, 런타임이 아님 // 에이전트는 새 권한을 요청하거나 부여할 수 없음
안티패턴 4: 비상 킬 스위치 없음
// ❌ 절대 금지: 침해된 에이전트를 멈출 방법 없음 agent.run(); // 그리고 기도 // ✅ 항상: 서킷 브레이커 + 킬 스위치 const controller = new AbortController(); const breaker = new CircuitBreaker({ maxFailures: 5, maxCost: 100, // 최대 $100 timeout: 60000, signal: controller.signal, }); // 외부 킬 스위치 adminApi.on('kill-agent', (agentId) => { if (agentId === agent.id) { controller.abort('관리자에 의한 비상 종료'); revokeAllCredentials(agent.id); notifySecurityTeam(agent.id, 'EMERGENCY_KILL'); } });
보안 체크리스트
AI 에이전트를 프로덕션에 배포하기 전 매번 확인하세요.
아이덴티티 & 인증:
- 에이전트가 고유 아이덴티티를 보유 (공유 자격증명 아님)
- 자격증명이 단기 수명 (며칠/몇 달이 아닌 분/시간)
- 위임된 사용자 인가를 위한 OAuth 2.1 + PKCE 적용
- 에이전트 설정에 정적 API 키 없음
- 자격증명 로테이션 자동화 완료
인가 & 권한:
- 툴 접근이 화이트리스트 기반 (명시적 허용, 기본 거부)
- 스코프가 세밀하고 행동별로 특정
- 동적 인가가 컨텍스트 반영 (시간, 위험, 사용자 등급)
- 에이전트가 자체적으로 권한 상승 불가
- 고위험 작업에 휴먼인더루프 승인 필수
툴 게이트웨이:
- 모든 툴 호출이 중앙 게이트웨이를 통과
- 입력 파라미터가 검증 및 정제됨
- 에이전트별, 툴별 레이트 리밋 적용
- MCP 또는 동등한 프로토콜로 툴 통신 표준화
- 출력이 PII 및 민감 데이터 필터링됨
모니터링 & 대응:
- 모든 에이전트 행동이 불변으로 로깅됨
- 로그에서 에이전트 행동과 사람 행동이 구분 가능
- 행동 이상 탐지 활성화
- 모든 에이전트에 비상 킬 스위치 존재
- 인시던트 대응 플레이북에 에이전트 침해 시나리오 포함
AI 에이전트는 2026년 가장 강력한, 동시에 가장 위험한 소프트웨어 패턴이에요. 실제 자격증명으로 실제 시스템에 대해 자율적으로 결정을 내리거든요. 에이전트 보안을 기존 API 보안처럼 다루면 잘못된 가정 위에 쌓는 거예요. 에이전트에는 동적 인가, 범위 제한 자격증명, 게이트웨이 기반 툴 접근 통제, 고위험 행동의 사람 감독, 그리고 지속적인 행동 모니터링이 필요해요. 이 가이드의 패턴들은 에이전트가 비결정적이고, 조작 가능하며, 권한이 신뢰도를 넘어설 때 실질적 피해를 줄 수 있다는 현실을 위해 설계됐어요. 실행 레이어를 제대로 잠그면 에이전트는 생산성을 몇 배로 뻥튀기하는 무기가 돼요. 무시하면? 프롬프트 인젝션 한 방이면 최악의 사고예요.
관련 도구 둘러보기
Pockit의 무료 개발자 도구를 사용해 보세요