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

Установка qf в закрытом контуре

Для установки без доступа в интернет заранее переносятся образ Control Plane, Helm-чарт и пакеты агента. Control Plane разворачивается в Kubernetes, а агенты подключаются к нему с заранее переданным якорем доверия CA.

В примерах используются адреса qf-cp.corp.local и registry.corp.local.


0. Архитектура и порты

Один deployment Control Plane обслуживает весь парк агентов. Агент держит с ним два канала и ходит в REST:

КаналНазначениеПорт контейнераNodePort по умолчанию
gRPC с mTLSправила и телеметрия844331443
gRPC enrollmentпервое подключение844431444
REST/HTTPSAPI и UI8080 за TLS-прокси

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

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

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

  1. Занести в контур: образ CP → внутренний реестр; чарт; пакеты агента → внутренний репо/файлы.
  2. Подготовить PostgreSQL и секреты: QF_DB_DSN, QF_MASTER_KEY, оба JWT-ключа и учётные данные администратора.
  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 верны, токены на хостах стёрты.