Изоляция учётных данных в пуле HTTP-соединений для AI-агентов
Изоляция учётных данных в пуле HTTP-соединений не даёт cookie, проверкам подлинности и состоянию учётной записи перейти через повторно используемый сокет в действие другого агента.

Повторно используемое HTTP-соединение само по себе не означает утечку учётных данных. Считая его безобидным, команды упускают настоящую проблему: объект клиента накапливает cookie, ответы на проверки подлинности, заголовки перенаправлений, идентификатор прокси или TLS-идентичность, а затем следующий запрос получает это состояние с другими учётными данными.
Изоляция учётных данных в пуле HTTP-соединений означает, что вы точно определяете, какое состояние может переходить между запросами, а остальным делиться запрещаете. Одного хоста назначения обычно слишком мало как границы для исполнителя действий или агента, который может действовать от имени нескольких учётных записей. Быстрый клиент, который иногда отправляет cookie не той учётной записи, хуже медленного, потому что ошибка выглядит как обычный успешный запрос.
Сокет отвечает за транспорт, а не за владение учётной записью
TCP-соединение передаёт байты для origin. Оно не даёт приложению надёжной границы учётной записи. HTTP/1.1 использует соединение последовательно, а HTTP/2 может одновременно вести множество потоков, но ни один из протоколов не утверждает, что каждый запрос в соединении относится к одному пользователю или сервисной учётной записи.
Это важно, потому что клиентские библиотеки часто скрывают несвязанные вещи за удобным интерфейсом с названием session, agent, client или transport. Разработчики создают один такой объект на хост и добавляют bearer-токен перед каждым вызовом. Заголовок bearer может каждый раз быть правильным, пока хранилище cookie, кэш Digest-проверок или контекст прокси незаметно остаются от последней учётной записи, использовавшей объект.
RFC 9110 рассматривает аутентификацию как поведение запроса и пространства защиты. Клиент отвечает на проверку для конкретных origin, схемы и realm. Он не утверждает, что открытое соединение принадлежит человеку или сервисной учётной записи. Если ваш клиент исходит из такого владения, это предположение заложено в вашем коде или библиотеке, а не в HTTP.
Повторное использование соединений остаётся полезным. Оно избавляет от повторных рукопожатий, снижает расход портов и помогает удалённому сервису под нагрузкой. Правило должно быть уже: повторно используйте соединение только между запросами, чьё состояние уровня соединения и состояние, управляемое клиентом, намеренно совместимы.
Состояния, которые стоит разделять, делятся на две группы:
- Состояние запроса включает Authorization, Cookie, заголовки учётной записи, тела запросов и значения идемпотентности. Вызывающий должен собирать их заново для каждого действия.
- Состояние соединения и клиента включает запись пула, сеанс прокси, выбор клиентского сертификата, поведение при перенаправлении, cookie, кэши проверок подлинности и настройки протокола. Клиент должен намеренно ограничить область действия или отключить каждый из этих элементов.
Bearer-учётные данные, которые используются только в заголовке Authorization, могут делить TCP-соединение с другими bearer-учётными данными, если библиотека отправляет заголовки для каждого запроса и не хранит состояние учётной записи. Это ограниченное утверждение, а не общее разрешение. Как только сервис также устанавливает cookie сеанса, перенаправляет на другой хост или запрашивает клиентский сертификат, схему пула нужно пересмотреть.
Cookie остаются состоянием учётной записи, даже если их имена выглядят безобидно
Хранилище cookie чаще всего становится источником случайного смешивания учётных записей. Команды видят API с bearer-токеном и считают cookie несущественными, пока балансировщик нагрузки, интерактивная точка входа или устаревший сервис не добавит Set-Cookie в ответ. Обычный HTTP-клиент, как правило, сохранит его, если не запретить это явно.
RFC 6265 определяет, как пользовательский агент выбирает cookie по домену, пути, атрибутам безопасности и связанным правилам. В его модели хранения нет поля «запись учётных данных, получившая эту cookie». Поэтому две bearer-учётные записи, обращающиеся к одному хосту и пути, могут соответствовать одной сохранённой cookie. Протокол безупречно следует своим правилам, а ваше приложение смешивает учётные записи.
Рассмотрим трассировку из тестового сервиса. Учётная запись alpha использует bearer-токен и получает cookie привязки:
GET /v1/whoami HTTP/1.1
Host: api.example.test
Authorization: Bearer alpha-token
HTTP/1.1 200 OK
Set-Cookie: route=alpha-node; Path=/; Secure; HttpOnly
Content-Type: application/json
{"account":"alpha"}
Следующее действие выбирает beta. Если общее хранилище отправит сохранённую cookie, запрос будет нести два сигнала владения:
GET /v1/whoami HTTP/1.1
Host: api.example.test
Authorization: Bearer beta-token
Cookie: route=alpha-node
Лучший исход: сервис отклонит несоответствие. Менее внимательные сервисы направят запрос beta через закреплённый сеанс alpha, привяжут его к неверному серверному контексту или примут тот заголовок, который окажется приоритетнее. Клиент не может рассчитывать, что удалённый сервис спасёт смешанный запрос.
Для межсервисных API я по умолчанию выбираю прямой подход: отключать неявное хранение cookie. Если API действительно нужны cookie, создайте хранилище cookie для одной записи учётных данных и одного предполагаемого сервисного контекста. Не делите его только потому, что совпадает строка хоста.
Также проверяйте входные данные запроса перед подстановкой учётных данных. Агент, плагин или вызывающий не должен иметь возможности передать собственный заголовок Cookie, который попадёт в запрос с учётными данными. Удаляйте неявные заголовки аутентификации и cookie, затем добавляйте только те заголовки, которые разрешает определение действия. Иначе вы построили изоляцию вокруг двери, которую вызывающие могут обойти.
Для проверок подлинности нужен текущий вызывающий
Ответ 401 не просто код ошибки. Он может предложить клиенту повторить запрос после выбора учётных данных, и эта логика выбора часто находится за пределами кода, создавшего исходный запрос. Обработчики Basic и Digest-аутентификации особенно склонны к такой схеме, но пользовательское промежуточное ПО может так же ошибаться с bearer-токенами.
В плохой реализации хранится изменяемое currentCredential в общем клиенте. Запрос A получает проверку, обработчик загружает секрет alpha, и клиент повторяет запрос. Запрос B начинается до завершения повтора и меняет currentCredential на beta. Теперь в обработчике проверки есть гонка, которую тесты часто не замечают, потому что запускают запросы по одному.
Не исправляйте это блокировкой вокруг глобального поля учётных данных. Блокировка сериализует работу, но за идентичность по-прежнему отвечает не тот объект. Привязывайте запись учётных данных к контексту запроса, переносите её через все повторы и отклоняйте повтор, если контекст отсутствует.
К Digest-аутентификации стоит относиться с особой осторожностью. В ней используются nonce, realm, счётчики и вычисленный ответ. Эти значения привязаны к пространству защиты, которое запросило проверку, а не к абстрактному общему клиенту. Basic-аутентификация проще на уровне протокола, но кэш, который автоматически добавляет её в последующие запросы, всё равно требует той же области действия учётных данных.
Используйте тестовую точку, которая возвращает отдельный realm или проверку для каждой учётной записи. Затем запустите параллельные запросы и убедитесь, что каждый повтор использует запись учётных данных, привязанную к собственному действию. Теста, который проверяет только итоговый статус 200, недостаточно. Сохраняйте на сервере личность запроса и завершайте тест с ошибкой, если проверка alpha приводит к попытке авторизации как beta.
Не принимайте 403 за проверку подлинности. Серверы используют 403 для множества решений об авторизации, и клиент не должен считать это разрешением искать другие учётные данные. Автоматический перебор учётных данных превращает отказ в доступе в проверку учётных записей, что небезопасно и трудно аудировать.
HTTP/2 упрощает сокрытие ошибок владения
HTTP/2 позволяет нескольким потокам использовать одно TLS-соединение. Это эффективно, но убирает привычный признак, когда один запрос ждёт за другим. Клиент может одновременно отправлять действия для alpha и beta, а их заголовки останутся разделёнными, только если библиотека воспринимает их как отдельные запросы до самого нижнего уровня.
RFC 9113 запрещает в запросах HTTP/2 специфичные для HTTP/1.1 заголовки соединения, такие как Connection. Это правило не привязывает cookie или аутентификацию к учётной записи. Оно лишь не позволяет выразить часть поведения промежуточных узлов как обычные заголовки запросов. Ваш клиент всё равно отвечает за выбор cookie, повторов, перенаправлений и любое общее промежуточное ПО.
Коалесцирование соединений HTTP/2 добавляет ещё одну особенность. Некоторые клиенты могут использовать одно защищённое соединение для нескольких origin, когда это разрешают проверка сертификата и имени. Коалесцирование само по себе не воспроизводит заголовок Authorization. Однако оно означает, что идентификатор пула, основанный на поверхностном сравнении хостов, может не описывать соединение, которое клиент фактически выбрал. Отделяйте решения об авторизации origin от оптимизации транспорта и тестируйте поведение используемой библиотеки.
Сжатие заголовков тоже беспокоит людей не по той причине. HPACK сжимает заголовки внутри соединения HTTP/2, а QPACK выполняет похожую работу в HTTP/3. Корректный клиент не декодирует значение Authorization из предыдущего запроса в следующий. Опасность заключается в прямом повторном использовании состояния в коде клиента, а также в возможных проблемах с метаданными в необычных моделях угроз. Помечайте чувствительные заголовки как не подлежащие индексации, если библиотека предоставляет такой контроль, но не считайте это заменой изоляции запросов.
HTTP/3 меняет транспорт с TCP на QUIC. Правило владения от этого не меняется. Пул, который позволяет хранилищу cookie или кэшу учётных данных свободно переходить между действиями, остаётся неверным и с QUIC.
Клиентские сертификаты отличаются. TLS-клиентский сертификат выбирается при установлении соединения, поэтому он действительно привязан к соединению. Никогда не мультиплексируйте разные идентичности клиентских сертификатов через одно соединение или запись пула. Включайте запись сертификата в идентификатор пула или предоставляйте каждому сертификату отдельный экземпляр клиента. Та же осторожность нужна для прокси, который аутентифицирует соединение до пересылки запросов.
Явно задайте идентичность пула в коде
Идентичность пула должна включать каждое значение, меняющее безопасный смысл нового соединения. Для API, использующего только bearer, с отключёнными cookie и без идентичности прокси это могут быть origin и идентификатор записи учётных данных. Для клиентского сертификата, пути с аутентификацией прокси или специального TLS-профиля включайте и эти идентификаторы.
Не используйте текст секрета как идентификатор в map. Это создаёт ненужные копии секрета в памяти и логах, усложняет ротацию и соблазняет кого-то вывести map при отладке. Используйте непрозрачный идентификатор записи учётных данных, сроком жизни которого управляет ваше хранилище или реестр действий.
Этот набросок TypeScript показывает общий подход. В нём используется Pool из Undici, но граница применима к любой клиентской библиотеке. credentialId - это идентификатор, а не значение bearer.
import { Pool } from "undici";
type Boundary = {
origin: string;
credentialId: string;
proxyId?: string;
clientCertificateId?: string;
};
const pools = new Map<string, Pool>();
function poolId(boundary: Boundary): string {
return [
boundary.origin,
boundary.credentialId,
boundary.proxyId ?? "direct",
boundary.clientCertificateId ?? "none"
].join("\u001f");
}
function poolFor(boundary: Boundary): Pool {
const id = poolId(boundary);
let pool = pools.get(id);
if (!pool) {
pool = new Pool(boundary.origin);
pools.set(id, pool);
}
return pool;
}
function headersFor(token: string, input: HeadersInit = {}): Headers {
const headers = new Headers(input);
headers.delete("authorization");
headers.delete("cookie");
headers.delete("proxy-authorization");
headers.set("authorization", `Bearer ${token}`);
return headers;
}
Набросок не даёт переданному вызывающим заголовку Authorization или Cookie попасть в запрос вместе с подставленными учётными данными. Он не реализует хранилище cookie, перенаправления, работу с прокси или настройку сертификатов. Это сделано намеренно: для каждого из этих элементов нужно осознанное решение, а не случайное значение по умолчанию.
Если все учётные данные обращаются к одной анонимной публичной точке, отдельные пулы могут быть излишни. Если точка видит разные учётные записи, используйте отдельные записи пула, пока у вас не появятся конкретная причина и подтверждение безопасности для совместного использования. Пул обходится дёшево по сравнению со смешиванием учётных записей.
Для ротации нужно отдельное правило. Когда запись учётных данных меняется, не назначайте новые действия её старому пулу. Дайте уже отправленным запросам завершиться с исходной записью, если это допускает семантика вашего действия, затем закройте пул. Не перенаправляйте повтор выполняющегося запроса на заменяющую запись. Ротация меняет полномочия, но не переопределяет смысл уже начатой работы.
Популярная рекомендация предлагает отключить keep-alive повсюду. Это уменьшает число общих транспортных объектов, поэтому после инцидента кажется безопасным. Но такой подход скрывает, правильно ли ограничены cookie, код перенаправлений и кэши проверок, и создаёт новую нагрузку от рукопожатий для каждого запроса. Сохраняйте пулинг, явно задавайте его владельца и тестируйте сложные сценарии.
Перенаправления могут отправить чистый запрос не туда
Обработка перенаправлений - это второй клиент, скрытый внутри первого. Библиотека получает ответ 301, 302, 303, 307 или 308, создаёт другой запрос и решает, какие заголовки сохранятся. Если это решение происходит после кода, задающего границу учётных данных, она может перенести заголовок учётной записи или cookie в непредусмотренное место назначения.
Для действий с подставленными учётными данными начните с отключённых или ручных перенаправлений. Проверяйте origin назначения перед переходом. Разрешайте перенаправления только если их ожидает определение действия, место назначения входит в одобренный набор origin и клиент собирает заголовки из исходного контекста учётных данных, а не копирует старый набор заголовков.
Код статуса имеет значение. В обычном поведении браузера 303 может изменить запрос на GET, тогда как 307 и 308 сохраняют метод и тело. Клиент с повторами, который считает их одинаковыми, может повторить запись с учётными данными на другой точке. Это проблема целостности действия, даже если секрет не пересекает origin.
Удаляйте заголовки Authorization и Cookie при каждом перенаправлении между разными origin. Во многих клиентах это уже поведение по умолчанию, но значение по умолчанию не заменяет результат теста. Проверьте используемую версию и конфигурацию. Для перенаправлений внутри одного origin всё равно нужен список разрешённых путей, когда учётные данные дают больше полномочий, чем должна получать исходная точка.
К аутентификации прокси применим тот же подход. Proxy-Authorization относится к выбранному пути через прокси, а не к исходному сервису. Никогда не помещайте его в общую map заголовков по умолчанию. Если два действия достигают одного API через разные аутентифицированные прокси, разделяйте их записи пула и делайте выбор прокси видимым в записи действия.
У неудачного теста изоляции есть узнаваемый вид
Большинство команд тестируют, что alpha может вызвать API и beta может вызвать API. Это доказывает, что учётные данные работают. Но не доказывает, что один долгоживущий клиент может переключиться с alpha на beta, не прихватив лишнее состояние.
Создайте небольшой тестовый сервер с двумя учётными записями, который сообщает, что он получил. Он должен возвращать учётную запись, определённую по Authorization, отражать входящий заголовок Cookie, устанавливать cookie для конкретной учётной записи и предоставлять точку перенаправления. Сервис должен отклонять запрос, когда метка учётной записи в cookie не совпадает с меткой в bearer. Такой отказ делает ошибку заметной, вместо того чтобы слой привязки её скрывал.
Выполните эту последовательность с той же конфигурацией клиента, что используется в рабочей среде:
- Отправьте запрос alpha через свежий пул. Убедитесь, что ответ устанавливает
route=alpha. - Отправьте запрос beta через тот же поиск пула, который использует рабочая среда. Проверьте, что сервер видит авторизацию beta и пустой заголовок Cookie, если только у beta нет отдельного хранилища, содержащего только cookie beta.
- Одновременно запустите запросы alpha и beta по HTTP/2. Пусть каждая точка задержит ответ, чтобы их потоки пересеклись, затем отдельно проверьте идентичности и cookie.
- Верните проверку 401 для каждой учётной записи и убедитесь, что повтор использует исходную запись учётных данных.
- Верните перенаправление на другой origin и убедитесь, что последующий запрос не содержит заголовков авторизации и cookie.
Проверки должны записывать больше, чем коды статуса. Сохраняйте выбранный идентификатор записи учётных данных, идентификатор пула, origin, версию протокола, цель перенаправления, наличие Cookie и учётную запись в ответе. Не записывайте сами учётные данные. При сбое эти поля покажут, вызвана ли утечка поиском пула, неявными заголовками, хранилищем cookie или промежуточным ПО повторов.
Проверяйте и ротацию учётных данных. Запустите запрос, который ждёт на сервере, замените запись учётных данных, затем отправьте новый запрос. Новое действие должно использовать новую идентичность пула. Решите и задокументируйте, завершится ли ожидающее действие с исходными полномочиями или будет отменено. Оба варианта могут быть оправданными, но молча менять полномочия в процессе нельзя.
Запустите тест через каждую поддерживаемую конфигурацию прокси. Состояние прокси и состояние origin часто находятся на разных уровнях библиотеки, поэтому именно здесь успокаивающий модульный тест превращается в полезный интеграционный.
Действия агентов должны иметь меньшую поверхность полномочий
Автономный агент для программирования должен запрашивать действие наподобие «вызови этот одобренный API, используя запись учётных данных billing-read». Он не должен получать токен и создавать широкий долгоживущий клиент, способный хранить состояние сеанса после изменения задачи. Исполнитель действий может привязать одобренное назначение, метод, запись учётных данных и границу клиента ещё до отправки первых байтов.
Sallyport следует этому разделению: он хранит учётные данные API и SSH в зашифрованном хранилище и сам выполняет действие, поэтому агент получает результат, а не учётные данные. Такая изоляция полезна, только если HTTP-путь также рассматривает каждую запись учётных данных как отдельный контекст полномочий.
Одобрение не заменяет изоляцию клиента. Оператор может одобрить законное действие beta, но небрежное общее хранилище cookie превратит его в смешанный запрос alpha и beta. Тогда запись одобрения документирует действие, отличающееся от того, которое оператор собирался разрешить. Помещайте границу ниже экрана одобрения, туда, где реально обрабатываются заголовки, повторы и выбор транспорта.
Для шлюза действий записывайте в событии аудита идентификатор записи учётных данных и идентичность пула, но никогда не секрет. Если вызывающий сообщает о расхождении учётных записей, вам нужно установить, выбрал ли исполнитель правильные учётные данные и добавил ли только состояние, которым владеет эта запись. Защищённый от подмены журнал действий помогает расследовать это, но не делает безопасной неоднозначную схему клиента.
Первое практическое изменение простое: найдите каждый общий HTTP-клиент и перечислите всё, что он помнит помимо открытого соединения. Если ответ включает cookie, ответы на проверки подлинности, перенаправления, клиентские сертификаты или идентичность прокси, назначьте явного владельца каждому элементу до следующей ротации токена или параллельного запуска агента, когда ошибка станет дорогой.
Вопросы и ответы
Может ли HTTP хранить заголовок Authorization в повторно используемом соединении?
TCP- или TLS-соединение обычно не хранит HTTP-заголовок Authorization после завершения запроса. Утечка возникает, когда клиентская обёртка, хранилище cookie, обработчик перенаправлений, настройка прокси или учётные данные, привязанные к соединению, повторно используют состояние предыдущего вызывающего.
Безопасно ли делить HTTP/2-соединение между разными учётными записями?
HTTP/2 мультиплексирует несколько потоков запросов в одном соединении, поэтому при разных учётных данных соединение плохо подходит как граница владения. Привязывайте учётные данные к каждому запросу и делите соединение только после тестов, подтверждающих, что клиент не переносит состояние учётной записи между потоками.
Должны ли разные учётные данные API использовать одно хранилище cookie?
Нет. Хранилище cookie индексирует cookie по хосту, домену, пути и связанным атрибутам, а не по bearer-учётным данным, с которыми они были получены. Общее хранилище может отправить cookie сеанса одной учётной записи вместе с заголовком авторизации другой.
Могут ли два клиентских сертификата использовать один пул HTTP-соединений?
Не используйте один пул для запросов, которые аутентифицируют TLS-соединение разными клиентскими сертификатами. Сертификат выбирается во время рукопожатия, ещё до появления отдельного HTTP-запроса.
Создают ли проверки подлинности с 401 риск утечки учётных данных?
Ответ 401 может заставить клиент выбрать учётные данные из кэша, обработчика запроса или контекста предыдущего запроса. Явно тестируйте запросы с проверкой подлинности и убедитесь, что повторная попытка выбирает только учётные данные, привязанные к текущему действию.
Как называть пулы при ротации токенов?
Пул соединений должен использовать стабильный идентификатор записи учётных данных, а не текст токена и не только хост назначения. При замене записи выводите старый пул из эксплуатации, чтобы новая работа не унаследовала его транспортный контекст.
Стоит ли отключить пул соединений, чтобы избежать смешивания учётных данных?
Нет. Отключение повторного использования может уменьшить симптом, но оставит без проверки общие хранилища cookie, обработку перенаправлений и кэши учётных данных, одновременно повышая задержку и нагрузку на соединения. Задайте явную границу и подтвердите её тестами.
Нужна ли аутентификации прокси отдельная граница пула?
Аутентификация прокси отделена от аутентификации исходного сервиса, однако её тоже можно закэшировать или согласовать в общем соединении с прокси. Включите идентификатор прокси в идентификатор пула и тестируйте его независимо от учётных данных исходного сервиса.
Что должно проверять интеграционное тестирование изоляции учётных данных?
Проверьте личность, которую вернул сервис, наличие входящего заголовка Cookie и выбранную клиентом запись учётных данных. Запустите тот же тест через HTTP/1.1 и HTTP/2, если рабочий клиент может использовать оба протокола.
Как шлюз AI-агента снижает риск раскрытия HTTP-учётных данных?
Шлюз должен получать запрошенное действие и ссылку на учётные данные, сам подставлять секрет и возвращать результат, не передавая секрет агенту. При этом шлюзу всё равно нужны корректные границы на уровне клиента: изоляция секрета не исправляет небрежное общее хранилище cookie.