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

Доступ «сервис → его потребители»

Задача: открыть порт сервиса только хостам-потребителям. В примере API платежей слушает TCP 8443, а доступ получают хосты сервисов checkout и orders.

qf выражает это через лейблы и object-group типа hostset: потребители определяются условием на лейблы (а не ручным списком IP), поэтому новый потребитель получает доступ автоматически, как только помечен нужным лейблом. Доступ задаётся лейблом, а не ручной правкой IP-правил.

Базовая модель (хосты, лейблы, политики, правила, object-groups) — в concepts.md.


Как понятия соответствуют объектам qf

ПонятиеОбъект qf
СервисХосты с лейблом, напр. app=payments
Порт сервисаdst_ports в правиле (или portset)
ПотребительХосты с лейблом, напр. consumes=payments
«Кто является потребителем»hostset-object-group: селектор {consumes=payments} → резолвится в IP этих хостов
«Открыть только потребителям»правило allow от hostset + правило deny остальным на тот же порт

hostsetдинамический: CP держит его актуальным. Добавили хост-потребитель с лейблом consumes=payments — его IP сам попадает в набор, доступ открывается без ручных правок.


В UI: сервис и потребитель — конвенция тегов

Модель «сервис → потребители» держится на совпадении значения тега с двух сторон: провайдер несёт app=payments, потребитель — consumes=payments, и общее значение payments — это то, по чему UI связывает оба конца.

UI знает про эту конвенцию:

  • Тег(и) сервиса — по умолчанию app. Это набор ключей: хост может объявлять сервис под app, а на других машинах — под role; любой из набора считается «я сервис с именем <значение>».
  • Тег потребителя — по умолчанию consumes, один ключ. Потребление нескольких сервисов пишется значением через запятую: consumes=payments,auth.

В фильтрах это связывает обе стороны одним поиском:

  • Hosts → фасет Service: выбрали payments — таблица показывает разом и провайдеров (app=payments), и потребителей (consumes=payments). Один запрос показывает обе стороны отношения.
  • Flows / Verdicts → выбор сервиса сужает список хостов до участников этого сервиса (провайдеры + потребители, с пометкой роли); «View in Hosts» открывает вид по всему флоту с тем же значением.

Это подсказка для навигации, не авторизация. Конвенция тегов влияет только на поиск/фильтры в UI. Реальный доступ по-прежнему определяет hostset-селектор в object-group + правила политики (см. выше) — фильтр не открывает и не закрывает ни одного порта.


Пример

  • Сервис payments-api, слушает TCP 8443, хосты помечены app=payments.
  • Потребители — сервисы checkout и orders, их хосты помечены consumes=payments.
  • Цель: 8443 на payments-хостах доступен только потребителям, всем прочим запрещён.

По этому примеру идут все шаги ниже.


Шаги (Web UI)

1. Лейбл на хостах сервиса

Штатно приезжает из enrollment-токена (label_template = {app: payments}). Уже живые хосты без лейбла — через группу: Host Groups → New → лейбл app=payments → добавить payments-машины.

2. Лейбл на хостах-потребителях

Хосты checkout и orders получают consumes=payments. Этот простой вариант подходит, пока один хост относится к одному набору потребителей. Схема для нескольких сервисов описана в разделе «Масштабирование на несколько сервисов».

3. Object Group типа hostset — «кто потребитель»

Object Groups → New:

  • Тип: hostset
  • Имя: payments-consumers
  • Селектор: consumes=payments

CP преобразует селектор в IP-адреса подходящих хостов и пересчитывает набор при изменении состава.

4. (опционально) Object Group типа portset — порт сервиса

Если у сервиса несколько портов или набор переиспользуется: Object Groups → Newportset payments-ports = 8443. Для одного порта он вписывается прямо в правило, шаг пропускается.

5. Политика на сервисе — allow потребителям + deny остальным

Policies → New policy → имя payments-access:

  • Host selector: app=payments — политика действует на хосты сервиса.
  • Rule 1 (узкий доступ), Priority 10:
    • Direction ingress, Action allow, Protocol tcp
    • Dst ports 8443
    • Match → Src host sets = payments-consumers (поле src_host_set_ids)
  • Rule 2 (закрыть остальным), Priority 20:
    • Direction ingress, Action deny, Protocol tcp, Dst ports 8443
    • Src — пусто (любой источник)

Порядок критичен (first-match-wins): Rule 1 с меньшим Priority ловит потребителей → allow; весь прочий трафик на 8443 падает в Rule 2 → deny. Оба правила обязательны: без deny сработает default-ALLOW и порт откроется всем.

6. Сохранение и переход к блокировке

При первом сохранении Rule 2 задаётся с действием log: новая политика применяется сразу и отдельного предпросмотра для неё нет. После периода наблюдения действие меняется на deny. Для этого изменения создаётся черновик; Зона влияния покажет payments-хосты и различия правил до действия Применить.


Проверка

  • Хост сервиса → вкладка Ruleset: оба правила присутствуют в порядке 10, 20.
  • С хоста-потребителя обращение на payments:8443 — проходит; с постороннего — reset/таймаут.
  • Хост сервиса → Verdicts: deny-события от посторонних (если на Rule 2 включить Log).
  • Хост сервиса → Counters: растёт счётчик Rule 2 на блокировках.

Рекомендация: сначала Rule 2 — действием log (не deny), с наблюдением в Verdicts, кто реально стучится на 8443 (не забыт ли легитимный потребитель). После проверки — log меняется на deny.


Эквивалент в Terraform

Если стационар (object-groups и политики) ведётся как код:

resource "qf_object_group" "payments_consumers" {
name = "payments-consumers"
type = "hostset"
spec = jsonencode({ selector = { matchLabels = { consumes = "payments" } } })
}

resource "qf_policy" "payments_access" {
name = "payments-access"
priority = 100
selector = jsonencode({ matchLabels = { app = "payments" } })

rule { # узкий доступ потребителям
priority = 10
action = "allow"
match = jsonencode({
protocol = "tcp"
dst_ports = ["8443"]
src_host_set_ids = [qf_object_group.payments_consumers.id]
})
}
rule { # закрыть остальным
priority = 20
action = "deny"
match = jsonencode({ protocol = "tcp", dst_ports = ["8443"] })
}
}

Подводные камни

  • IPv4 и IPv6. CIDR, ipset и conntrack работают с обеими семьями адресов. Обработка IPv6 включается на хосте параметром QF_DROP_IPV6=false; по умолчанию весь IPv6-трафик блокируется.
  • Ограничивается вход на сервис. Правило открывает входящий трафик на порт сервиса. Если нужно ограничить и исходящий с потребителей (чтобы checkout ходил только в payments) — отдельная политика с селектором consumes=payments и egress-правилами. Обычно не требуется.
  • Лимит правил на хост. 2048 правил на хост (нужны eBPF-фичи bpf_loop+ring-buffer, mainline ≥5.17 или бэкпорт). Много сервисов на одном хосте — счётчик стоит отслеживать (страница хоста → Datapath & capabilities).
  • hostset вычисляется при компиляции. После появления нового потребителя CP пересчитывает набор и отправляет хосту новое поколение правил.
  • Пустой hostset не открывает доступ всем. Если последний потребитель вышел из selector (или у matched-хостов нет dataplane-IP), CP отвергает новый bundle и сервисный хост продолжает исполнять last-good generation. В UI/API effective ruleset constraint будет unresolved; после появления подходящего хоста CP пересчитает набор.
  • consumes — мутируемое измерение. Если состав «кто кого потребляет» меняется часто и массово, лейбл хранится в Host Group, а не в enrollment-токене: токен не поддерживает массовое изменение уже выданных лейблов. См. fleet-management.md.

Масштабирование на несколько сервисов

matchLabels сравнивает значение целиком. Поэтому селектор consumes=payments не совпадёт с UI-лейблом consumes=payments,auth: запятая разбирается только фильтрами UI, но не компилятором политик.

Для хоста, который потребляет несколько сервисов, каждому отношению нужен отдельный ключ, например consumes-payments=true и consumes-auth=true. Тогда payments-consumers использует селектор consumes-payments=true, а auth-consumersconsumes-auth=true. Если нужна навигация по конвенции UI, дополнительный лейбл consumes=payments,auth можно хранить только как поисковую подсказку; права доступа на нём не строятся.


Развёрнутый пример: кубер-кластер + БД + jump, всё прочее закрыто

Этот пример объединяет доступ к PostgreSQL, SSH через jump-хост и baseline deny:

  • Кубер-кластер — приложения на worker-нодах. qf — хостовый firewall, ловит трафик на TC-хуке ноды, не пода (IP подов динамичны, датапат — IPv4-по-хостам). Поэтому «потребитель БД» — это worker-ноды.
  • Postgres — primary + реплики на выделенных хостах.
  • Jump-хост — единственный вход по SSH ко всем серверам.
  • Цель: 5432 на postgres-хостах открыт только worker-нодам; 22 на всех хостах — только с jump; весь остальной входящий трафик закрыт.

Лейблы

ХостыЛейблы
postgres primary + репликиapp=postgres
worker-нодыrole=k8s-worker, consumes=postgres
jumprole=jump

Object-groups (hostset — динамические, CP держит IP актуальными)

  • pg-consumers — селектор consumes=postgres (worker-ноды)
  • pg-hosts — селектор app=postgres (postgres-хосты; нужен для репликации между собой)
  • jump-hosts — селектор role=jump

Политики

1. postgres-access — host selector app=postgres:

  • Rule allow, priority 10: ingress tcp 5432, src host set = pg-consumers
  • Rule allow, priority 11: ingress tcp 5432, src host set = pg-hosts (реплика↔реплика)

2. ssh-jump — host selector * (все хосты):

  • Rule allow, priority 10: ingress tcp 22, src host set = jump-hosts

3. baseline-deny — host selector *, самый низкий приоритет:

  • Rule deny, priority 1000: ingress, src — пусто (любой источник)

Как это открывает доступ

Эффективный ruleset на хосте — правила всех подходящих политик, отсортированные по priority, first-match-wins. Postgres-хост собирает (в порядке приоритета):

  • Rule allow, priority 10: ingress tcp 5432, src host set = pg-consumers
  • Rule allow, priority 10: ingress tcp 22, src host set = jump-hosts (из ssh-jump)
  • Rule allow, priority 11: ingress tcp 5432, src host set = pg-hosts
  • Rule deny, priority 1000: ingress, src — любой (из baseline-deny)

Worker-нода на 5432 ловит первое правило → allow; jump на 22 → allow; весь прочий входящий трафик доходит до priority 1000deny. Добавили новую worker-ноду с consumes=postgres — её IP автоматически попадает в pg-consumers, и доступ открывается без изменения правил.

Типичные ошибки этого сценария

  • «Закрыть всё» = явный baseline deny-all. Default в qf — ALLOW; без политики 3 порты открыты всем. Именно правило deny @1000 даёт default-deny.
  • deny-all нарушит работу сети кластера. Сплошной deny ingress на worker-нодах блокирует kubelet/etcd/overlay (VXLAN, 10250, 2379–2380, 6443). Либо добавляется allow на cluster-internal порты (напр. src = pg-consumers для внутрикластерного трафика между нодами), либо baseline сужается до управляемых портов. Сплошной deny-all на worker-ноды без проверки не применяется — сначала log, затем Verdicts.
  • Реплика → реплика. Забыли pg-hosts allow (@11) — сломаете streaming replication: базы перестанут видеть друг друга на 5432.