Перейти к основному содержимому

Схемы размещения фильтра: шлюз, исполнитель или оба

Политика http_filter задаётся в каждом компоненте отдельно: общей конфигурации у шлюза и исполнителя нет (см. Конфигурирование приложения). Поэтому «включить фильтр» — это не одно решение, а выбор схемы: где именно принимается решение о допуске запроса.

Практически осмысленных схем три:

Схемаhttp_filter шлюзаhttp_filter исполнителяКратко
A. Двойной контрольAllowListAllowList (та же политика)Максимальная защита: граница отсекает лишнее, исполнитель остаётся последним рубежом перед целевым сервисом.
B. Контроль на границеAllowListAllowAllСамая дешёвая по ресурсам и по эксплуатации; опирается на доверие к внутреннему транспорту и на целостность процесса шлюза.
C. Контроль в глубинеAllowAllAllowListОсмысленна, когда границей владеет не та команда, что целевым сервисом, или как этап поэтапной раскатки.

Во всех схемах речь идёт только о транзитном трафике клиентов. Служебные точки /health и /metrics самих компонентов работают на отдельных портах и фильтром не обрабатываются.

Что общего у всех схем: откуда вообще берутся запросы

Выбор схемы разумно делать, держа в голове одно свойство архитектуры (подробный разбор — в статье Взаимодействие gateway ↔ executor):

  • единственный способ поместить запрос в очередь шлюза — прислать его на внешний HTTP-порт шлюза; API «положить запрос в очередь» у мостов нет;
  • исполнитель не принимает входящих соединений от Netgap: он сам, по pull-модели, забирает запросы из моста запросов шлюза по mTLS (TLS 1.3) с проверкой ролевого SPIFFE ID сервера;
  • каждое внутреннее соединение авторизуется отдельно: у каждого моста свой allowlist SPIFFE ID (deny by default), с проверкой цепочки и CRL при каждом рукопожатии.

Отсюда следует и сильная сторона схемы B (в очередь нельзя «подбросить» запрос снаружи), и смысл схемы A (в очередь можно подбросить запрос изнутри самого шлюза, если шлюз скомпрометирован или запущен с другой политикой).

Схема A: двойной контроль (шлюз + исполнитель)

Одинаковая политика AllowList в обеих конфигурациях. Это схема по умолчанию для промышленной эксплуатации и именно она собрана в примерах поставки config/netgap-gateway-restricted.yaml и config/netgap-executor-restricted.yaml.

# в конфигурации netgap-gateway и в конфигурации netgap-executor — посимвольно одинаково
http_filter:
mode: AllowList
rules:
- paths:
- /api/reports/**
methods:
- GET
- paths:
- /api/orders
methods:
- GET
- POST

Сиквенс: обычный запрос проходит две независимые проверки

Ключевая деталь: в GatewayRequest нет поля «фильтр пройден». Исполнитель не доверяет решению шлюза и не может его унаследовать — он принимает собственное решение по собственной конфигурации. Именно поэтому вторая проверка что-то значит.

Сиквенс: граница скомпрометирована или работает со старой политикой

Это ситуация, ради которой схема и существует. Запрос попал в очередь, минуя ограничение на границе, — например, процесс шлюза скомпрометирован, подменён бинарь, или шлюз перезапущен с ошибочной конфигурацией AllowAll.

Побочный, но ценный эффект: рост netgap_executor_blocked_requests при нулевом netgap_gateway_blocked_requests — однозначный сигнал, что граница работает не с той политикой, которую ожидает доверенный сегмент. Это готовое правило алертинга (см. Заблокированные запросы).

От каких векторов защищает вторая проверка

ВекторКак реализуетсяЧто даёт проверка на исполнителе
Компрометация процесса шлюза (RCE, подмена бинаря, эксплуатация уязвимости в разборе HTTP на границе)Атакующий получает контроль над компонентом, который стоит ближе всего к недоверенной сети, и формирует произвольные запросы уже «изнутри»Целевой сервис остаётся доступен только в пределах политики: расширить поверхность атаки на нём нельзя без второй компрометации — уже в доверенном сегменте
Ошибочная или неполная раскатка политики (шлюз запущен с mode: AllowAll или со старым файлом конфигурации)Человеческий фактор, откат конфигурации, рассинхронизация окруженийОграничение продолжает действовать; факт расхождения виден в аудите и метриках исполнителя, а не остаётся незамеченным
Подмена или добавление компонента на границе (второй экземпляр шлюза, «временный» шлюз для отладки, будущий входящий балансировщик с собственной конфигурацией)Запросы приходят из авторизованного мостами источника, но политика источника неизвестнаПолитика проверяется там, где она принадлежит владельцу целевого сервиса, и не зависит от количества и настроек компонентов на границе
Злоупотребление доступом к мостам изнутри доверенного сегментаВладелец валидного ключа с ролью http1-request-reader/http1-response-writer (см. остаточные риски внутреннего транспорта)Ограничивает не сам доступ к мостам, но сужает то, что через контур можно выполнить на целевом сервисе
Расширение политики «по ошибке в одну сторону»Кто-то расширил правила на границе, не согласовав с владельцем целевого сервисаНовый путь или метод не заработает, пока изменение не внесено в обе конфигурации — политика становится согласованным решением двух сторон

Чего вторая проверка не даёт

Честные границы схемы, чтобы не переоценивать её вклад:

  • Это не защита от ошибки в самой реализации фильтра. Оба компонента используют один и тот же код сопоставления путей и методов: дефект матчера повторится на обоих рубежах одинаково.
  • Это не защита от ошибки в политике. Правило, ошибочно разрешающее лишнее, будет одинаково ошибочным в обеих конфигурациях — на то она и одинаковая политика.
  • Это не защита целевого сервиса от доверенного сегмента. Кто может обратиться к целевому сервису напрямую, минуя исполнителя, тот не ограничен фильтром вообще; здесь работают сетевая изоляция и аутентификация на самом сервисе.
  • Это не аутентификация. Фильтр не смотрит на токены, заголовки и источник — см. Границы применимости.

Цена схемы

По производительности — на каждый запрос:

  • дополнительное сопоставление пути и методов в исполнителе перед вызовом целевого сервиса. Операция дешёвая и предсказуемая: сравнение строк по сегментам пути, без регулярных выражений и без обращений вовне; стоимость линейна по числу правил и путей в них. На фоне разбора HTTP, двух вызовов gRPC поверх mTLS и собственно вызова целевого сервиса вклад пренебрежимо мал и в наблюдаемых величинах (netgap_executor_target_call_time_seconds, гистограммы задержек шлюза) обычно не различим;
  • заметным этот вклад становится только на политиках с сотнями и тысячами правил: проверка выполняется дважды, поэтому и «дорогая» политика оплачивается дважды. Практическая рекомендация — держать политику компактной (объединять пути в одном правиле, использовать шаблоны по сегментам вместо перечисления), а не отказываться от второй проверки.

По эксплуатации — постоянно:

  • две конфигурации нужно поддерживать синхронно, а любое изменение политики — это перезапуск обоих компонентов (конфигурация читается один раз при старте);
  • в момент раскатки существует окно рассинхронизации; как его пройти безопасно — см. Порядок изменения политики;
  • блокировка на исполнителе — «поздняя»: запрос уже занял место в очереди, удержал клиентское соединение в пуле и прошёл два вызова мостов. В штатном режиме таких блокировок нет (их отсекает граница), но при рассинхронизации ресурсы границы расходуются впустую.

Схема B: контроль только на шлюзе (исполнитель AllowAll)

# netgap-gateway
http_filter:
mode: AllowList
rules:
- paths:
- /api/reports/**
methods:
- GET
# netgap-executor
http_filter:
mode: AllowAll

Сиквенс: решение принимается один раз, на границе

Почему схема надёжна

Надёжность здесь опирается не на «доверие по умолчанию», а на конкретные свойства контура:

  • Единственная точка входа. Единственный способ попасть в очередь шлюза — внешний HTTP-порт, который защищён фильтром. Другого канала подачи запроса не существует: у мостов нет операции «положить запрос», исполнитель работает по pull-модели и сам инициирует все внутренние соединения.
  • Внутренний транспорт неотключаем и криптографически защищён. Обмен между шлюзом и исполнителем идёт только по mTLS (TLS 1.3) с взаимной аутентификацией; plaintext-режима и опции «не проверять сертификат» в продукте нет (ADR 0009).
  • Авторизация раздельная и deny by default. Каждый мост принимает только клиентов из своего allowlist ролевых SPIFFE ID, с проверкой цепочки до корней из конфигурации и CRL при каждом рукопожатии. Сертификат чужой инсталляции или чужой роли отклоняется до передачи прикладных данных.
  • Исполнитель не адресуем снаружи. Он не публикует входящих точек для трафика и разговаривает только с сервером, предъявившим ожидаемый ролевой SPIFFE ID, — перенаправить его на подставной «шлюз» подменой DNS или адреса не получается.
  • Никакого персистентного следа конфигурации. Политика приходит одним YAML-документом в stdin и не может быть подменена файлом на диске или переменной окружения.

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

Какие векторы и условия остаются

ВекторУсловие реализацииВероятностьПоследствия и что видно оператору
Ошибочная конфигурация границы: шлюз запущен с mode: AllowAll или с устаревшими правиламиОшибка раскатки, откат конфигурации, ручной перезапуск «чтобы быстрее», рассинхронизация окруженийНаиболее вероятная из всех — человеческий фактор, не требующий атакующегоОграничение исчезает полностью и тихо: целевой сервис доступен по всем путям. В метриках это выглядит как отсутствие блокировок, то есть «всё хорошо»; отдельного сигнала нет — в схеме A такой случай подсветил бы счётчик блокировок исполнителя
Компрометация процесса шлюза: RCE, подмена бинаря или конфигурации при запускеУязвимость в компоненте на границе либо доступ к хосту/пайплайну запуска шлюзаНизкая: компонент на Rust, узкая функциональность, конфигурация не берётся с диска. Но это именно тот компонент, который смотрит в недоверенную сетьПолный обход политики; целевой сервис доступен целиком в пределах сетевой достижимости исполнителя. В аудите — штатные события прохождения запросов, признаков блокировки нет
Дополнительный или подменённый источник запросов на границе (второй шлюз, отладочный экземпляр, будущий входящий балансировщик со своей конфигурацией)Административное решение или изменение топологии без пересмотра политикиНизкая-средняя, растёт вместе со сложностью инсталляцииПолитика фактически определяется настройками нового компонента; исполнитель принимает всё, что пришло из авторизованного моста
Злоупотребление внутренним доступом: владелец валидного ключа роли исполнителя или узел в доверенном сегментеКомпрометация приватного ключа (окно — до попадания в CRL и перезапуска компонентов) либо доступ к сегментуНизкая при коротких leaf-сертификатах и автоматизации выпускаФильтр этот вектор не закрывает ни в одной из схем: обратиться к целевому сервису напрямую можно, минуя Netgap. Компенсация — сетевая изоляция и аутентификация на целевом сервисе
Расхождение «что видит граница» и «что делает исполнитель»Запрос модифицируется на пути между компонентамиПренебрежимо малаЦелостность канала обеспечивает TLS 1.3: модификация трафика приводит к разрыву соединения, а не к проходу изменённого запроса

Итог: единственный реалистичный сценарий полной потери контроля в схеме B — это состояние самого шлюза (его конфигурация или целостность его процесса). Схема B ровно на этом и стоит: она принимает целостность границы как допущение, а схема A — нет.

Компенсирующие меры, если выбрана схема B

  • Доставлять конфигурацию шлюза только из доверенного secret-provider, без промежуточных файлов на диске (см. Конфигурирование приложения).
  • Контролировать факт применения политики после каждого перезапуска: прогнать на границе заведомо запрещённый запрос и убедиться, что он получает 403/405 и приращение netgap_gateway_blocked_requests. Проверка «фильтр включён» дешевле, чем расследование по факту.
  • Не рассчитывать на фильтр как на единственный контроль доступа: аутентификация на целевом сервисе остаётся обязательной.
  • Помнить, что http_filter: mode: AllowAll в исполнителе — это осознанный выбор схемы, зафиксированный в конфигурации: режим всегда задан явно, «забыть» секцию нельзя — без неё компонент не запустится.

Схема C: контроль только на исполнителе (шлюз AllowAll)

Схема-зеркало предыдущей, и она имеет смысл, хотя выглядит менее выгодной.

Когда осмысленна:

  • разное владение компонентами: границей управляет команда периметра, целевым сервисом и исполнителем — команда сервиса; политика допуска принадлежит владельцу API и должна действовать независимо от настроек границы;
  • на границе уже стоит внешний контроль — WAF, API-gateway или сетевые политики, — и дублировать его правилами Netgap на шлюзе не хотят, но последний рубеж перед целевым сервисом нужен;
  • этап поэтапной раскатки: ужесточение сначала обкатывается в доверенном сегменте, где блокировки видны в метриках исполнителя, и только потом переносится на границу;
  • несколько источников на границе: если запросы могут приходить от разных компонентов, единая точка применения политики — исполнитель.

Цена схемы:

  • блокировка поздняя и дорогая. Запрещённый запрос занимает место в неограниченной по размеру очереди шлюза, паркует клиентское TCP-соединение в пуле, проходит вызовы обоих мостов поверх mTLS — и только потом получает отказ. Поток сканирующих запросов из недоверенной сети нагружает границу так, как если бы фильтра не было вовсе;
  • граница не защищена от исчерпания ресурсов самой политикой: см. остаточный риск «шлюз буферизует запросы в памяти» во взаимодействии компонентов;
  • теряется диагностический сигнал. В схемах A и B рост netgap_executor_blocked_requests при нулевом netgap_gateway_blocked_requests означает рассинхронизацию политик. В схеме C это штатное состояние, поэтому такой алерт настроить нельзя, и разбирать блокировки придётся по событиям аудита;
  • высокая кардинальность меток path в метриках блокировок исполнителя на сканирующем трафике — ограничивайте её на стороне сбора метрик (см. Метрики блокировок).

Клиент при этом получает ровно те же 403/405 с пустым телом, что и в схеме B, — снаружи разница не видна. То есть схема C даёт тот же уровень ограничения доступа к целевому сервису, но оплачивает его ресурсами границы.

Как выбрать

КритерийA. Двойной контрольB. Только шлюзC. Только исполнитель
Защита при компрометации/ошибке конфигурации шлюзаданетда
Ранний отказ, экономия ресурсов границыдаданет
Стоимость сопоставления на запросдвойнаяодинарнаяодинарная
Сложность эксплуатации (синхронизация, перезапуски)высокаянизкаянизкая
Работает сигнал «политики разошлись»даданет
Типовое применениепромышленный контур с недоверенной внешней сетьювнутренние и полудоверенные контуры, стенды с ограниченным доступомразное владение компонентами, внешний WAF на границе, этап раскатки

Рекомендация по умолчанию — схема A: её вычислительная цена мала и предсказуема, а закрывает она самый неприятный класс сценариев — те, где граница перестала быть надёжной. Схема B оправдана там, где целостность и конфигурация шлюза контролируются теми же средствами и той же командой, что и доверенный сегмент. Схема C — осознанный выбор в пользу владения политикой, а не в пользу производительности.

Порядок изменения политики в двух компонентах

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

  • Сужение политики (убираем путь или метод): сначала шлюз, затем исполнитель.
  • Расширение политики (добавляем путь или метод): сначала исполнитель, затем шлюз.
  • Переход B → A: сначала привести исполнителя к AllowList с той же политикой — это расширение ограничения в глубину, внешнее поведение не меняется.
  • Переход A → B: сначала перевести исполнителя в AllowAll, политика на границе продолжает действовать без перерыва.

После каждого перехода проверяйте по метрикам, что блокировки происходят там, где ожидается: netgap_gateway_blocked_requests — на границе, netgap_executor_blocked_requests — нулевой в схемах A и B.

Ссылки