MCP 제로 트러스트 인증

MCP 제로 트러스트 인증 (JWT 서명) (Zero Trust Auth (JWT Signer))

MCP 서버는 요청이 실제로 LiteLLM을 거쳤는지 검증할 내장 방법이 없어요. 이 가드레일이 없으면 MCP 서버에 직접 도달할 수 있는 어떤 클라이언트든 도구를 호출할 수 있고, 접근 제어를 완전히 우회할 수 있어요.

MCPJWTSigner가 이를 해결해요. 모든 아웃바운드 도구 호출에 단기 RS256 JWT를 서명해요. MCP 서버는 LiteLLM의 공개 키로 서명을 검증해요. LiteLLM을 거치지 않은 요청은 유효한 서명이 없어 거부돼요.

출처: 문서

본문


기본 설정 (Basic setup)

config에 가드레일을 추가하고 MCP 서버를 LiteLLM의 JWKS 엔드포인트에 연결하세요. 모든 도구 호출에 클라이언트 측 변경 없이 서명된 JWT가 자동 생성돼요.

config.yaml:

mcp_servers:
  - server_name: weather
    url: http://localhost:8000/mcp
    transport: http

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true
      issuer: "https://my-litellm.example.com"  # defaults to request base URL
      audience: "mcp"                            # default: "mcp"
      ttl_seconds: 300                           # default: 300

자체 서명 키 가져오기 (Bring your own signing key). 자동 생성 키는 재시작 시 손실되므로 프로덕션에 권장돼요.

export MCP_JWT_SIGNING_KEY="-----BEGIN RSA PRIVATE KEY-----\n..."
# or point to a file
export MCP_JWT_SIGNING_KEY="file:///secrets/mcp-signing-key.pem"

FastMCP로 검증된 MCP 서버 구축:

weather_server.py:

from fastmcp import FastMCP, Context
from fastmcp.server.auth.providers.jwt import JWTVerifier

auth = JWTVerifier(
    jwks_uri="https://my-litellm.example.com/.well-known/jwks.json",
    issuer="https://my-litellm.example.com",
    audience="mcp",
    algorithm="RS256",
)

mcp = FastMCP("weather-server", auth=auth)

@mcp.tool()
async def get_weather(city: str, ctx: Context) -> str:
    caller = ctx.client_id  # JWT `sub` — the verified user identity
    return f"Weather in {city}: sunny, 72°F (requested by {caller})"

if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)

FastMCP는 JWKS를 자동 가져오고 서명 키가 바뀌면 다시 가져와요.

LiteLLM은 OIDC 디스커버리를 게시해 MCP 서버가 수동 구성 없이 키를 찾게 해요:

GET /.well-known/openid-configuration  →  { "jwks_uri": "https://<litellm>/.well-known/jwks.json" }
GET /.well-known/jwks.json             →  { "keys": [{ "kty": "RSA", "alg": "RS256", ... }] }

필요할 때만 더 읽으세요: 기업 IdP 신원을 JWT에 넣기, 호출자에 특정 클레임 강제, 사용자 지정 메타데이터 추가, AWS Bedrock AgentCore Gateway 사용, JWT 거부 디버깅.


IdP 신원을 MCP JWT에 넣기 (Thread IdP identity into MCP JWTs)

기본적으로 아웃바운드 JWT sub는 LiteLLM의 내부 user_id예요. 사용자가 Okta, Azure AD 또는 다른 IdP로 인증하면 MCP 서버는 사용자 이메일이나 직원 ID가 아닌 LiteLLM 내부 ID를 봐요.

verify+re-sign으로 LiteLLM이 먼저 들어오는 IdP 토큰을 검증한 다음 실제 신원 클레임을 사용해 아웃바운드 JWT를 구축해요. MCP 서버는 원래 IdP를 직접 신뢰하지 않으면서 사용자의 실제 신원을 얻게 돼요.

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true
      issuer: "https://my-litellm.example.com"

      # Validate the incoming Bearer token against the IdP
      access_token_discovery_uri: "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration"
      verify_issuer: "https://login.microsoftonline.com/{tenant}/v2.0"
      verify_audience: "api://my-app"

      # Which claim to use for `sub` in the outbound JWT — first non-empty value wins
      end_user_claim_sources:
        - "token:sub"       # from the verified incoming JWT
        - "token:email"     # fallback to email
        - "litellm:user_id" # last resort: LiteLLM's internal user_id

들어오는 토큰이 JWT가 아니라 opaque(일부 IdP가 발행)라면 인트로스펙션 엔드포인트를 추가하세요. LiteLLM이 토큰을 POST하고(RFC 7662) 반환된 클레임을 사용해요:

      token_introspection_endpoint: "https://idp.example.com/oauth2/introspect"

지원되는 end_user_claim_sources 값:

소스 해석
token:<claim> 검증된 들어오는 JWT의 모든 클레임(예: token:sub, token:email, token:oid)
litellm:user_id LiteLLM의 내부 사용자 ID
litellm:email LiteLLM 인증 컨텍스트의 사용자 이메일
litellm:end_user_id 별도로 설정된 경우 최종 사용자 ID
litellm:team_id LiteLLM 인증 컨텍스트의 팀 ID

필수 속성이 없는 호출자 차단 (Block callers missing required attributes)

일부 MCP 서버는 검증된 직원만 도달할 수 있어야 하는 민감 작업을 노출해요(서비스 계정이나 외부 API 키는 안 됨). 이를 LiteLLM 레이어에서 강제해 MCP 서버가 요청을 전혀 받지 않게 할 수 있어요.

required_claims는 들어오는 토큰에 나열된 클레임 중 하나라도 없으면 403으로 거부해요. optional_claims는 유용하지만 필수는 아닌 클레임을 전달해요.

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true

      access_token_discovery_uri: "https://idp.example.com/.well-known/openid-configuration"

      # Service accounts without `employee_id` are blocked before the tool runs
      required_claims:
        - "sub"
        - "employee_id"

      # Forward these into the outbound JWT when present — skipped silently if absent
      optional_claims:
        - "groups"
        - "department"

차단 시 클라이언트가 보는 것:

HTTP 403
{ "error": "MCPJWTSigner: incoming token is missing required claims: ['employee_id']. Configure the IdP to include these claims." }

모든 JWT에 사용자 지정 메타데이터 추가 (Add custom metadata to every JWT)

MCP 서버가 LiteLLM이 네이티브로 가지지 않는 컨텍스트(어떤 배포가 요청을 보냈는지, 테넌트 ID, 환경 태그)를 필요로 할 수 있어요. 클레임 연산(claim operations)으로 아웃바운드 JWT의 클레임을 주입·재정의·제거할 수 있어요.

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true

      # add: insert only when the key is not already in the JWT
      add_claims:
        deployment_id: "prod-us-east-1"
        tenant_id: "acme-corp"

      # set: always override — even if the claim came from the incoming token
      set_claims:
        env: "production"

      # remove: strip claims the MCP server shouldn't see
      remove_claims:
        - "nbf"   # some validators reject nbf; remove it if yours does

연산은 add_claimsset_claimsremove_claims 순서로 실행돼요. set_claims는 항상 add_claims보다 우선하고 remove_claims가 둘 다를 이겨요.


AWS Bedrock AgentCore Gateway

Bedrock AgentCore Gateway는 두 개의 별도 JWT를 사용해요. 하나는 전송 연결 인증용, 다른 하나는 도구 호출 권한 부여용이에요. 서로 다른 aud 값과 TTL이 필요하므로 단일 JWT로는 둘 다 작동하지 않아요.

LiteLLM은 하나의 훅에서 둘 다 발행하고 별도 헤더에 주입할 수 있어요:

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true
      issuer: "https://my-litellm.example.com"
      audience: "mcp-resource"   # for the MCP resource layer
      ttl_seconds: 300

      # Second JWT for the transport channel — same sub/act/scope, different aud + TTL
      channel_token_audience: "bedrock-agentcore-gateway"
      channel_token_ttl: 60      # transport tokens should be short-lived

LiteLLM은 모든 도구 호출에 두 헤더를 주입해요:

  • Authorization: Bearer *** — audience mcp-resource, TTL 300s
  • x-mcp-channel-token: Bearer <channel-token> — audience bedrock-agentcore-gateway, TTL 60s

두 토큰 모두 같은 LiteLLM 키로 서명되므로 MCP 서버는 JWKS 엔드포인트 하나만 신뢰하면 돼요.


JWT에 들어갈 스코프 제어 (Control which scopes go into the JWT)

기본적으로 LiteLLM은 요청별 최소 권한 스코프를 생성해요:

  • 도구 호출 → mcp:tools/call mcp:tools/{name}:call
  • 도구 목록 → mcp:tools/call mcp:tools/list

MCP 서버가 자체 스코프 강제를 하고 특정 형식이 필요하다면 allowed_scopes를 설정해 자동 생성을 완전히 대체하세요:

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true

      allowed_scopes:
        - "mcp:tools/call"
        - "mcp:tools/list"
        - "mcp:admin"

어떤 도구가 호출되든 모든 JWT가 정확히 그 스코프를 가집니다.


JWT 거부 디버깅 (Debug JWT rejections)

MCP 서버가 401을 반환하고 JWT 안에 무엇이 있는지 모르겠다면 debug_headers를 활성화하세요. LiteLLM이 서명된 키 클레임을 담은 x-litellm-mcp-debug 응답 헤더를 추가해요:

config.yaml:

guardrails:
  - guardrail_name: mcp-jwt-signer
    litellm_params:
      guardrail: mcp_jwt_signer
      mode: pre_mcp_call
      default_on: true
      debug_headers: true

응답 헤더:

x-litellm-mcp-debug: v=1; kid=a3f1b2c4d5e6f708; [email protected]; iss=https://my-litellm.example.com; exp=1712345678; scope=mcp:tools/call mcp:tools/get_weather:call

kid가 MCP 서버가 JWKS에서 가져온 것과 일치하는지, iss/aud가 서버의 기대값과 일치하는지, exp가 지나지 않았는지 확인하세요. 헤더가 클레임 메타데이터를 누출하므로 프로덕션에서는 비활성화하세요.


JWT 클레임 참조 (JWT claims reference)

클레임
iss issuer config 값(또는 요청 기본 URL)
aud audience config 값(기본값: "mcp")
sub end_user_claim_sources로 해석(기본: user_id → api-key hash → "litellm-proxy")
act.sub team_idorg_id"litellm-proxy"(RFC 8693 위임)
email LiteLLM 인증 컨텍스트의 user_email(가능할 때)
scope 도구 호출별 자동 생성, 또는 설정 시 allowed_scopes
iat, exp, nbf 표준 타이밍 클레임(RFC 7519)

제한 사항 (Limitations)

  • OpenAPI 기반 MCP 서버(spec_path 설정)는 JWT 주입을 지원하지 않아요. LiteLLM이 경고를 기록하고 헤더를 건너뛰어요. 전체 JWT 주입을 위해 SSE/HTTP 전송 서버를 사용하세요.
  • 키페어는 기본적으로 인메모리이며 MCP_JWT_SIGNING_KEY가 설정되지 않으면 재시작할 때마다 회전돼요. FastMCP의 JWTVerifier는 JWKS 키 ID 매칭으로 키 회전을 투명하게 처리해요.

더 알아보기 (Learn more)