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

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

Типовая задача сегментации: у вас есть сервис (например API платежей на порту 8443) и его потребители — другие сервисы, которым можно к нему обращаться. Нужно открыть порт сервиса только потребителям, а всем остальным — закрыть.

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. Потребляет сразу несколько сервисов — вешаете несколько лейблов (consumes=payments, consumes=auth, …).

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. Preview impact → Save

Preview impact покажет точный список payments-хостов, которые заденет политика. Save — правила компилируются и уезжают на хосты за пару секунд.


Проверка

  • Хост сервиса → вкладка 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 матчат обе семьи; v6-энфорс включается на хосте QF_DROP_IPV6=false (по умолчанию v6 режется целиком — opt-in гейт).
  • Ограничивается вход на сервис. Правило открывает входящий трафик на порт сервиса. Если нужно ограничить и исходящий с потребителей (чтобы checkout ходил только в payments) — отдельная политика с селектором consumes=payments и egress-правилами. Обычно не требуется.
  • Лимит правил на хост. 2048 правил на хост (нужны eBPF-фичи bpf_loop+ring-buffer, mainline ≥5.17 или бэкпорт). Много сервисов на одном хосте — следите за счётчиком (страница хоста → Datapath & capabilities).
  • hostset резолвится при компиляции. Новый потребитель → каскад пересчитывает и дошлёт бандл автоматически (пара секунд), не мгновенно.
  • consumes — мутируемое измерение. Если состав «кто кого потребляет» меняется часто и массово, держите лейбл в Host Group, а не в enrollment-токене (массового релейбла токена нет). См. fleet-management.md.

Масштабирование на много сервисов

Паттерн повторяется на каждый сервис: своя пара лейблов (app=X / consumes=X), свой hostset (X-consumers), своя политика (X-access). Лейблы consumes=* складываются — один хост может быть потребителем нескольких сервисов одновременно.


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

Более жизненная постановка, где виден весь механизм разом:

  • Кубер-кластер — приложения на 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.