Главная / Статьи

Двойные границы OAuth для шлюзов MCP: защита идентичности пользователя

Шлюзы MCP не должны объединять все вызовы в один учётный аккаунт сервиса. Входная проверка в сочетании с обменом данными от имени OAuth сохраняет целостность идентификатора каждого пользователя.

Издатель: REACTAPP.TOP11 мин чтения2444 слов

Источники: Solving the Identity Termination Problem in MCP Gateway Architectures

Шлюз, расположенный между агентом, управляемым MCP, и корпоративными системами, должен иметь два границы OAuth, чтобы лицо, сделавшее запрос, оставалось идентифицируемым в каждой системе, с которой взаимодействует агент.

Сбой

Рассмотрим типичный сбой. Сотрудник через шлюз MCP просит агента оформить заявку от имени коллеги. Шлюз проверяет сотрудника в системе корпоративной идентификации, верифицирует токен-носитель и передаёт запрос в API ITSM, которое создаёт заявку. Через месяц аудитор открывает хранилище заявок и видит, что во всех заявках, оформленных агентом, указано одно имя — идентификатор собственного приложения шлюза. Настоящие заявители отсутствуют в записях.

Та же ошибка возникает во всех системах, расположенных ниже по цепочке после шлюза. Система управления персоналом приписывает каждую заявку на отпуск к учетной записи сервиса шлюза, реестр активов относит к ней каждую передачу устройства, а процесс закупок указывает его в качестве утверждающего лица для каждой заявки. Идентичность заканчивается у шлюза, и ничто дальше не может её восстановить.

Шлюзы MCP стали стандартным мостом между агентами LLM и корпоративными системами хранения данных, однако в большинстве реальных реализаций остается неясным один вопрос: чья идентичность авторизует каждый запрос, поступающий дальше по цепочке? Доминируют два антипаттерна.

Прекращение работы учетной записи. После проверки того, кто звонит, шлюз взаимодействует со всеми бэкенд-системами как с единым общим сервисным принципалом. Любые действия, совершаемые им в IT-системе, системе управления персоналом или системе закупок, приписываются его сервисной учетной записи, а не человеку, сделавшему запрос. Логи аудита становятся бесполезными, а принцип минимальных привилегий нарушается. Ненадлежаще настроенный запрос может использовать полные возможности шлюза против любой бэкенд-системы, к которой он может получить доступ.

Доверие клиенту. Шлюз принимает любой полученный токен-носитель и передает его без изменений дальнейшим API, не проверяя эмитента, целевую аудиторию или объем полномочий. Кража токена на любом этапе передачи тихо предоставляет право на произвольные запросы к дальнейшим системам.

Ни один из вариантов не является приемлемым для корпоративных агентных ИИ-систем, особенно с учетом функции управления в рамках Рамочной программы NIST по управлению рисками ИИ, которая требует сохранения индивидуальной ответственности при автоматизированных действиях на регулируемых системах.

Предложение

Дизайн включает два границы OAuth. На стороне клиента входящая граница извлекает и проверяет токен деlegированного доступа, а именно токен типа bearer OAuth 2.0 (RFC 6749), выданный вызывающему пользователю для аудитории шлюза. Выходящая граница осуществляет обмен токенами от имени шлюза (в духе стандарта RFC 8693) или, если цепочка доверия это позволяет, прямую передачу токена, при этом генерируется токен для нижестоящей аудитории, который по-прежнему содержит идентификатор оригинального пользователя.

Разрешение на использование инструмента определяется на границе на основе обработанных данных токена, а не внутри реализации самого инструмента. Это позволяет Gateway отвечать за передачу идентификатора, вместо того чтобы каждый инструмент или бэкенд должен был это запоминать.

Паттерн: Delegated Boundary OAuth

Коротко говоря: Gateway использует две границы OAuth, а не одну, причем идентификатор пользователя служит договором, передаваемым между ними.

Граница входящих запросов. Извлечение токена-носителя, проверка эмитента и аудитории, а также анализ утверждений происходят в одном слое шлюза — обычно в промежуточном компоненте обработки запросов или в инжекторе зависимостей в стиле FastMCP, который выполняется до каждой функции инструмента. Именно здесь отвечается на вопросы «Кто запрашивает доступ, и разрешено ли ему использовать этот инструмент?», и ни одна реализация инструмента не может этот этап пропустить. Сопоставление диапазонов полномочий с инструментами осуществляется декларативно: каждый обнаруживаемый инструмент указывает необходимые ему диапазоны OAuth, и граница отклоняет его выполнение, если токен не содержит этих диапазонов. Такое отклонение представляет собой структурированную ошибку авторизации, а не ошибку кода 5xx, возникающую в результате проверки прав на более глубоких уровнях стека, что приводит к избыточной нагрузке на процессор.

Граница отправки запросов. Аутентификация запросов, направляемых ниже по цепочке, осуществляется с помощью токена, указывающего имя исходного пользователя, а не с использованием собственного идентификатора сервиса шлюза. Для этого используются два механизма. Механизм, работающий от имени шлюза (Microsoft Entra OBO, пул идентификаторов AWS Cognito или пользовательский механизм STS), выдает новый токен, адресованный API ниже по цепочке, при этом субъектом токена по-прежнему остается человек, совершающий вызов. Прямая передача, предназначенная для сервисов ниже по цепочке, находящихся в рамках явного соглашения о доверии с шлюзом, передаёт входящий токен без изменений. Именно шлюз обеспечивает возможность подтверждения прохождения запросов, а не просто предположение этого.

Между запросами. Токены считаются состоянием, действительным только в течение одного запроса. Ничто не кэшируется между запросами, ничего не сохраняется в сессии, и никакое изменяемое состояние не обменивается между процессами. Каждый вызов инструмента MCP снова считывает токен из актуальных заголовков и забывает его после завершения запроса.

Реализация

Приходящие запросы: извлечение делегированного токена

Граница обработки приходящих запросов представляет собой простой промежуточный компонент, который выполняется перед каждой функцией инструмента. В Python-сервере MCP, построенном на фреймворке в стиле FastMCP, для получения токена используется механизм вставки значений CurrentHeaders, а также небольшой утилитный инструмент:

from typing import Optional

def extract_bearer_token(headers: dict) -> str:
    auth = headers.get("authorization") or headers.get("Authorization")
    if not auth or not auth.startswith("Bearer "):
        raise AuthError("missing or malformed bearer token")
    return auth[len("Bearer "):]

Далее происходит проверка этого токена по эмитенту и аудитории, ожидаемым шлюзом, с использованием опубликованных ключей подписи поставщика идентичности (JWKS):

from jose import jwt

def validate_inbound_token(token: str, jwks: dict, audience: str, issuer: str) -> dict:
    claims = jwt.decode(
        token, jwks,
        audience=audience,
        issuer=issuer,
        options={"require_exp": True, "require_iat": True},
    )
    return claims  # sub, scp/scope, exp, etc.

Проверка должна происходить внутри процесса. Передача задачи проверки JWT на внешний прокси может сработать в ситуациях с ограниченным уровнем доверия, но это лишает шлюз возможности обеспечить соответствие между диапазоном прав и используемым инструментом до того, как инструмент начнет выполнять свои действия.

Выходные запросы: от имени шлюза

При отправке запросов шлюз заменяет входной пользовательский токен на токен, предназначенный для конечных пользователей. Именно эта замена отличает данный подход от простого пересылания запросов, поскольку API конечного пользователя никогда не видит входной токен напрямую. Шлюз отправляет запрос на конечную точку обслуживания токенов поставщика идентификации с использованием параметра urn:ietf:params:oauth:grant-type:jwt-bearer (Entra) или запроса assume-role-with-web-identity (AWS), передавая входной токен в качестве элемента подтверждения личности. Поставщик возвращает новый токен для API конечного пользователя, в котором значение атрибута sub по-прежнему соответствует исходному пользователю.

def exchange_obo(inbound_token: str, downstream_audience: str) -> str:
    response = requests.post(
        f"{IDP_TOKEN_ENDPOINT}",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "client_id": GATEWAY_CLIENT_ID,
            "client_secret": GATEWAY_CLIENT_SECRET,
            "assertion": inbound_token,
            "scope": f"{downstream_audience}/.default",
            "requested_token_use": "on_behalf_of",
        },
        timeout=5,
    )
    response.raise_for_status()
    return response.json()["access_token"]

Затем обмененный токен присоединяется к исходному HTTP-запросу:

def call_downstream(downstream_token: str, path: str) -> dict:
    return requests.get(
        f"{DOWNSTREAM_BASE_URL}{path}",
        headers={"Authorization": f"Bearer {downstream_token}"},
        timeout=10,
    ).json()

Сопоставление диапазонов и распространение идентичности

Функции инструмента указывают необходимые диапазоны при регистрации, и механизм контроля обеспечивает их соблюдение перед запуском основной части инструмента:

@tool(required_scopes=["ticketing.write"])
def open_ticket(headers: CurrentHeaders, summary: str, body: str) -> dict:
    token = extract_bearer_token(headers)
    claims = validate_inbound_token(token, jwks, audience=GATEWAY_AUD, issuer=ISSUER)
    enforce_scopes(claims, required=["ticketing.write"])  # central, not tool-local
    downstream_token = exchange_obo(token, downstream_audience=TICKETING_AUD)
    return call_downstream(downstream_token, path="/tickets")

Поскольку маппинг областей применения является декларативным, его можно проверить. Любой, кто аудитирует шлюз, может получить все инструменты и необходимые для них области применения непосредственно из реестра, без необходимости читать код инструментов.

Входные границы одинаковы у всех поставщиков облачных услуг. Различаются только выходные схемы связи, причем обе приведённые ниже реализации имеют одну и ту же общую структуру.

В AWS: API Gateway, Lambda и Cognito

В AWS входной барьер может представлять собой пользовательского авторизатора на основе Lambda в Amazon API Gateway. Он проверяет токен по пулу пользователей Amazon Cognito и возвращает документ с правилами аутентификации вместе с картой контекста, содержащей разобранные утверждения, чтобы сервер MCP на базе Lambda мог извлечь эти утверждения из event.requestContext.authorizer без необходимости повторной проверки. Что касается выходного направления, то операция sts:AssumeRoleWithWebIdentity использует входной токен (или токен, выданный пулом идентичностей Cognito) и предоставляет временные учетные данные AWS, связанные с федеративной идентичностью пользователя. Любой конечный пункт API Gateway, расположенный за ним, или внешняя API, доверяющая JWT от Cognito, тогда видит информацию о пользователе в утверждении sub.

Минимальный обмен данными между Cognito и STS в Lambda API Gateway выглядит следующим образом:

import boto3

def aws_obo(inbound_token: str, role_arn: str) -> dict:
    sts = boto3.client("sts")
    creds = sts.assume_role_with_web_identity(
        RoleArn=role_arn,
        RoleSessionName="mcp-gateway-obo",
        WebIdentityToken=inbound_token,
        DurationSeconds=900,
    )
    return creds["Credentials"]

Политика доверия предполагаемой роли указывает пул пользователей Cognito в качестве федеративного субъекта, а также проверяет атрибут aud входящего токена, что означает, что роль может принять только токен, выданный для этого шлюза.

В Azure: процесс Entra on-behalf-of

В Azure входящий брандмауэр проверяет входящий токен по регистрации приложения Entra шлюза, используя URI идентификатора приложения шлюза в качестве аудитории. Для выходного запроса выполняется обмен данными по протоколу Entra on-behalf-of, направленный на регистрацию приложения самого нижестоящего API:

import msal

def entra_obo(inbound_token: str, downstream_scope: str) -> str:
    app = msal.ConfidentialClientApplication(
        client_id=GATEWAY_APP_ID,
        client_credential=GATEWAY_CLIENT_SECRET,
        authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    )
    result = app.acquire_token_on_behalf_of(
        user_assertion=inbound_token,
        scopes=[downstream_scope],
    )
    if "access_token" not in result:
        raise AuthError(result.get("error_description", "obo exchange failed"))
    return result["access_token"]

API нижнего уровня должен принимать токены, предназначенные исключительно для регистрации приложений Entra. Логика авторизации считывает значение oid (идентификатор объекта) или upn (имя основного пользователя), чтобы отнести запрос к исходному пользователю. Структура шаблона остается неизменной, меняется лишь способ связи с поставщиком идентичности.

Связь с логированием и оценкой

Применение механизма распространения идентичности наиболее эффективно при его записи. Если шлюз фиксирует информацию о входящем пользователе при каждом вызове инструмента, аудит позволяет привязать действие агента к конкретному человеку. То же поле может включаться в записи оценки, тем самым связывая результаты тестов с людьми, поведению которых реагирует система. Считайте идентичность, получаемую с помощью этого шаблона, первым столбцом во всех записях аудита и оценки, генерируемых системой.

Когда этот шаблон подходит, а когда — нет

Он подходит в следующих случаях:

  • Шлюз обеспечивает доступ к системам, в которых индивидуальная идентичность имеет значение для авторизации, аудита или соблюдения норм, таким как управление IT-услугами, HR, финансы и регулируемые рабочие процессы.
  • Провайдер корпоративных идентификаторов поддерживает стандарты обмена данными: Entra OBO, Cognito с веб-идентификацией STS или обмен токенами Auth0 и Okta.
  • API, находящиеся ниже по цепочке, располагаются внутри той же зоны доверия, что и шлюз, и могут быть настроены на прием токенов пользователей.
  • Реализация соответствует стандартам NIST AI RMF, SOX, HIPAA или аналогичным нормативам, требующим привязки автоматизированных действий к конкретному пользователю.

Этот подход не подходит в следующих случаях:

  • Каждый запрос, направляемый ниже по цепочке, явно не связан с конкретным пользователем, например, это только чтение референтных данных без каких-либо побочных эффектов, зависящих от пользователя.
  • Доверенный прокси на нижнем уровне уже обеспечивает полную реализацию делегированных правил работы шлюза, из-за чего собственная работа шлюза становится избыточной. В таком случае шлюз создает лишь входные границы, а также вариант с делегированным доверием для выходных операций, осуществляя пересылку данных непосредственно в соответствии с явным договором о доверии и признавая, что прокси является настоящей защитной границей.
  • API на нижнем уровне отклоняет токены, предназначенные для пользователей, и их переработка не рассматривается. Следует отложить применение данного подхода до изменения ситуации. Обращение к сервисному принципалу «на время» должно быть осознанным решением с описанием связанных рисков, а не стандартной практикой.
  • Эксплуатационные аспекты:

    • Задержка. Каждая операция обмена через OBO требует дополнительного запроса к токен-эндпоинту для каждого внутреннего вызова. Сохраняйте обмененные токены столько, сколько действителен входящий токен, идентифицируемый по ключу (sub, downstream_audience); они никогда не должны использоваться между разными пользователями.
    • Ограничения поставщика идентификации. Квоты токен-эндпоинта — это серьезный фактор. Обеспечьте запас пропускной способности и отслеживайте уровень сбоев при обмене токенами.
    • Обновление токенов. Длительные сессии агентов могут длиться дольше срока действия одного токена. Планируйте ротацию токенов обновления или согласитесь с тем, что цикл работы агента завершается при истечении срока его токена, что обычно является более безопасным подходом.
    • Альтернативный вариант с учетной записью сервиса. Для задач, не связанных с пользователями, таких как проверки работоспособности или запланированная фоновая синхронизация, используйте специальный путь обмена через учетную запись сервиса. Сделайте его явным, ограничьте его диапазон использования и ведите записи о его действиях.

    Где этот подход не справляется

    Он предполагает, что поставщик идентификации поддерживает стандарт обмена данными от имени другой стороны, однако многие старые решения этого не делают. В таких средах внешняя часть системы вынуждена передавать токены напрямую, при этом с каждым последующим сервисом заключается письменное соглашение о доверии. Это приемлемо для бэкендов, находящихся в пределах зоны доверия шлюза, но недостаточно, когда последующий сервис управляется другой командой или сторонним поставщиком. Правильным путем миграции является обновление поставщика идентификации, а не постоянное использование метода пересылки данных.

    Этот подход также предполагает, что внутренние сервисы принимают токены, связанные с пользователями. Многие корпоративные системы были созданы до появления механизмов делегированной авторизации и аутентифицируются только по принципам, связанным с самими сервисами. В таком случае необходимо дождаться обновления сервиса или внедрения прокси-сервера, учитывающего идентичность пользователей. Частичная реализация этого подхода, при которой некоторые запросы аутентифицируются по пользователям, а другие — по сервисам, является нормальным состоянием во время миграции, однако её следует рассматривать как технический долг с установленной датой устранения, а не как стандартную практику.

    Наконец, это не решает проблему привязки ответственности агента-исполнителя. Сохранение идентичности человека при переходе через шлюз является правильным основным механизмом контроля, однако агент на основе LLM всё равно может быть склонён к действиям, которые пользователь никогда буквально не запрашивал. Такая схема гарантирует привязку к источнику, но не цели. Компенсирующие меры контроля несут эту нагрузку: подтверждение человеком для действий с высоким влиянием, минимальные объёмы полномочий на входном уровне и логирование доказательств для каждого вызова.

    Чек-лист внедрения

    • Документируйте входной контракт (исполнитель, аудитория, поддерживаемые объёмы полномочий) в виде версионированного интерфейса и управляйте им как публичным API.
    • Реализуйте соответствие между объёмами полномочий и инструментами в виде декларативного поля при регистрации инструмента, а не в виде спорадических проверок, расположенных внутри кода инструмента.
  • Настройте механизм обмена от имени пользователя с поставщиком идентификации предприятия для каждого внутреннего бэкенда. Установите механизм OBO в качестве стандартного и сохраните прямую передачу данных для бэкендов, находящихся в пределах явных границ доверия шлюза.
  • Храните обмененные токены в течение срока действия входящего токена, используя в качестве ключа идентификатор пользователя и целевую аудиторию, и никогда не делитесь ими между пользователями.
  • При каждом вызове инструмента записывайте как входящий, так и исходящий значения параметра sub, чтобы можно было отслеживать весь процесс передачи данных от начала до конца.
  • Разрешите использование одного узко ограниченного пути сервисного аккаунта для работы без участия пользователя и требуйте одобрения при каждом его использовании.
  • Зарегистрируйте внутренние API, которые отклоняют токены, предназначенные для конкретной аудитории, как архитектурный недостаток, и работайте над устранением этого ограничения.
  • По мере того как американские компании и федеральные программы внедряют Рамку управления рисками ИИ NIST, её функция управления требует наличия индивидуальной ответственности при автоматизированных действиях на регулируемых системах. Ограничение доступа к идентификаторам только на уровне шлюза прямо противоречит этому требованию и может привести к выявлению нарушений в рамках стандартов SOX, HIPAA и FedRAMP, где каждое автоматизированное изменение должно быть привязано к конкретному человеку.

    Дополнительная литература

    Основами являются документы RFC 6749 (рамка авторизации OAuth 2.0) и RFC 8693 (обмен токенами OAuth 2.0) от IETF. В документации поставщиков описывается механизм OAuth 2.0 On-Behalf-Of в платформе идентификации Microsoft, справочник API AWS STS AssumeRoleWithWebIdentity и пулы идентификаторов Amazon Cognito. NIST публикует Рамку управления рисками ИИ (AI RMF 1.0), а спецификация MCP содержит раздел, посвящённый авторизации, который стоит прочитать вместе со всем вышеупомянутым.