Доступ «сервис → его потребители»
Типовая задача сегментации: у вас есть сервис (например 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 → New →
portset payments-ports = 8443. Для одного порта впишите его прямо в правило и
пропустите шаг.
5. Политика на сервисе — allow потребителям + deny остальным
Policies → New policy → имя payments-access:
- Host selector:
app=payments— политика действует на хосты сервиса. - Rule 1 (узкий доступ), Priority 10:
- Direction
ingress, Actionallow, Protocoltcp - Dst ports
8443 - Match → Src host sets =
payments-consumers(полеsrc_host_set_ids)
- Direction
- Rule 2 (закрыть остальным), Priority 20:
- Direction
ingress, Actiondeny, Protocoltcp, Dst ports8443 - Src — пусто (любой источник)
- Direction
Порядок критичен (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 |
| jump | role=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 1000 → deny. Добавили новую 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-hostsallow (@11) — сломаете streaming replication: базы перестанут видеть друг друга на 5432.