Назад в блог

mTLS для MCP: когда взаимный TLS сильнее OAuth и как его внедрить

mTLS выносит секрет из header в handshake: 4 шага, когда стоит и когда нет, ротация и сочетании с OAuth для multi-tenant MCP — по спекам 2025-11-25/2026-07-28.

Иллюстрация к материалу: mTLS для MCP: когда взаимный TLS сильнее OAuth и как его внедрить

Bearer в Authorization удобен, но копируется в логи, кэши и историю. mTLS (mutual TLS) убирает секрет из запроса в TLS-handshake: агент показывает X.509 сертификат до первого MCP-сообщения. Повтора из лога нет, но и завести его сложнее — нужен PKI.

Что такое mTLS для MCP

mTLS — взаимная аутентификация на транспорте: и агент (клиент), и MCP-сервер (или прокси перед ним) предъявляют сертификаты, подписанные доверенным CA. Спецификации MCP 2025-11-25 и 2026-07-28 делают authorization OPTIONAL и SHOULD по OAuth (RFC 8707, 6750, 9728 и др.), но не описывают mTLS — он вне спеки, а не против неё. Значит: работает, но не discoverable — ни один tools/list не скажет «тут нужен сертификат», каждый caller настраивается руками.

Сертификат живёт в tls.Config (Go), ssl.SSLContext (Python), undici Agent (TypeScript) — никогда в MCP payload. Запрос POST /mcp tools/list уходит без Authorization: Bearer.

Handshake в 4 шага

  1. Агент → сервер: ClientHello с SNI mcp.example.com.
  2. Сервер → агент: CertificateRequest с листом допустимых CA.
  3. Агент → сервер: Certificate + CertificateVerify (CN=orders-agent, issuer — ваш CA).
  4. Сервер проверяет цепочку до доверенного CA, читает subject — соединение аутентифицировано. Только теперь идёт первый MCP message, уже без credential.

Скопированный из лога запрос без приватного ключа не воспроизведётся — секрет привязан к соединению, а не к header.

Когда стоит / не стоит

Стоит, если:

  • оба конца — ваши workloads с уже работающим CA;
  • сеть — интернет или широкая внутренняя, нужен транспорт-уровень до приложения;
  • compliance требует клиентский сертификат на каждое соединение;
  • хотите убрать replayable secret из request.

Не стоит, если:

  • к серверу должен попасть человек из чата: Claude, ChatGPT, Cursor, VS Code — не умеют подставлять клиентский сертификат (Claude docs — 6 типов auth без сертификата, ChatGPT — нельзя, Cursor/VS Code — url/headers/oauth без cert);
  • нужен per-user identity — сертификат идентифицирует workload, а не человека, для on behalf of нужен OAuth;
  • некому ротировать — expiry валит handshake до любого MCP-кода, и в логах — транспортная ошибка, а не 401.

Ротация и отзыв

Сертификаты истекают — handshake падает. План:

  • 90-дневный cert → ротация на 60-й день, 30 дней запас;
  • дистрибуция через ACME / Vault PKI / CM;
  • сервер доверяет двум CA в переходный период;
  • для отзыва — CRL или OCSP, для маленького флота проще перевыпустить CA и рестарт.

Сочетание с OAuth: defense in depth

mTLS и bearer — разные слои. Практика: terminate mTLS на edge-прокси, за ним — OAuth по спеке MCP. Прокси валидирует сертификат и прокидывает X-Client-Cert-CN в backend, backend уже решает user × scope. RFC 8705 (mTLS-bound token) формально не в списке MCP, поэтому их просто ставят рядом: mTLS = «какая машина», OAuth = «какой пользователь и какие права». Украденный bearer без сертификата бесполезен.

Где Codenik

Codenik gateway ставит mTLS там где он уместен: для workload-to-workload вызовов между вашими агентами и приватными MCP — mTLS на edge (Go/Python/TS агенты). Для чатов и desktop-клиентов — второй маршрут через OAuth с discovery (RFC 8414, DCR). Один и тот же MCP за двумя дверями: сертификат для машин, токен для людей, оба — в единый аудит agent_id × human_delegator × tool.

Короткий вывод

mTLS — afternoon в коде если PKI уже есть, и проект если нет. Берите его для своих агентов где нужен транспорт-без секрета, оставляйте OAuth для людей. Тогда replay из лога не работает, а человек всё равно проходит по своим правам.

Источники и дальнейшее чтение