Fastify v5とMCP v2.0 — ステートレスAIバックエンドにおけるセキュリティ実装

Maru

@maru

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

Fastify v5とMCP v2.0 — ステートレスAIバックエンドにおけるセキュリティ実装

Model Context Protocol (MCP) v2.0の仕様策定により、従来のコネクション指向のセッション維持方式から脱却し、標準HTTPベースのステートレス・アーキテクチャへの移行が本格化しました。このステートレス設計によりサーバーの水平スケーリングは非常に容易になりますが、一方で、リクエスト間のデータ改ざんやトークンの悪用といった新たなセキュリティ上の脅威も伴います。本稿では、高性能なNode.js WebフレームワークであるFastify v5とネイティブ暗号化技術を組み合わせ、ロードバランサー配下でも大規模リクエストを安全に処理可能なステートレスAIバックエンドの構築方法を検討します。

MCP v2.0のステートレス移行とマルチリクエストにおけるセキュリティ課題

Model Context Protocol (MCP) v2.0の仕様では、従来のコネクション指向の初期化プロセスやセッションIDヘッダーを廃止し、標準HTTPベースのステートレス・アーキテクチャが採用されました。すべてのツールリクエストは独立したHTTP呼び出しとして処理されるため、サーバーは水平スケーラビリティとロードバランシングに適した、堅牢なマイクロサービスとして動作可能です。

しかし、複数回の対話が必要となる大規模言語モデル(LLM)ツールの実行においては、サーバーがステートレス性を維持するために現在の進行状態を外部に保持させるという構造的問題が生じます。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サーバーが連携するエンタープライズ環境では、特定のサーバー用に発行された権限トークンが別のサーバーで悪用される「Confused Deputy」攻撃を遮断しなければなりません。セキュリティレベルの低いツールサーバー用のトークンを盗み出し、権限の高い他のコアサーバーに再利用するというシナリオが代表的です。

これを防ぐため、MCP v2.0ではRFC 8707リソースインジケーター仕様を適用します。クライアントは認可サーバーへトークンをリクエストする際、対象サーバーの固有識別子であるCanonical URIを指定する必要があり、認可サーバーはその識別子を対象者(Audience)として明示した専用トークンを発行します。

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ベースのログ管理体系へと移行することをお勧めします。


参考リンク