Установка qf в закрытом контуре (Control Plane + агенты)
Подробное руководство по развёртыванию qf в среде без доступа в интернет (air-gap).
Покрывает подготовку артефактов, установку Control Plane в Kubernetes, настройку доверия и
энролмента, установку агентов на хосты и проверку. Плейсхолдеры (qf-cp.corp.local,
registry.corp.local) заменяйте на свои.
0. Архитектура и порты
Один Control Plane (в Kubernetes) обслуживает весь парк агентов. Агент держит с CP два канала и ходит в REST:
| Канал | Назначение | Порт по умолчанию (в чарте) |
|---|---|---|
| gRPC (mTLS) | стационарный стрим: пуш правил, телеметрия | 8443 |
| gRPC enrollment (без mTLS) | первичный ввод хоста (bootstrap) | 8444 |
| REST/HTTPS | API + UI (для операторов и Terraform) | 8080 за TLS-прокси |
Как агент вычисляет адреса. Агент получает один параметр QF_ENDPOINT и выводит из
него остальное:
QF_ENDPOINT=host(без порта) → gRPChost:31443, enrollhost:31444, RESThttps://host.QF_ENDPOINT=host:P(с портом) → gRPChost:P, enrollhost:P+1, RESThttps://host.
REST всегда https://<host> без порта (то есть за прокси на 443). Поэтому, если публикуете
gRPC на порту 8443, агент с QF_ENDPOINT=qf-cp.corp.local:8443 пойдёт в enrollment на
8444 и в REST на https://qf-cp.corp.local. Держите пары портов согласованными
(gRPC=P, enroll=P+1).
┌─────────────── Kubernetes ───────────────┐
агенты ──gRPC:8443(mTLS)──► Service (NodePort/LB) ──► qf-cp (2 реплики)
агенты ──enroll:8444──────► │
операторы ─HTTPS:443─► Ingress/прокси ──REST:8080──► │
PostgreSQL (в контуре)
1. Что подготовить в контуре
1.1 Артефакты (занести в контур заранее)
- Образ Control Plane — контейнер
qf-cpнужной версии, залитый в внутренний реестр контура (registry.corp.local/qf-cp:<версия>). - Helm-чарт
qf-cp— как OCI-артефакт во внутреннем реестре либо распакованный каталог чарта. - Пакеты агента —
qf-agent_<версия>_amd64.debи/илиqf-agent-<версия>-1.x86_64.rpm, размещённые во внутреннем apt/dnf-репозитории или просто скопированные файлом на control-ноду Ansible.
В закрытом контуре не используется загрузка релизов из GitHub — только внутренний реестр/репозиторий или копия файлом.
1.2 Инфраструктура
- Kubernetes-кластер (для CP). При
replicaCount > 1нужен RWX-storage под PKI-том (см. 3.3) и общийQF_JWT_SECRET. - PostgreSQL внутри контура, доступный из кластера (строка подключения — в секрет CP).
- Внутренний CA или способ выпустить TLS-сертификат для REST/UI (HTTPS).
- Средство публикации портов наружу кластера:
NodePort,LoadBalancerили L4-проброс (HAProxy/nginx stream). gRPC-канал mTLS терминировать нельзя — проксируйте на L4 (TCP passthrough), иначе mTLS сломается.
1.3 Требования к хостам-агентам
- Linux с eBPF-хелпером
bpf_loop+ ring-buffer мапой (mainline ≥5.17 или дистро-бэкпорт; агент пробит фичи при старте) и включённым BTF (/sys/kernel/btf/vmlinuxприсутствует) — нужно для eBPF. На большинстве современных дистрибутивов BTF включён. - Для сосуществования с Cilium — ядро ≥6.6 (там qf использует TCX). На <6.6 рядом с Cilium агент не стартует (намеренно, по результату проверки).
- Архитектура x86_64.
2. Занос артефактов в контур
Схема стандартная для air-gap: собрать/скачать на «грязной» стороне, перенести носителем, залить во внутренние реестры.
# на стороне с интернетом — сохранить образ
podman pull ghcr.io/<owner>/qf-cp:<версия>
podman save ghcr.io/<owner>/qf-cp:<версия> -o qf-cp-<версия>.tar
# перенести qf-cp-<версия>.tar, чарт и пакеты агента в контур, затем:
podman load -i qf-cp-<версия>.tar
podman tag ghcr.io/<owner>/qf-cp:<версия> registry.corp.local/qf-cp:<версия>
podman push registry.corp.local/qf-cp:<версия>
Helm-чарт и пакеты .deb/.rpm положите во внутренний OCI-реестр и apt/dnf-репозиторий
соответственно (или оставьте файлами для установки вручную/через Ansible source=file).
3. Установка Control Plane (Helm)
3.1 Секреты
Обязательные значения (хранятся в Kubernetes Secret):
QF_DB_DSN— строка подключения к PostgreSQL контура.QF_MASTER_KEY— 32-байтный ключ в hex (шифрует ключ CA в БД). Сгенерировать:openssl rand -hex 32. Храните вне контура/в KMS; при потере CA не расшифровать.QF_JWT_SECRET— ≥32 байта. Обязателен приreplicaCount > 1(иначе per-pod эфемерные секреты ломают проверку JWT между репликами).openssl rand -hex 32.- Bootstrap-админ:
QF_ADMIN_USERNAME,QF_ADMIN_PASSWORD(и опц.QF_ADMIN_EMAIL).
3.2 values для контура
Минимальный values-airgap.yaml (подставьте свои):
replicaCount: 2
image:
repository: registry.corp.local/qf-cp # внутренний реестр
tag: "<версия>"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: corp-registry # если реестр требует авторизации
service:
type: NodePort # или LoadBalancer; для L4-проброса подойдёт NodePort
httpPort: 8080
grpcPort: 8443
enrollPort: 8444
ingress:
enabled: true # REST/UI за HTTPS
className: nginx
host: qf-cp.corp.local
tls:
- secretName: qf-cp-tls
hosts: [qf-cp.corp.local]
pki:
persistentVolume:
size: 1Gi
accessMode: ReadWriteMany # RWX обязателен при replicaCount > 1
storageClass: "<ваш-rwx-класс>"
secrets:
dbDSN: "postgres://qf:***@postgres.corp.local:5432/qf?sslmode=require"
masterKey: "<hex-32-байта>"
jwtSecret: "<hex-32-байта>"
adminUsername: "admin"
adminPassword: "<сложный-пароль>"
env:
QF_LOG_LEVEL: info
# ВНЕШНИЙ адрес, по которому агенты видят CP. КРИТИЧНО: не localhost.
QF_CP_HOST: qf-cp.corp.local
QF_CP_ENDPOINT: "qf-cp.corp.local:8444"
# если REST за реверс-прокси — указать CIDR прокси, чтобы верно видеть client IP:
# QF_TRUSTED_PROXIES: "10.42.0.0/16"
Важное:
QF_CP_HOST/QF_CP_ENDPOINT— внешний адрес CP (неlocalhost), иначе агенты не подключатся.accessMode: ReadWriteManyпри 2 репликах — иначе PKI-том привяжется к одной ноде, и вторая реплика зависнет вPending.- gRPC (
8443) и enrollment (8444) должны быть доступны агентам; REST (8080) — за TLS-прокси на 443.
3.3 TLS для REST/UI
Выпустите сертификат внутреннего CA на qf-cp.corp.local и положите его в secret
qf-cp-tls (tls.crt + tls.key), на который ссылается ingress.tls. Это TLS канала
операторов/Terraform; к mTLS агентов он отношения не имеет (тот держит собственный PKI CP).
3.4 Установка
kubectl create namespace qf
# чарт из внутреннего OCI-реестра:
helm upgrade --install qf-cp oci://registry.corp.local/charts/qf-cp \
--version <версия чарта> -n qf -f values-airgap.yaml
# или из распакованного каталога:
# helm upgrade --install qf-cp ./qf-cp -n qf -f values-airgap.yaml
3.5 Проверка CP
kubectl -n qf get pods # обе реплики Running/Ready
kubectl -n qf get svc # порты 8080/8443/8444 опубликованы
curl -sk https://qf-cp.corp.local/healthz # ответ здоровья
Зайдите в UI (https://qf-cp.corp.local) под bootstrap-админом и смените пароль.
3.6 Экспорт якоря доверия CA (для энролмента агентов)
Агентам нужен якорь доверия к CA control-plane. Достаньте его один раз:
# PEM CA:
curl -sk https://qf-cp.corp.local/pki/ca.crt -o ca.crt
# отпечаток (для air-gap pin по хэшу, вместо переноса PEM):
curl -sk https://qf-cp.corp.local/pki/ca.sha256
# либо из PEM локально:
openssl x509 -in ca.crt -noout -fingerprint -sha256 # привести к нижнему регистру, убрать ':'
Разнесите ca.crt (или его отпечаток) на хосты вне канала связи — это защищает первый,
ещё не-mTLS, контакт от подмены.
4. Подготовка энролмента
4.1 Bulk-токен с лейблами (массовый ввод)
Для парка выпускается bulk-токен — один токен на много хостов, который автоматически размечает каждый входящий хост. Создать (UI → API Tokens/Enrollment, или REST):
curl -sk -X POST https://qf-cp.corp.local/tokens \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"type": "bulk",
"ttl_seconds": 86400,
"max_uses": 200,
"label_template": { "env": "prod", "tier": "web" },
"target_group_id": "<uuid-группы-или-опустить>"
}'
# → в ответе поле token
label_template— лейблы, которые CP штампует на каждый энроллящийся по токену хост. Именно по этим лейблам селекторы политик решают, кого касаются правила → правила доставляются на хост автоматически, без ручных шагов.target_group_id— опциональная host-группа, в которую хост попадёт автоматически.max_usesиttl_secondsограничивают токен. Токен одноразовый на хост и стирается из конфига агента после успешного энролла.
Под разные роли выпускайте разные токены с разными label_template (web, db, mgmt…).
4.2 Политики (заранее или параллельно)
Заведите политики, чьи селекторы матчат ваши лейблы (через UI или Terraform-провайдер). Как
только хост энроллится и получает лейблы — соответствующий ruleset компилируется и отправляется на хост.
Fleet-wide политики (selector: {}) требуют явного подтверждения (защита от массового применения).
5. Установка агентов
Агент — пакет .deb/.rpm. В контуре ставим из внутреннего репозитория или файлом.
Конфиг агента — /etc/qf/agent.conf (env-стиль, права 0600).
5.1 Минимальный /etc/qf/agent.conf
QF_ENDPOINT=qf-cp.corp.local:8443 # gRPC-порт; enroll выведется как 8444, REST https
QF_ENROLL_TOKEN=<bulk-токен> # стирается агентом после успешного энролла
QF_ENROLL_CA=/etc/qf/ca.crt # якорь доверия (см. 3.6) — ОБЯЗАТЕЛЕН
# альтернатива переносу PEM — pin по отпечатку (CA тянется по REST, принимается по хэшу):
# QF_ENROLL_CA_FINGERPRINT=<64-символьный sha256>
# опционально:
# QF_IFACE=eth0 # по умолчанию — интерфейс дефолтного маршрута
# QF_PKI_DIR=/etc/qf # где агент хранит cert/key
# QF_MASK_MAC=true # приватность: хранить только OUI MAC
Про якорь доверия — ровно один режим, порядок приоритета: QF_ENROLL_CA_FINGERPRINT
(air-gap, хэш вне канала) → QF_ENROLL_CA (переданный PEM) → QF_ENROLL_CA_FETCH=true
(TOFU по REST, только если REST-серт CP публично доверенный — в закрытом контуре обычно
нет). Без якоря агент не стартует — это защита от MITM на энролменте, не молчаливый
fallback на системный trust.
Не кладите заранее agent.crt/ключ подписи — их агент получает сам при энролменте;
руками — только agent.conf и ca.crt (либо отпечаток).
5.2 Установка вручную
# Debian/Ubuntu — из apt-репо контура:
apt-get install -y qf-agent=<версия>
# или файлом:
dpkg -i qf-agent_<версия>_amd64.deb
# RHEL/Alma/Rocky — из dnf-репо контура:
dnf install -y qf-agent-<версия>
# или файлом:
rpm -i qf-agent-<версия>-1.x86_64.rpm
# положить agent.conf (5.1) и ca.crt, затем:
systemctl enable --now qf-agent
systemctl status qf-agent
5.3 Раскатка на парк через Ansible (air-gap)
Идемпотентная роль устанавливает пакет, кладёт CA-анкер, пишет agent.conf, стартует
сервис. Для закрытого контура — источник repo (внутренний репозиторий) или file
(копия с control-ноды).
Пример group_vars / -e:
# источник пакета: внутренний репозиторий контура
qf_agent_source: repo
qf_agent_version: "<версия>"
# либо копировать локальный файл с control-ноды:
# qf_agent_source: file
# qf_agent_pkg_src_deb: files/qf-agent_<версия>_amd64.deb
# qf_agent_pkg_src_rpm: files/qf-agent-<версия>.x86_64.rpm
# подключение к CP
qf_endpoint: "qf-cp.corp.local:8443"
qf_enroll_token: "<bulk-токен>" # обычно per-group (web/db/mgmt)
# якорь доверия CA — один из вариантов:
qf_ca_src: "files/ca.crt" # роль скопирует PEM на хосты
# либо отпечаток вне канала (PEM не переносится):
# qf_enroll_ca_fingerprint: "<64-символьный sha256>"
Запуск роли по вашему инвентарю (стандартный ansible-playbook). Роль:
- проверяет ядро и архитектуру, ставит пакет из выбранного источника;
- копирует
ca.crt(если заданqf_ca_src) и пишет минимальныйagent.conf; - запускает systemd-юнит; если хост уже заенроллен (есть
agent.crt) — рендерит токен пустым, не перетирая уже стёртый токен (повторный прогон безопасен).
Разные роли парка — разные qf_enroll_token (через group_vars), чтобы лейблы и группы
проставлялись верно.
6. Проверка и приёмка
- На CP (UI/API): все хосты появились в списке, статус
active, лейблы соответствуют токену.GET /hostsпокажет парк;GET /hosts/{id}/ruleset— что реально применено. - На хосте:
systemctl is-active qf-agent=active; в логах агента — строка загрузки датапаса с версией ядра, вариантом матчера и способом attach (TCX/legacy). - Токены: после энролла в
agent.confзаенролленных хостовQF_ENROLL_TOKENпуст. - Связность: подключение к каналу управления (gRPC) не должно быть перекрыто вашими же политиками — canal защищён management-guard, но deny на другие сегменты проверяйте отдельно.
7. Типичные проблемы
| Симптом | Причина / решение |
|---|---|
| Агент падает при старте: нет якоря CA | Не задан QF_ENROLL_CA/_FINGERPRINT. Положите ca.crt или отпечаток (3.6) |
| Агент не подключается к CP | QF_CP_HOST/QF_CP_ENDPOINT в CP — localhost; поставьте внешний адрес. Проверьте, что gRPC/enroll-порты проброшены на L4 (не терминируйте mTLS) |
Вторая реплика CP в Pending | PKI-том ReadWriteOnce при 2 репликах. Нужен ReadWriteMany или replicaCount: 1 |
| JWT-ошибки/разлогин между репликами | Не задан общий QF_JWT_SECRET при replicaCount > 1 |
| Хост не получает правил | Лейблы хоста не матчат селекторы политик. Сверьте label_template токена и селекторы |
| Агент на ядре <6.6 рядом с Cilium не стартует | Ожидаемо (конфликт qdisc). Для coexist нужно ядро ≥6.6 (TCX) |
| Enrollment отклонён | Токен исчерпан (max_uses) или истёк (ttl). Выпустите новый |
Порядок действий кратко
- Занести в контур: образ CP → внутренний реестр; чарт; пакеты агента → внутренний репо/файлы.
- Подготовить PostgreSQL, секреты (
QF_DB_DSN,QF_MASTER_KEY,QF_JWT_SECRET, админ). helm upgrade --install qf-cpсvalues-airgap.yaml; проверить поды/порты/HTTPS.- Экспортировать якорь CA (PEM или отпечаток), разнести вне канала.
- Выпустить bulk-токен(ы) с
label_template; завести политики под эти лейблы. - Раскатать агентов (repo/file, Ansible), задав
QF_ENDPOINT+ токен + якорь CA. - Принять: хосты
active, лейблы и ruleset верны, токены на хостах стёрты.