본문 바로가기
서버구축 (WEB,DB)

Zero Trust 관점에서 본 Guardrail 중심 MCP 서버 보안 설계와 운영 방안

by 날으는물고기 2026. 1. 31.

Zero Trust 관점에서 본 Guardrail 중심 MCP 서버 보안 설계와 운영 방안

728x90

전체 목표와 핵심 원칙

목표

  • MCP 서버는 “도구 실행/리소스 읽기”라는 강력한 권한을 다루므로,
    접속 관문(NGINX)에서 강하게 걸러내고, MCP 내부에서는 역할/스코프/입력검증으로 “행동”을 통제합니다.
  • 툴/리소스를 코드 하드코딩이 아니라 DB/JSON으로 관리하고, 관리용 Web API로 추가/수정하면, MCP 서버는 이를 실시간 반영합니다.

원칙

  1. 외부에서 MCP로 직통 접근 금지 (MCP는 내부망/loopback에만 바인딩)
  2. mTLS로 “클라이언트 단위” 강제 식별 (토큰보다 앞선 1차 관문)
  3. Zero Trust = 기본 차단 + 최소 허용
  4. Guardrail은 2단
    • (A) 서버 레벨: role/scope 기반 Tool/Resource 필터
    • (B) 툴 레벨: arguments 검증/수정/차단 (예: analyze_tool_request)
  5. 감사 로깅은 필수: 누가(인증서 DN) 언제 어떤 메서드로 무엇(uri/tool/args)을 시도했는지

아키텍처 전체 구성

[Client / Agent / IDE Plugin]
   |
   | (1) TLS + mTLS (클라 인증서)
   v
[NGINX Reverse Proxy]
   - mTLS verify
   - IP allowlist / geo / time window (옵션)
   - rate limit / conn limit
   - request size limit
   - (옵션) OIDC / JWT 검증
   - 헤더에 인증 컨텍스트 주입(역할/주체)
   |
   v
[MCP Server (WebSocket JSON-RPC)]
   - dynamic registry (DB/JSON 캐시)
   - server-level RBAC (tools/resources 허용목록)
   - tool-level guardrail (args 검사)
   - audit log
   |
   +--> [Admin API (FastAPI)]
         - tools/resources CRUD
         - roles/guardrail rules CRUD
         - change log/versioning

권장 분리

  • /mcp/ws (WebSocket): mTLS 필수 + rate limit
  • /admin/* (관리 API): mTLS + 추가로 IP allowlist + (가능하면) 별도 포트/별도 서브도메인 + 접근자 최소화

mTLS 구성 단계 (CA/서버/클라이언트 인증서)

내부 CA 생성(예시)

# CA 키/인증서 생성
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/C=KR/O=PagesKR/OU=Security/ CN=PagesKR-MCP-CA" \
  -out ca.crt

NGINX 서버 인증서

openssl genrsa -out server.key 4096
openssl req -new -key server.key \
  -subj "/C=KR/O=PagesKR/OU=MCP/CN=mcp.example.internal" \
  -out server.csr

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 825 -sha256

클라이언트 인증서 (역할을 OU로 구분 추천)

SOC 에이전트용

openssl genrsa -out soc_client.key 4096
openssl req -new -key soc_client.key \
  -subj "/C=KR/O=PagesKR/OU=SOC_ASSISTANT/CN=soc-agent-01" \
  -out soc_client.csr

openssl x509 -req -in soc_client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out soc_client.crt -days 365 -sha256

DEV 에이전트용

openssl genrsa -out dev_client.key 4096
openssl req -new -key dev_client.key \
  -subj "/C=KR/O=PagesKR/OU=DEV_ASSISTANT/CN=dev-agent-01" \
  -out dev_client.csr

openssl x509 -req -in dev_client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out dev_client.crt -days 365 -sha256

실무 팁

  • OU(조직단위) 또는 인증서 확장 필드에 role을 심어 두면, NGINX/MCP에서 role 매핑이 쉬워집니다.
  • 인증서 폐기는 CRL/OCSP 도입 또는 “짧은 만료 + 자동 갱신” 전략이 운영상 편합니다.

NGINX 앞단 구성 (mTLS + WebSocket + Zero Trust)

핵심: MCP 서버는 내부에만 바인딩

  • MCP 서버: 127.0.0.1:8765 (또는 내부 전용 VLAN IP)
  • 외부에서 8765 접근은 방화벽/보안그룹으로 차단
300x250

NGINX http 블록(WebSocket reverse proxy) 예시

# /etc/nginx/nginx.conf (중요 부분만 예시)
http {
  map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
  }

  # (A) Rate limit (클라이언트별 요청 제한)
  limit_req_zone $binary_remote_addr zone=mcp_req:10m rate=10r/s;
  limit_conn_zone $binary_remote_addr zone=mcp_conn:10m;

  upstream mcp_ws_backend {
    server 127.0.0.1:8765;
  }

  upstream mcp_admin_backend {
    server 127.0.0.1:8000;
  }

  server {
    listen 9443 ssl;
    server_name mcp.example.internal;

    # 서버 인증서
    ssl_certificate     /etc/nginx/certs/server.crt;
    ssl_certificate_key /etc/nginx/certs/server.key;

    # mTLS: 클라이언트 인증서 검증
    ssl_client_certificate /etc/nginx/certs/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # (B) 요청 크기 제한(과도한 payload 방지)
    client_max_body_size 1m;

    # (C) 기본 보안 헤더(필요 시)
    add_header X-Content-Type-Options nosniff;

    # ---------------------------
    # MCP WebSocket
    # ---------------------------
    location /mcp/ws {
      # Zero Trust: 접속망 제한(예: 사내망/VPN만)
      # allow 10.0.0.0/8;
      # deny all;

      limit_req  zone=mcp_req burst=20 nodelay;
      limit_conn mcp_conn 20;

      proxy_pass http://mcp_ws_backend;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;

      # ★ 헤더 스푸핑 방지: 클라이언트가 보내더라도 항상 덮어쓰기
      proxy_set_header X-Client-Verify $ssl_client_verify;
      proxy_set_header X-Client-DN     $ssl_client_s_dn;
      proxy_set_header X-Client-CN     $ssl_client_s_dn_cn;
      proxy_set_header X-Client-OU     $ssl_client_s_dn_ou;

      # (옵션) 요청 추적
      proxy_set_header X-Request-ID $request_id;
    }

    # ---------------------------
    # Admin API (훨씬 더 엄격하게)
    # ---------------------------
    location /admin/ {
      # 강력 권장: Admin은 별도 Bastion/VPN IP만 허용
      # allow 10.10.10.10;
      # deny all;

      # 추가 인증(옵션): Basic Auth / JWT / OIDC 등
      # auth_basic "Restricted";
      # auth_basic_user_file /etc/nginx/.htpasswd;

      proxy_pass http://mcp_admin_backend;
      proxy_set_header X-Client-DN $ssl_client_s_dn;
      proxy_set_header X-Request-ID $request_id;
    }
  }
}

NGINX Zero Trust 체크 포인트

  • mTLS on이 1차 필수
  • /admin/은 별도 포트/호스트로 분리하면 더 안전
  • proxy_set_header로 인증 컨텍스트를 서버가 주입하고, MCP는 이 헤더만 신뢰 (클라가 보내는 값 신뢰 금지)

MCP 서버 설계: “동적 레지스트리 + RBAC + Guardrail”

동적 구성 데이터 모델

권장 구성 파일/테이블(예: DB/JSON)

  1. tools
  • name, description, inputSchema, handlerType, handlerConfig, tags, riskLevel
  1. resources
  • uri, name, mimeType, backendType(file/db/http), backendConfig, sensitivity
  1. roles (RBAC)
  • roleName, allowedTools[], allowedResourcePrefixes[], denyRules 등
  1. guardrail_rules
  • toolName별 allow/deny 조건, arg validation, max limit, forbidden patterns 등
  1. audit_logs
  • request_id, client_dn, role, method, tool/resource, args hash, result status, latency

세션 role 결정 로직

  • NGINX가 넣어주는 X-Client-OU를 role로 매핑
    • OU=SOC_ASSISTANT → role=SOC_ASSISTANT
  • 또는 DN/CN을 키로 내부 DB에서 role lookup (더 유연)

중요한 점: “initialize params로 role을 받는 방식”은 조작 가능하므로
반드시 mTLS 인증서/NGINX 주입 헤더 기반으로 role을 결정하세요.

Server-level Guardrail (RBAC)

  • tools/list에서 role 허용 목록만 노출
  • tools/call에서 role 허용 여부 재검증
  • resources/list/read에서도 prefix 기반 허용 여부 확인

Tool-level Guardrail: analyze_tool_request + 입력정책 엔진

서버 레벨이 “어떤 툴/리소스를 쓸 수 있나”를 막는다면, 툴 레벨은 “어떤 입력으로 실행할 수 있나”를 통제합니다.

Guardrail 정책 예시(JSON)

예: guardrail.json

{
  "defaults": {
    "maxLimit": 1000,
    "maxTimeRangeDays": 30
  },
  "tools": {
    "search_logs": {
      "rules": [
        {"type": "limit_max", "value": 200},
        {"type": "time_range_days_max", "value": 14},
        {"type": "deny_pattern", "field": "query", "pattern": "(?i)secret|password|api[_-]?key"}
      ]
    },
    "compare_policies": {
      "rules": [
        {"type": "allow_uri_prefix", "field": "oldPolicyUri", "prefix": "policy://security/"},
        {"type": "allow_uri_prefix", "field": "newPolicyUri", "prefix": "policy://security/"}
      ]
    }
  }
}

Guardrail 동작 방식

  • tools/call을 받으면:
    1. role 허용 tool인지 확인 (server-level)
    2. tool별 guardrail 규칙 적용
      • limit 상한 조정(자동 sanitize)
      • 기간 상한 조정
      • 금칙 패턴 탐지 시 deny
    3. 최종 sanitized args로 실제 handler 실행

운영 팁

  • “deny”만 하면 사용자 경험이 나빠지니, 가능한 경우 review/sanitize로 자동 수정 후 실행하는 패턴이 실용적입니다.
  • 다만 보안 고위험(예: 삭제/변경)은 무조건 deny 또는 별도 강한 승인 경로로 분리하세요.

“동적으로 툴/리소스를 추가/수정”하는 운영 구조

Admin API로 CRUD

  • POST /admin/tools 새로운 tool 정의 추가
  • PUT /admin/tools/{name} 수정
  • POST /admin/resources 리소스 추가
  • PUT /admin/resources/{uri} 수정
  • POST /admin/roles 역할 정책 변경
  • POST /admin/guardrail 가드레일 규칙 변경

MCP 서버 반영 방식(추천 2가지)

방식 A: 즉시 반영 (Registry 메모리 갱신)
  • Admin API가 변경 시 Registry 업데이트
  • 장점: 즉시 반영, 단순
  • 주의: 동시성/롤백/검증 필요
방식 B: 버전 기반 반영 (config version + reload)
  • DB에 config_version을 두고
  • Admin API가 변경하면 version 증가
  • MCP 서버는 주기적으로 version 체크 후 reload
  • 장점: 안전, 롤백 용이(버전 되돌리기)
  • 운영에 더 적합

로깅/감사(Audit) 설계

반드시 남겨야 할 필드

  • request_id (NGINX에서 주입)
  • client_dn / client_cn / role
  • method (tools/call, resources/read 등)
  • 대상 (tool name, resource uri)
  • arguments (원문 저장은 민감할 수 있으니 해시+요약 권장)
  • 결과 status (allow/deny/error)
  • latency, backend call id

경보(탐지) 포인트 예시

  • 짧은 시간에 resources/read 대량 호출
  • role 불일치 접근 시도(DEV가 security 정책 full 요청 등)
  • guardrail deny가 급증
  • 과도한 limit/timeRange 반복 요청

배포/운영 체크리스트

네트워크/배포

  • MCP는 127.0.0.1에만 바인딩
  • NGINX만 외부(또는 VPN) 노출
  • /admin은 별도 네트워크 경로/포트/서브도메인 분리
  • 방화벽/보안그룹에서 MCP 포트 직접 접근 차단

인증/인가

  • mTLS on, CA 관리 체계(발급/폐기/만료)
  • 인증서 DN/OU 기반 role 매핑
  • role별 tools/resources 허용 목록 최소화

Guardrail

  • tool별 limit/timeRange 상한
  • 위험 패턴 차단(민감 키워드, 과도한 wildcard 등)
  • 변경/삭제 계열 tool은 별도 승인/분리(가능하면 MCP에 직접 노출 금지)

로깅/모니터링

  • NGINX access log + MCP audit log 분리 수집
  • deny/error 급증 알림
  • rate limit hit 알림

빠른 테스트 방법

인증서로 접속 가능한지(개념)

WebSocket 클라이언트(예: wscat류)를 사용할 때 인증서 지정이 가능해야 합니다.
(툴마다 옵션이 달라서, 보통은 프록시 뒤에서 테스트용 클라이언트를 준비합니다.)

NGINX에서 mTLS 강제 확인

  • 클라이언트 인증서 없이 접속 시 TLS handshake 단계에서 실패해야 정상
  • access log에 $ssl_client_verifySUCCESS로 찍히는지 확인

추천 “최소 안전” 표준 구성

  1. NGINX(mTLS) + rate limit + IP allowlist로 1차 차단
  2. MCP 서버는 동적 registry(DB/JSON) 기반으로 tools/resources 제공
  3. MCP 내부에 server-level RBAC + tool-level guardrail
  4. Admin API는 최고 보안 구역(별도 네트워크, mTLS+추가 인증, 변경 이력/승인)
  5. 전 구간 audit log를 남겨서 사후 추적/탐지 연계
728x90
그리드형(광고전용)

댓글