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

Установка 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/HTTPSAPI + UI (для операторов и Terraform)8080 за TLS-прокси

Как агент вычисляет адреса. Агент получает один параметр QF_ENDPOINT и выводит из него остальное:

  • QF_ENDPOINT=host (без порта) → gRPC host:31443, enroll host:31444, REST https://host.
  • QF_ENDPOINT=host:P (с портом) → gRPC host:P, enroll host:P+1, REST https://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)
Агент не подключается к CPQF_CP_HOST/QF_CP_ENDPOINT в CP — localhost; поставьте внешний адрес. Проверьте, что gRPC/enroll-порты проброшены на L4 (не терминируйте mTLS)
Вторая реплика CP в PendingPKI-том 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). Выпустите новый

Порядок действий кратко

  1. Занести в контур: образ CP → внутренний реестр; чарт; пакеты агента → внутренний репо/файлы.
  2. Подготовить PostgreSQL, секреты (QF_DB_DSN, QF_MASTER_KEY, QF_JWT_SECRET, админ).
  3. helm upgrade --install qf-cp с values-airgap.yaml; проверить поды/порты/HTTPS.
  4. Экспортировать якорь CA (PEM или отпечаток), разнести вне канала.
  5. Выпустить bulk-токен(ы) с label_template; завести политики под эти лейблы.
  6. Раскатать агентов (repo/file, Ansible), задав QF_ENDPOINT + токен + якорь CA.
  7. Принять: хосты active, лейблы и ruleset верны, токены на хостах стёрты.