Читать 6 мин

Действительно ли соблюдаются границы учетных данных поддоменов API?

Границы учетных данных поддоменов API не дают токенам попадать на соседние, региональные, vanity- и клиентские хосты через перенаправления или нестрогий код клиента.

Действительно ли соблюдаются границы учетных данных поддоменов API?

Имя хоста не говорит о принадлежности. Это часть идентификатора назначения, который определяет, куда отправится учетная запись API. Если клиент хранит один bearer-токен и считает api.example.com, files.example.com, api.eu.example.com и customer.example.com практически одинаковыми только потому, что у них общий суффикс, опасное решение уже принято.

Такая ошибка часто скрывается за удобной архитектурой. Компания может владеть всеми именами в родительском домене. DNS может направлять несколько имен на один балансировщик. Один сертификат может покрывать их все. Но это не означает, что учетные данные, выданные для одного API, должны попадать на другой хост. Для учетных данных нужно явное правило назначения, а для этого правила нужны тесты, которые упадут, если кто-то позже расширит его без проверки.

Хосты не наследуют доверие от родительского домена

api.example.com и admin.example.com могут иметь общий регистрируемый домен, но это разные источники. Источник включает схему, хост и порт. https://api.example.com и https://api.example.com:8443 тоже являются разными источниками. Это важно, потому что клиент выбирает сетевое назначение по этой полномочности, а не по маркетинговой связи между сервисами.

RFC 9110 определяет область защиты HTTP-аутентификации через источник и, если он указан, область realm. В документе также говорится, что одной области realm недостаточно: так можно раскрыть учетные данные другим ресурсам того же источника. Когда нужна изоляция разных сторон, рекомендуется использовать отдельные хосты или порты. Это полезное предупреждение, но оно не разрешает считать все имена под одним суффиксом единой областью защиты.

Инженеры часто смешивают три разные идеи:

  • Родительский домен задает пространство имен DNS.
  • Сайт объединяет связанные веб-источники для некоторых правил безопасности браузера.
  • Источник определяет одну схему, хост и порт назначения HTTP.

Только первые две идеи создают впечатление, что поддомены связаны. Диспетчер API-учетных данных должен опираться на третью.

Это важно даже тогда, когда издатель токена помещает в него широкое утверждение aud. Широкая аудитория сообщает принимающему сервису, что он может принять. Она не указывает клиенту отправлять токен на каждую конечную точку, которая потенциально способна его принять. Отправитель по-прежнему обязан ограничивать распространение.

Я видел, как это происходит по одному и тому же сценарию. Команда начинает с одной конечной точки, добавляет api.staging.example.com и оставляет помощник с названием getApiKey(). Через полгода этот помощник используется пятью сервисами. У него нет аргумента хоста, списка разрешений и понимания, почему вызов нужно отклонить. Ключ стал небезопасным не из-за экзотической атаки. Код перестал хранить сведения о том, что ключ предназначен для одного конкретного адресата.

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

Четыре типа имен хостов ломаются по-разному

Для соседних, vanity-, региональных и клиентских доменов нужны отдельные тестовые случаи, поскольку причины отказа у них различаются. Одно расплывчатое утверждение, что токен остается «в наших доменах», пропустит как минимум один из этих вариантов.

Соседний хост находится рядом с нужным: metrics.example.com вместо api.example.com. На нем может оказаться общий входной шлюз, старый сервис или внутренний инструмент, который никогда не должен видеть рабочие учетные данные. Особенно опасен такой хост, если общий прокси передает заголовки без изменений на вышестоящий сервис, выбранный настройкой маршрута.

Vanity-хост это удобный псевдоним вроде api.brand.example или developer.example.com. Такие имена появляются при переименовании продукта, покупке компании или миграции. Псевдоним может завершаться на другом периферийном узле, использовать другую телеметрию или перенаправлять на канонический хост. Не разрешайте ему доступ к учетным данным только потому, что однажды увидели перенаправление в браузере.

Региональный хост меняет расположение и часто владельца сервиса: api.us.example.com, api.eu.example.com или api.ap-southeast.example.com. До того как приложение отклонит токен, запрос может уже передать деловые данные. Ответ 401 не доказывает, что отправка учетных данных и тела запроса в этот регион была приемлемой.

Клиентский хост делает ошибки в многоклиентской системе еще серьезнее: acme.vendor.example и northwind.vendor.example могут проходить через одну инфраструктуру, но каждое имя обозначает границу клиента. Токен, достаточно широкий для работы на обоих хостах, может быть намеренно нужен центральной плоскости управления. Но он не должен по умолчанию использоваться для запросов, направленных клиентам.

Внесите каждую категорию в инвентаризацию. Не сводите их к полю allowed_domains и не считайте, что подстановочный знак упрощает политику. Он лишь прячет сложное решение.

Привязывайте выбор учетных данных ко всей полномочности

Безопасный клиент сопоставляет полномочность запроса с одной записью учетных данных, а если совпадения нет, отклоняет запрос. Полная полномочность включает схему, нормализованное имя хоста и фактический порт. Для обычного HTTPS API на порту 443 видимая конфигурация может оставаться простой, но реализация все равно должна отклонять неожиданный порт, а не молча использовать тот же токен.

Рабочая инвентаризация может выглядеть так:

credentials:
  billing-production:
    allowed:
      - https://api.billing.example.com:443
    header: Authorization
    scheme: Bearer

  telemetry-eu:
    allowed:
      - https://ingest.eu.example.net:443
    header: X-Write-Key

Дело не в YAML. Важно, что имя токена не существует само по себе. У billing-production есть одно явно указанное назначение, а telemetry-eu не утечет на американскую конечную точку только потому, что вызывающий код изменил строку хоста.

Избегайте такого шаблона:

const headers = {
  Authorization: `Bearer ${process.env.PRODUCTION_API_TOKEN}`
};

await fetch(userSuppliedUrl, { headers });

Здесь неограниченное назначение связано с учетными данными высокого уровня. Разработчики иногда возражают, что URL берется из надежного файла конфигурации. Но этот файл тоже является границей входных данных. Его могут изменить инструменты развертывания, переменные окружения, запрос на слияние, функциональный флаг или взломанная задача сборки.

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

  1. Разбирайте URL настоящим анализатором URL, а не проверкой суффикса строки.
  2. Требуйте https:, кроме документированного исключения для локальной разработки.
  3. Приводите имя хоста к нижнему регистру и каноническому виду через анализатор.
  4. Сравнивайте получившиеся схему, хост и порт с точными разрешенными адресами.
  5. Добавляйте заголовок аутентификации только после успешного совпадения.

Не пишите host.endsWith("example.com"). Так пройдет notexample.com. Не считайте задачу решенной, заменив это на endsWith(".example.com"). Такое выражение по-прежнему дает токен каждому текущему и будущему поддомену, включая имя, делегированное клиенту, поставщику или забытой среде разработки.

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

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

Обработка перенаправлений создает вторую границу для учетных данных. Первый хост может быть разрешен, а затем вернуть заголовок Location, указывающий на vanity-имя, региональную конечную точку, объектное хранилище или хост, который атакующий контролирует через открытое перенаправление. Если клиент следует перенаправлению автоматически, перед отправкой любых учетных данных или тела запроса он должен повторно проверить назначение.

libcurl особенно наглядно показывает разницу. По умолчанию он не отправляет внутренне созданные данные аутентификации или явно заданные заголовки cookie на другой хост во время перенаправления. Опция CURLOPT_UNRESTRICTED_AUTH меняет это поведение и может отправлять учетные данные на хосты, выбранные ответами перенаправления. Проект curl отдельно предупреждает, что с пользовательскими заголовками нужно быть осторожнее: библиотека не может понять, какие произвольные заголовки содержат секреты.

Именно эта деталь часто вводит опытные команды в заблуждение. Они проверяют Basic-аутентификацию стандартным клиентом и видят, что межхостовое перенаправление обрабатывается безопасно. Затем рабочая интеграция использует X-Api-Key, Authorization: Bearer или X-Signature, добавленные как обычный заголовок. Библиотека может сохранить такой заголовок, если приложение само его не удалит. Успешный тест одного механизма аутентификации ничего не говорит о другом.

Обрабатывайте перенаправления по типу запроса:

  • Для операций записи отклоняйте перенаправления, если они не предусмотрены контрактом API.
  • Для операций чтения проверяйте каждое новое назначение и заново собирайте заголовки по записи учетных данных этого адресата.
  • Для подписанных запросов создавайте подпись заново после авторизации конечного назначения. Никогда не передавайте подпись, созданную для первого хоста.
  • Для загрузок не передавайте исходный заголовок авторизации на URL хранилища с предварительной подписью. Ограниченное право уже передается через подпись в запросе или поля формы.

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

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

Создайте стенд отрицательных тестов до того, как доверите списку разрешений

Проверяйте каждый HTTP-вызов
Журнал Activity записывает отдельные вызовы и помогает разобраться в подозрительном запросе.

Главный тест отрицательный: учетные данные должны появиться на нужном хосте и не должны появиться на каждом правдоподобном неправильном хосте. Это можно проверить на двух локальных HTTPS-серверах, но приемник должен записывать только наличие и название чувствительных заголовков. Не помещайте настоящие значения в тестовые журналы.

Для проверки на уровне shell сопоставьте безопасные имена с локальными серверами через опцию --resolve в curl и используйте одноразовый токен. Запустите один приемник на порту 8443 для разрешенного хоста, а другой на 9443 для соседнего. Каждый приемник должен выводить запись такого вида:

host=api.test.example
path=/v1/ping
authorization=present
x-api-key=absent

Приемник соседнего хоста должен выводить обратный результат:

host=metrics.test.example
path=/v1/ping
authorization=absent
x-api-key=absent

Затем проверяйте настоящую обертку HTTP-клиента, а не ее упрощенную копию. Проба curl может показать базовую связку:

curl --silent --show-error \
  --resolve api.test.example:8443:127.0.0.1 \
  --header 'Authorization: Bearer test-token-do-not-use' \
  https://api.test.example:8443/v1/ping

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

Используйте матрицу, которая заставляет принять решения, обычно оставляемые без внимания:

Запрошенный адресОжидаемое поведение учетных данных
https://api.test.exampleОтправить предназначенные для теста учетные данные
https://metrics.test.exampleОтклонить до отправки
https://api.eu.test.exampleОтклонить, если адрес не настроен отдельно
https://tenant-a.test.exampleОтклонить, если привязка к клиенту не задана явно
Разрешенный хост перенаправляет на соседнийСледовать только после повторной авторизации, обычно без исходных учетных данных

Не используйте для этого внешний сервис сбора запросов. Иначе вы приучите команду отправлять тестовые секреты третьей стороне, пока проверяете, отправляет ли она секреты третьим сторонам. Локальный приемник легко создать, а данные останутся под вашим контролем.

Добавьте матрицу в непрерывную интеграцию. Модульный тест isAllowedHost() полезен, но интеграционный тест ловит типичный регресс: кто-то добавляет заголовок по умолчанию на нижнем уровне HTTP после того, как проверка хоста уже прошла.

Правила браузера не распространяются на клиентов и агентов

Браузерная терминология порождает неверные предположения в серверном и агентском коде. «Один сайт» может включать поддомены, а «один источник» нет. MDN приводит https://example.org и https://login.example.org как пример двух источников, относящихся к одному сайту. Это различие важно, поскольку взломанный поддомен может атаковать соседний через правила одного сайта.

По умолчанию Fetch использует credentials: "same-origin", поэтому браузерный fetch автоматически не добавляет учетные данные в межисточниковые запросы. Разработчик может увидеть это значение, проверить вызов из интерфейса и решить, что bearer-токен не перейдет с одного поддомена на другой. Для серверного кода это неверно. Серверная обертка fetch может добавить любой заголовок, который передал автор. CLI делает то же самое. Автономный агент может вызвать универсальный HTTP-инструмент с URL и заголовками, если инструмент не удерживает учетные данные вне его доступа.

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

Cookies создают еще один источник путаницы. Атрибуты домена cookie могут разрешить отправку на поддомены, а host-only cookie этого не делают. У bearer-заголовков нет сопоставимой встроенной области домена. Если клиент добавляет Authorization, он явно принимает решение для этого запроса. Не переносите модель cookies на API-ключи.

Для клиентских доменов нужна граница издателя, а не соглашение об именах

Не передавайте SSH-ключи агентам
Sallyport запускает SSH через встроенный помощник sp-ssh, а SSH-ключи остаются внутри зашифрованного хранилища.

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

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

Не решайте проблему шаблоном *.customers.example.com, объявляя каждый совпавший адрес клиентским. Спросите, кто может создавать такие имена, кто управляет делегированием DNS, какие хосты ведут в предварительные среды и сохраняются ли имена после отключения клиента. Подстановочный знак превращает все эти вопросы в решения безопасности, обычно без записи о проверке.

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

Та же проблема возникает с частным DNS внутри компании. payments.prod.internal и payments.dev.internal могут быть недоступны из интернета, но это разные назначения с разными операционными средствами контроля. Внутренний DNS не заменяет ограничение области действия учетных данных.

Проверяйте хост до обнаружения сервиса и после нормализации

Пропускайте вызовы MCP через шлюз
Подключайте Claude Code или другого агента с поддержкой MCP через sp mcp, не передавая ему сохраненные учетные данные.

Обнаружение сервисов, собственная маршрутизация и настройки прокси могут незаметно обойти разумный список разрешений. Если код проверяет исходный URL, а затем преобразователь заменяет адрес на внутреннюю цель, приложение может отправить учетные данные другой полномочности. И наоборот, проверка изменяемого заголовка Host может разрешить запрос, хотя сетевое соединение установлено в другое место.

Используйте полномочность разобранного URL как вход для политики. Устанавливайте TLS-соединение с этим адресом и обычно проверяйте сертификат. Не отключайте проверку сертификата ради внутреннего маршрута или тестового стенда. В рекомендациях по безопасности самого curl говорится: клиент, который не может аутентифицировать узел, не знает, что подключился к нужному серверу.

Затем определите, что означает прокси в вашей модели доверия. Прямой прокси это транспортный выбор, а не новое назначение учетных данных, если клиент устанавливает TLS с разрешенным источником через него. Обратный прокси, завершающий TLS, входит в границу сервиса и требует такой же проверки, как сам API. HTTP-прокси, получающий незашифрованные заголовки авторизации, имеет доступ к учетным данным. Не называйте это «просто инфраструктурой».

Нормализуйте интернационализированные имена хостов через реализацию URL, соответствующую стандартам, и сравнивайте канонический результат. Отклоняйте URL с данными пользователя вроде https://[email protected]/: учетные данные в URL попадают в журналы, историю и отладочный вывод. Отклоняйте фрагменты в HTTP-запросах и явно решайте, могут ли строки запроса содержать предварительно подписанные учетные данные. Универсальная функция очистки не спасет дизайн, который принимает любой URL и надеется потом найти плохие варианты.

Правильный порядок прост: разобрать, нормализовать, авторизовать назначение, выбрать учетные данные, создать заголовки, подключиться. Если последующая операция меняет полномочность, начните проверку назначения заново.

Аудитируйте решение, а не секрет

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

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

Хорошая запись аудита отвечает на конкретный вопрос:

request_id=01J...
authority=https://api.billing.example.com:443
credential=billing-production
destination_check=allowed
redirect_count=0
result=201

Для отклоненного запроса к соседнему хосту запись должна показывать credential=none и destination_check=denied. Это подтверждает, что клиент отказал до выбора секрета. Если журнал сначала содержит имя учетных данных, а затем сообщает о 403 от неправильного хоста, система уже отправила больше, чем должна была.

Sallyport хранит секрет в зашифрованном хранилище и проверяет действие до того, как агент получает любой материал учетных данных. Журналы Activity и Sessions помогают изучить путь действия и отозвать сеанс работающего агента, если запрос с привязкой к хосту пошел не туда.

Сначала составьте инвентаризацию. Для каждой учетной записи укажите одно предназначенное для нее назначение, а затем назовите один соседний, один vanity- или миграционный, один региональный и один клиентский адрес, который не должен ее получать. Если ваш клиент сегодня не может выполнить такие отрицательные проверки, у него нет границы учетных данных. Есть только соглашение, на которое надеются.

Вопросы и ответы

Работают ли API-ключи автоматически на всех поддоменах?

Нет. api.example.com и billing.example.com являются разными источниками, даже если обеими зонами управляет одна команда. Считайте привязку учетных данных точной, если поставщик явно не документирует более широкую аудиторию и вы не проверили ее границы.

Безопасно ли отправлять один bearer-токен на все поддомены?

Они должны получать токен только в том случае, если клиент специально настроен на его отправку туда. Общий родительский домен подтверждает владение пространством имен, но не разрешает повторно использовать токен одного сервиса.

Может ли HTTP-перенаправление раскрыть API-учетные данные другому хосту?

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

Нужны ли для каждого клиентского поддомена отдельные учетные данные?

Нет. Для tenant-a.api.example.com обычно нужны собственные учетные данные, привязанные к клиенту, либо токен с признаком клиента, который проверяет сервис. Общий административный ключ для запросов к хостам клиентов превращает ошибку маршрутизации в инцидент между клиентами.

Нужны ли региональным API отдельные учетные данные?

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

Безопасны ли vanity-домены API для рабочих учетных данных?

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

Как проверить, отправляет ли клиент токен на соседние домены?

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

Останавливает ли CORS отправку учетных данных на поддомен?

CORS определяет, что может прочитать JavaScript в браузере. Агент на сервере, CLI или собственный HTTP-клиент способен отправить любой заголовок на любой хост, если код не остановит такой запрос.

Стоит ли использовать списки разрешенных хостов с подстановочными знаками?

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

Как исправить ошибку в границе действия учетных данных в первую очередь?

Сначала составьте список всех привязок «учетные данные - хост» и удалите широкие значения по умолчанию вроде *.example.com. Затем добавьте отрицательный тест для каждого ключа: один одобренный адрес должен его получить, а соседний, vanity-, региональный и клиентский адреса не должны.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov