Установка qf в закрытом контуре
Для установки без доступа в интернет заранее переносятся образ Control Plane, Helm-чарт и пакеты агента. Control Plane разворачивается в Kubernetes, а агенты подключаются к нему с заранее переданным якорем доверия CA.
В примерах используются адреса qf-cp.corp.local и registry.corp.local.
0. Архитектура и порты
Один deployment Control Plane обслуживает весь парк агентов. Агент держит с ним два канала и ходит в REST:
| Канал | Назначение | Порт контейнера | NodePort по умолчанию |
|---|---|---|---|
| gRPC с mTLS | правила и телеметрия | 8443 | 31443 |
| gRPC enrollment | первое подключение | 8444 | 31444 |
| REST/HTTPS | API и UI | 8080 за TLS-прокси | — |
Как агент вычисляет адреса. Агент получает один параметр QF_ENDPOINT и выводит из
него остальное:
QF_ENDPOINT=host→ gRPChost:31443, enrollmenthost:31444, RESThttps://host.QF_ENDPOINT=host:P→ gRPChost:P, enrollmenthost:P+1, RESThttps://host.
REST всегда использует https://<host> без явного порта, то есть порт 443.
Enrollment-порт должен следовать сразу за gRPC-портом. Для стандартных NodePort
достаточно QF_ENDPOINT=qf-cp.corp.local: агент выберет 31443 и 31444.
┌─────────────── Kubernetes ───────────────┐
агенты ──gRPC:31443(mTLS)─► Service (NodePort/LB) ──► qf-cp (2 реплики)
агенты ──enroll:31444─────► │
операторы ─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-кластер. При
replicaCount > 1нужны RWX-хранилище для PKI, общийQF_JWT_SECRETи общийQF_JWT_PRIVATE_KEY. - PostgreSQL внутри контура, доступный из кластера (строка подключения — в секрет CP).
- Внутренний CA или способ выпустить TLS-сертификат для REST/UI (HTTPS).
- Публикация agent-портов через
NodePort,LoadBalancerили L4-прокси. Канал gRPC с mTLS передаётся как TCP без завершения TLS на балансировщике.
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. Перенос артефактов
Артефакты загружаются в среде с интернетом, проверяются, переносятся разрешённым способом и публикуются во внутренних реестрах.
# на стороне с интернетом — сохранить образ
VERSION=0.25.6 # нужная версия релиза
podman pull "ghcr.io/qzmi4meister/qf-cp:${VERSION}"
podman save "ghcr.io/qzmi4meister/qf-cp:${VERSION}" -o "qf-cp-${VERSION}.tar"
# перенести tar, чарт и пакеты агента в контур, затем:
podman load -i "qf-cp-${VERSION}.tar"
podman tag "ghcr.io/qzmi4meister/qf-cp:${VERSION}" \
"registry.corp.local/qf-cp:${VERSION}"
podman push "registry.corp.local/qf-cp:${VERSION}"
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.keyиbundle-signing.keyв PKI-томе. Генерация:openssl rand -hex 32.QF_JWT_SECRET— не менее 32 байт для подписи refresh-токенов. Генерация:openssl rand -hex 32.QF_JWT_PRIVATE_KEY— общий RSA-ключ подписи access-токенов. Генерация:openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out jwt-private.pem.- Bootstrap-админ:
QF_ADMIN_USERNAME,QF_ADMIN_PASSWORD(и опц.QF_ADMIN_EMAIL).
Оба JWT-ключа обязательны при replicaCount > 1. Master key и JWT-ключи входят
в защищённую резервную копию; их потеря нарушает расшифровку PKI или действующие
сессии.
3.2 values для контура
Минимальный values-airgap.yaml (плейсхолдеры заменяются своими):
replicaCount: 2
image:
repository: registry.corp.local/qf-cp # внутренний реестр
tag: "<версия>"
pullPolicy: IfNotPresent
imagePullSecrets:
- name: corp-registry # если реестр требует авторизации
service:
type: ClusterIP # REST/UI публикуется через Ingress
httpPort: 8080
grpcPort: 8443
enrollPort: 8444
agentService:
enabled: true
type: NodePort # для внешнего VIP можно выбрать LoadBalancer
externalTrafficPolicy: Local
grpcNodePort: 31443
enrollNodePort: 31444
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-storage-class>"
secrets:
dbDSN: "postgres://qf:***@postgres.corp.local:5432/qf?sslmode=require"
masterKey: "<hex-32-байта>"
jwtSecret: "<hex-32-байта>"
# Передаётся в Helm через --set-file, см. команду ниже.
jwtPrivateKey: ""
adminUsername: "admin"
adminPassword: "<сложный-пароль>"
env:
QF_LOG_LEVEL: info
# ВНЕШНИЙ адрес, по которому агенты видят CP. КРИТИЧНО: не localhost.
QF_CP_HOST: qf-cp.corp.local
QF_CP_ENDPOINT: "qf-cp.corp.local:31444"
# если REST за реверс-прокси — указать CIDR прокси, чтобы верно видеть client IP:
# QF_TRUSTED_PROXIES: "10.42.0.0/16"
Важное:
QF_CP_HOSTиQF_CP_ENDPOINTсодержат внешний адрес Control Plane.accessMode: ReadWriteManyпри 2 репликах — иначе PKI-том привяжется к одной ноде, и вторая реплика зависнет вPending.- NodePort
31443и31444должны быть доступны агентам; REST публикуется через TLS-прокси на порту 443. - При
externalTrafficPolicy: Localвнешний L4-балансировщик направляет трафик только на ноды с готовыми pod Control Plane.
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
CHART_VERSION=0.25.6 # версия перенесённого чарта
# чарт из внутреннего OCI-реестра:
helm upgrade --install qf-cp oci://registry.corp.local/charts/qf-cp \
--version "$CHART_VERSION" -n qf -f values-airgap.yaml \
--set-file secrets.jwtPrivateKey=jwt-private.pem
# или из распакованного каталога:
# 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 # agent NodePort: 31443/31444
curl --cacert corp-root-ca.crt https://qf-cp.corp.local/healthz
После первого входа в https://qf-cp.corp.local учётные данные bootstrap-администратора
заменяются постоянными.
3.6 Экспорт якоря доверия CA (для энролмента агентов)
Агентам нужен якорь доверия к внутреннему CA Control Plane. Сертификат или его отпечаток получают в доверенной административной среде:
# REST-сертификат проверяется внутренним корпоративным CA.
curl --cacert corp-root-ca.crt \
https://qf-cp.corp.local/pki/ca.crt -o ca.crt
# Отпечаток для варианта без переноса PEM.
openssl x509 -in ca.crt -outform DER \
| sha256sum \
| awk '{print $1}'
ca.crt или отпечаток передаётся агентам через отдельный доверенный канал. Получение
якоря через тот же непроверенный endpoint не защищает первое подключение от подмены.
4. Подготовка энролмента
4.1 Bulk-токен с лейблами (массовый ввод)
Для парка используется bulk-токен: один токен подключает несколько хостов и назначает им заданные лейблы. Токен создаётся в UI или через REST:
ADMIN_TOKEN=PASTE_ADMIN_API_TOKEN_HERE
curl --cacert corp-root-ca.crt -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" }
}'
# → в ответе поле token
label_template— лейблы для каждого хоста, подключённого по токену. Селекторы политик используют эти лейблы при выборе хостов.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 с правами 0600.
5.1 Минимальный /etc/qf/agent.conf
QF_ENDPOINT=qf-cp.corp.local # NodePort 31443/31444, REST 443
QF_ENROLL_TOKEN=PASTE_BULK_TOKEN_HERE # стирается агентом после успешного энролла
QF_ENROLL_CA=/etc/qf/ca.crt # якорь доверия (см. 3.6) — ОБЯЗАТЕЛЕН
# альтернатива переносу PEM — pin по отпечатку (CA тянется по REST, принимается по хэшу):
# QF_ENROLL_CA_FINGERPRINT=PASTE_SHA256_HEX_HERE
# опционально:
# QF_IFACE=eth0 # по умолчанию — интерфейс дефолтного маршрута
# QF_PKI_DIR=/etc/qf # где агент хранит cert/key
# QF_MASK_MAC=true # приватность: хранить только OUI MAC
Приоритет способов доверия: QF_ENROLL_CA → сохранённый ca.crt →
QF_ENROLL_CA_FINGERPRINT → QF_ENROLL_CA_FETCH=true. Последний вариант подходит
только для REST API с сертификатом от доверенного системного CA. Без одного из этих
способов агент отказывается от первого подключения.
Заранее не кладутся agent.crt/ключ подписи — их агент получает сам при энролменте;
руками — только agent.conf и ca.crt (либо отпечаток).
5.2 Установка вручную
VERSION=0.25.6 # версия перенесённого пакета
# Debian/Ubuntu — из apt-репо контура:
apt-get install -y "qf-agent=${VERSION}"
# или файлом:
dpkg -i "qf-agent_${VERSION}_amd64.deb"
# RHEL/Alma/Rocky — из dnf-репо контура:
dnf install -y "qf-agent-${VERSION}"
# или файлом:
rpm -i "qf-agent-${VERSION}-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-<версия>-1.x86_64.rpm
# подключение к CP
qf_endpoint: "qf-cp.corp.local"
qf_enroll_token: "<bulk-токен>" # обычно per-group (web/db/mgmt)
qf_drop_ipv6: true # сохранить безопасное значение агента
# якорь доверия CA — один из вариантов:
qf_ca_src: "files/ca.crt" # роль скопирует PEM на хосты
# либо отпечаток вне канала (PEM не переносится):
# qf_enroll_ca_fingerprint: "<64-символьный sha256>"
Роль запускается стандартной командой ansible-playbook для нужного inventory. Она:
- проверяет архитектуру, ставит пакет из выбранного источника;
- копирует
ca.crt(если заданqf_ca_src) и пишет минимальныйagent.conf; - запускает systemd-юнит; если хост уже заенроллен (есть
agent.crt) — рендерит токен пустым, не перетирая уже стёртый токен (повторный прогон безопасен).
Разные роли парка — разные qf_enroll_token (через group_vars), чтобы лейблы и группы
проставлялись верно.
Ansible-роль и агент используют один безопасный default: QF_DROP_IPV6=true.
Перед обновлением роли проверьте текущий /etc/qf/agent.conf: если в нём уже стоит
QF_DROP_IPV6=false и IPv6-энфорс нужно сохранить, явно задайте qf_drop_ipv6: false
в inventory до раскатки. Та же явная настройка вернёт прежнее поведение, если роль уже
применена. Значение false подходит только после добавления разрешений для ICMPv6 ND/RA.
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пуст. - Связность: при старте агент разрешает исходящий TCP к IPv4-адресам gRPC- и
enrollment-endpoint. Политика не может перекрыть эти правила, но смена адреса в
DNS требует перезапуска агента.
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 и QF_JWT_PRIVATE_KEY при replicaCount > 1 |
| Хост не получает правил | Лейблы хоста не матчат селекторы политик. Сверяются label_template токена и селекторы |
| Агент на ядре <6.6 рядом с Cilium не стартует | Ожидаемо (конфликт qdisc). Для coexist нужно ядро ≥6.6 (TCX) |
| Enrollment отклонён | Токен исчерпан (max_uses) или истёк (ttl). Нужен новый |
Порядок действий кратко
- Занести в контур: образ CP → внутренний реестр; чарт; пакеты агента → внутренний репо/файлы.
- Подготовить PostgreSQL и секреты:
QF_DB_DSN,QF_MASTER_KEY, оба JWT-ключа и учётные данные администратора. helm upgrade --install qf-cpсvalues-airgap.yaml; проверить поды/порты/HTTPS.- Экспортировать якорь CA (PEM или отпечаток), разнести вне канала.
- Выпустить bulk-токен(ы) с
label_template; завести политики под эти лейблы. - Раскатать агентов (repo/file, Ansible), задав
QF_ENDPOINT+ токен + якорь CA. - Принять: хосты
active, лейблы и ruleset верны, токены на хостах стёрты.