Fastify v5와 MCP v2.0 — 무상태 AI 백엔드 보안 구현하기

Maru

@maru

Fastify v5와 MCP v2.0 — 무상태 AI 백엔드 보안 구현하기

Fastify v5와 MCP v2.0 — 무상태 AI 백엔드 보안 구현하기

모델 컨텍스트 프로토콜(MCP) v2.0 규격이 확정되면서, 기존의 연결 지향적인 세션 유지 방식에서 벗어나 표준 HTTP 기반의 무상태 아키텍처로의 전환이 본격화되었습니다. 이러한 무상태 설계는 서버의 수평 확장을 매우 쉽게 만들어주지만, 다회차 요청 간의 데이터 변조와 토큰 오용 같은 새로운 보안 위협을 동반합니다. 고성능 Node.js 웹 프레임워크인 Fastify v5와 네이티브 암호화 기술을 결합하여, 로드밸런서 뒤에서도 안전하게 대규모 요청을 처리할 수 있는 무상태 AI 백엔드 구축 방법을 고민해 봅니다.

MCP v2.0의 무상태 전환과 다회차 요청 보안 과제

Model Context Protocol(MCP) v2.0 명세는 기존의 연결 지향적인 초기화 과정과 세션 ID 헤더를 제거하고, 표준 HTTP 기반의 무상태 아키텍처를 도입했습니다. 이제 모든 도구 요청은 독립적인 HTTP 호출로 처리되므로, 서버는 수평 확장성과 로드밸런싱이 용이한 현대적인 마이크로서비스 형태로 견고하게 동작할 수 있습니다.

하지만 다회차 상호작용이 필요한 대형 언어 모델 도구 실행의 경우, 서버는 무상태성을 유지하기 위해 현재의 진행 상태를 외부에 노출해야 하는 구조적 문제를 안게 됩니다. MCP v2.0은 사용자 입력이 추가로 필요한 상황에서 input_required 결과 상태와 함께 requestState라는 불투명한 문자열에 중간 상태 정보를 담아 클라이언트에 전달하고, 다음 단계에서 이를 그대로 되돌려 받는 다회차 요청 방식을 통해 이 문제를 해결합니다.

이 과정에서 클라이언트나 호스트 에이전트가 requestState 내부의 임시 데이터나 권한 범위를 임의로 수정하여 서버에 재전송할 위험이 존재합니다. 이처럼 신뢰할 수 없는 클라이언트가 중간 상태 값을 악용하여 비정상적인 권한을 획득하는 혼동된 대리인(Confused Deputy) 공격을 방어하려면, 외부로 전달되는 상태 값의 기밀성과 무결성을 보장하는 강력한 암호화 보호 설계가 백엔드에 필수적으로 구현되어야 합니다.

Node.js Crypto를 이용한 requestState 대칭 암호화와 서명

외부로 반출되는 requestState 파라미터의 기밀성과 무결성을 동시에 확보하려면 AES-256-GCM 대칭 키 암호화를 적용해야 합니다. MCP 공식 적합성 테스트가 사용하는 HMAC-SHA256 방식은 변조를 막아주지만 세션 정보가 클라이언트에 평문으로 노출된다는 보안상 한계가 있습니다. 다중 서버 인스턴스가 투입되는 로드밸런서 환경에서 복호화가 끊김 없이 작동하려면, 서버 메모리가 아닌 환경 변수에 32바이트 대칭 키를 정적으로 주입하여 관리하는 무상태 설계가 필수적입니다.

Node.js 내장 node:crypto 모듈을 사용해 대칭 키 암호화와 인증 태그 검증을 구현한 예시입니다.

typescript
import crypto from 'node:crypto';

const ALGO = 'aes-256-gcm';
const KEY = Buffer.from(process.env.MCP_STATE_KEY || '', 'hex'); // 32바이트 고정 키

export function encryptState(payload: string): string {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv(ALGO, KEY, iv);
  let encrypted = cipher.update(payload, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const tag = cipher.getAuthTag().toString('hex');
  return `${iv.toString('hex')}:${tag}:${encrypted}`;
}

export function decryptState(token: string): string {
  const [ivHex, tagHex, encrypted] = token.split(':');
  const decipher = crypto.createDecipheriv(ALGO, KEY, Buffer.from(ivHex, 'hex'));
  decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

이 구조를 사용하면 외부에서 값이 미세하게 변조되는 즉시 복호화 단계의 decipher.final() 호출에서 예외가 차단되어, 무상태 아키텍처의 최대 보안 위협인 Confused Deputy 공격을 사전에 무력화할 수 있습니다.

Fastify v5 보안 통합 및 LogController 적용 패턴

Fastify v5와 TypeBox의 결합은 고성능 무상태 백엔드를 설계할 때 최상의 타입 안전성과 직렬화 속도를 보장합니다. MCP v2.0의 HTTP 전환으로 인해 모든 도구 요청은 매번 완벽한 구조적 유효성 검사를 거쳐야 하며, 이를 선언적으로 처리하는 스키마 엔진이 필수적입니다. TypeBox를 활용하면 컴파일 타임의 정적 타입 지원과 Fastify 내부의 고성능 JSON 스키마 엔진을 동시에 누릴 수 있습니다.

보안 영역에서는 OAuth 2.1 보호 자원 메타데이터 명세를 지원하는 실전적인 흐름을 구성해야 합니다. 인증 토큰 없이 유효하지 않은 요청이 도달하면 401 Unauthorized 응답과 함께 WWW-Authenticate 헤더에 resource_metadata 매개변수를 실어 반환합니다. 이 방식을 통해 에이전트 클라이언트는 하드코딩된 설정에 의존하지 않고, 런타임에 필요한 인증 서버 스펙과 토큰 요구 사항을 동적으로 탐색하고 획득할 수 있습니다.

또한 Fastify v5.10.0에서 정식 도입된 LogController 클래스를 활용하면 무상태 서버 인프라의 로깅 효율성을 극대화할 수 있습니다. 기존의 최상위 로깅 관련 비활성화 옵션들은 지원 중단 단계를 거쳐 향후 v6 버전에서 완전히 제거될 예정입니다. 따라서 아래의 실전 코드처럼 LogController를 상속받아 헬스체크 경로와 같이 불필요한 요청 로그를 제거하고, 정밀하게 요청과 응답을 통제하는 아키텍처를 도입해야 합니다.

typescript
import Fastify, { LogController, FastifyRequest } from 'fastify';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';

// Fastify v5.10.0 이상에서 권장하는 LogController 상속 구현
class CustomLogController extends LogController {
  override isLogDisabled(request: FastifyRequest): boolean {
    // 헬스체크 경로의 로그 기록을 제외하여 무상태 서버의 리소스 낭비 방지
    return request.url === '/health';
  }
}

const app = Fastify({
  logger: true,
  logController: new CustomLogController()
}).withTypeProvider<TypeBoxTypeProvider>();

// TypeBox를 사용한 도구 등록 요청 스키마 정의
const registerToolSchema = {
  body: Type.Object({
    toolName: Type.String(),
    requestState: Type.Optional(Type.String())
  })
};

// MCP v2.0 registerTool 동작을 처리하는 보호된 엔드포인트
app.post('/registerTool', { schema: registerToolSchema }, async (request, reply) => {
  const { toolName, requestState } = request.body;
  const authHeader = request.headers.authorization;

  // OAuth 2.1 보호 자원 메타데이터 인증 흐름 대응
  if (!authHeader) {
    return reply
      .status(401)
      .header(
        'WWW-Authenticate',
        'Bearer error="invalid_token", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
      )
      .send({ error: '인증이 필요합니다.' });
  }

  // requestState 복호화 및 유효성 확인 검증
  if (requestState) {
    request.log.info({ toolName }, '요청 상태 서명 검증 후 도구를 등록합니다.');
  }

  return { success: true, tool: toolName };
});

이 패턴은 @fastify/type-provider-typebox 플러그인을 사용하여 별도의 어댑터 없이도 런타임 검증과 TypeScript 정적 타입을 일치시킵니다. WWW-Authenticate 헤더를 통해 자원 발견 과정을 자동화하는 동시에, 새로운 로그 컨트롤러 계층을 통해 고부하 에이전트 환경에서도 런타임 오버헤드와 로그 수집 비용을 크게 낮출 수 있습니다.

다중 서버 에이전트 환경에서의 토큰 재사용 방지

여러 개의 독립적인 MCP 서버가 협업하는 엔터프라이즈 환경에서는 특정 서버를 위해 발급된 권한 토큰이 다른 서버에서 오용되는 혼동된 대리자 공격을 차단해야 합니다. 클라이언트가 보안 수준이 낮은 도구 서버용 토큰을 탈취하여 권한이 높은 다른 핵심 서버에 재사용하는 시나리오가 대표적입니다.

이를 방지하기 위해 MCP v2.0은 RFC 8707 리소스 인디케이터 규격을 적용합니다. 클라이언트는 인가 서버에 토큰을 요청할 때 대상 서버의 고유 식별자인 Canonical URI를 지정해야 하며, 인가 서버는 이 식별자가 대상자로 명시된 전용 토큰을 발행합니다.

Fastify 백엔드에서는 라우트 진입 전 preHandler 훅을 통해 유입된 JWT의 대상자(aud) 클레임이 자사의 Canonical URI와 정확히 일치하는지 대조하는 검증 로직이 필수적입니다.

typescript
// Fastify v5 JWT 대상자 검증 훅 예시
fastify.addHook('preHandler', async (request, reply) => {
  const token = request.headers.authorization?.split(' ')[1];
  if (!token) {
    return reply.code(401).send({ error: 'Missing token' });
  }
  
  const payload = fastify.jwt.verify(token);
  if (payload.aud !== process.env.CANONICAL_SERVER_URI) {
    return reply.code(403).send({ error: 'Audience mismatch' });
  }
});

이와 같은 대상자 검증을 전역 또는 라우트별 훅으로 적용하면, 무상태 아키텍처 환경에서도 다중 에이전트 간의 교차 토큰 오용을 안전하게 예방할 수 있습니다.

확장성 있는 AI 에이전트 인프라로의 진화

MCP v2.0의 무상태 HTTP 설계 전환은 AI 에이전트 인프라를 표준 마이크로서비스 아키텍처로 확장할 수 있는 길을 열었습니다. 고성능 Fastify v5 프레임워크와 네이티브 암호화 기반의 상태 보호 기법을 결합하면 대규모 요청 환경에서도 안전하고 지연 시간 없는 에이전트 백엔드를 설계할 수 있습니다. 기존의 세션 유지 방식 도구를 운영 중이라면, 향후 12개월 동안 제공되는 유예 기간을 활용해 점진적으로 무상태 환경과 OpenTelemetry 기반 로깅 체계로 전환하는 것을 권장합니다.


참고 링크