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

qf — эксплуатационный runbook

Операционные процедуры для запуска qf в проде.


Содержание

  1. Бэкап и восстановление
  2. Ротация секретов
  3. Управление сертификатами
  4. Обслуживание PostgreSQL
  5. Масштабирование qf-cp
  6. Debug-чеклист
  7. Управление политиками
  8. Известные ограничения

1. Бэкап и восстановление

1.1 Бэкап PKI-каталога

PKI-каталог (QF_PKI_DIR, дефолт /etc/qf/pki) содержит:

ФайлОписание
ca.crtСертификат CA (публичный, можно распространять)
ca.keyПриватный ключ CA (AES-256-GCM, зашифрован QF_MASTER_KEY)
server.crt / server.keyTLS-сертификат и ключ сервера CP
bundle-signing.key / bundle-signing.pubПара ключей Ed25519 для подписи бандлов
# Бэкап
PKI_DIR=/etc/qf/pki
BACKUP_FILE="qf-pki-$(date +%Y%m%d-%H%M%S).tar.gz"

tar -czf "$BACKUP_FILE" -C "$(dirname $PKI_DIR)" "$(basename $PKI_DIR)"
chmod 600 "$BACKUP_FILE"

# Хранить бэкап в защищённом месте (шифрование at-rest)
# Приватный ключ CA уже зашифрован QF_MASTER_KEY, но бэкап всё равно хранится безопасно

Важно: PKI всегда бэкапится вместе с QF_MASTER_KEY. Бэкап PKI без соответствующего мастер-ключа бесполезен.

1.2 Восстановление PKI-каталога

BACKUP_FILE="qf-pki-20260101-120000.tar.gz"
TARGET_DIR=/etc/qf

tar -xzf "$BACKUP_FILE" -C "$TARGET_DIR"
chown -R root:root "$TARGET_DIR/pki"
chmod 700 "$TARGET_DIR/pki"
chmod 600 "$TARGET_DIR/pki"/*.key

# Перезапустить CP с тем же QF_MASTER_KEY, что был при снятии бэкапа
systemctl restart qf-cp

1.3 Бэкап PostgreSQL

# Полный дамп
pg_dump -h localhost -U qf -d qf -F c -f "qf-db-$(date +%Y%m%d-%H%M%S).pgdump"

# Восстановление
pg_restore -h localhost -U qf -d qf --clean "qf-db-20260101-120000.pgdump"

2. Ротация секретов

2.1 Ротация QF_MASTER_KEY

QF_MASTER_KEY шифрует приватный ключ CA at-rest (файл ca.key, AES-256-GCM). Ротация требует перешифровки ключа CA.

Процедура:

  1. Сгенерировать новый мастер-ключ:

    NEW_KEY=$(openssl rand -hex 32)
    echo "New key: $NEW_KEY" # сохранить безопасно до продолжения
  2. Остановить qf-cp (чтобы не читать ключ CA в процессе ротации):

    systemctl stop qf-cp
    # Или: kubectl -n qf scale deployment qf-cp --replicas=0
  3. Перешифровать ключ CA новым мастер-ключом Go-тулой (собрать из исходников):

    # Расшифровать старым ключом, перешифровать новым
    go run ./cp/cmd/rekey/ \
    --pki-dir /etc/qf/pki \
    --old-key "$OLD_MASTER_KEY" \
    --new-key "$NEW_KEY"

    Замечание: тула rekey перечитывает ca.key, расшифровывает старым ключом, перешифровывает новым, атомарно пишет обратно.

  4. Обновить секрет и перезапустить:

    # Systemd: править /etc/qf/cp.env или environment-файл
    sed -i "s/QF_MASTER_KEY=.*/QF_MASTER_KEY=$NEW_KEY/" /etc/qf/cp.env
    systemctl start qf-cp

    # Kubernetes: обновить Secret
    kubectl -n qf create secret generic qf-cp-secrets \
    --from-literal=QF_MASTER_KEY="$NEW_KEY" \
    --from-literal=QF_DB_DSN="..." \
    --dry-run=client -o yaml | kubectl apply -f -
    kubectl -n qf rollout restart deployment/qf-cp
  5. Проверить старт:

    journalctl -u qf-cp -n 20
    # Искать: "PKI initialized" без ошибок

Эффект: ноль простоя агентов. Агенты работают по mTLS с уже выпущенными сертами; ключ CA нужен только для подписи новых сертов (энролмент). Энролмент кратко недоступен на время перезапуска (~секунды).

2.2 Ротация QF_JWT_SECRET

JWT-секрет подписывает access- и refresh-токены (HMAC-SHA256). Ротация инвалидирует все активные сессии — пользователям придётся войти заново.

NEW_JWT=$(openssl rand -hex 32)

# Обновить и перезапустить
sed -i "s/QF_JWT_SECRET=.*/QF_JWT_SECRET=$NEW_JWT/" /etc/qf/cp.env
systemctl restart qf-cp

# Kubernetes
kubectl -n qf patch secret qf-cp-secrets \
--patch="{\"stringData\":{\"QF_JWT_SECRET\":\"$NEW_JWT\"}}"
kubectl -n qf rollout restart deployment/qf-cp

Эффект: все браузерные сессии и API-клиенты на JWT Bearer-токенах инвалидируются немедленно. API-токены (opaque-токены в БД) не затронуты.

2.3 Ротация ключа подписи бандлов

Ключ подписи бандлов (Ed25519) подписывает бандлы политик, доставляемые агентам. Ротация требует, чтобы агенты получили новый публичный ключ до вывода старого.

Процедура:

  1. Сгенерировать новую пару ключей:

    openssl genpkey -algorithm ed25519 -out /etc/qf/pki/bundle-signing-new.key
    openssl pkey -in /etc/qf/pki/bundle-signing-new.key -pubout -out /etc/qf/pki/bundle-signing-new.pub
  2. Настроить CP на двойную подпись обоими ключами (dual-sign в CP не реализован — обходной путь: rolling-restart с новым ключом, агенты переэнроллятся за обновлённым bundle-signing.pub).

  3. Заменить файлы ключей атомарно:

    mv /etc/qf/pki/bundle-signing.key /etc/qf/pki/bundle-signing-old.key
    mv /etc/qf/pki/bundle-signing-new.key /etc/qf/pki/bundle-signing.key
    mv /etc/qf/pki/bundle-signing-new.pub /etc/qf/pki/bundle-signing.pub
  4. Перезапустить CP. Новые бандлы подпишутся новым ключом.

  5. Переэнроллить агентов (они получают обновлённый bundle-signing.pub при энролменте):

    # На каждом хосте агента: удалить старый PKI-материал и перезапустить
    rm /etc/qf/agent.crt /etc/qf/agent.key /etc/qf/ca.crt /etc/qf/bundle-signing.pub
    # Задать enrollment-токен в /etc/qf/agent.conf
    systemctl restart qf-agent

3. Управление сертификатами

3.1 Проверить срок сертификата

Сертификаты агентов выпускаются при энролменте. Срок действия по умолчанию: 90 дней.

# Проверить серт конкретного агента
openssl x509 -in /etc/qf/agent.crt -noout -dates -subject

# Список всех сертов и их сроков в БД
psql "$QF_DB_DSN" -c "
SELECT h.hostname, c.serial, c.not_after, c.status
FROM certificates c
JOIN hosts h ON h.id = c.host_id
WHERE c.status = 'active'
ORDER BY c.not_after ASC;"

3.2 Переэнролмент агента (серт истёк или отозван)

Когда серт агента истекает или отзывается, CP отвергает его mTLS-соединение. Агент уходит в degraded и должен переэнроллиться.

На хосте агента:

# 1. Создать новый enrollment-токен через CP API (или UI)
TOKEN=$(curl -s -X POST http://cp.example.com:8080/tokens \
-H "Authorization: Bearer $JWT" \
-H "X-Tenant-ID: $TENANT_ID" \
-H 'Content-Type: application/json' \
-d '{"label_template":{},"ttl_seconds":3600,"max_uses":1}' | jq -r .token)

# 2. На агенте: удалить старый серт-материал и задать токен
rm /etc/qf/agent.crt /etc/qf/agent.key
echo "QF_ENROLL_TOKEN=$TOKEN" >> /etc/qf/agent.conf

# 3. Перезапустить агента — он переэнроллится и получит новый серт
systemctl restart qf-agent

После переэнролмента убрать QF_ENROLL_TOKEN из agent.conf (агент стирает его сам при успехе, но это стоит проверить):

grep QF_ENROLL_TOKEN /etc/qf/agent.conf # должно быть пусто или отсутствовать

3.3 Отозвать сертификат

# Через API: пометить хост как revoked
curl -s -X PATCH http://cp.example.com:8080/hosts/$HOST_ID \
-H "Authorization: Bearer $JWT" \
-H "X-Tenant-ID: $TENANT_ID" \
-H 'Content-Type: application/json' \
-d '{"status":"revoked"}'

# В БД (напрямую)
psql "$QF_DB_DSN" -c "
UPDATE certificates SET status='revoked', revoked_at=NOW()
WHERE host_id='<host-uuid>' AND status='active';"

Блоклист отзыва перечитывается CP автоматически каждые 5 минут; перезапуск не нужен.

3.4 Ротация TLS-серта сервера CP

TLS-серт сервера CP (server.crt/server.key) защищает HTTPS- и gRPC-соединения.

# Остановить CP, сгенерировать новый серт, подписанный существующим CA
systemctl stop qf-cp

# Серт регенерируется автоматически через LoadOrInit, если отсутствует
mv /etc/qf/pki/server.crt /etc/qf/pki/server.crt.bak
mv /etc/qf/pki/server.key /etc/qf/pki/server.key.bak

systemctl start qf-cp
# CP регенерирует server.crt/server.key при старте, если файлы отсутствуют

4. Обслуживание PostgreSQL

4.1 Управление партициями

Партициями управляет встроенный PartitionManager (работает в CP каждые 24 часа). Ретенция по умолчанию:

ТаблицаТип партицииРетенция
log_eventsдневная7 дней
flow_eventsдневная14 дней
counter_snapshotsнедельная30 дней
system_eventsбез партиций (DELETE)30 дней

PartitionManager создаёт на каждом цикле 7 дней будущих дневных партиций и 2 недели будущих недельных.

Вручную расширить партиции:

psql "$QF_DB_DSN" <<'SQL'
-- Создать партицию на следующий день вручную
DO $$
DECLARE
d date := CURRENT_DATE + 8; -- на день дальше обычного lookahead
BEGIN
EXECUTE format(
'CREATE TABLE IF NOT EXISTS log_events_%s PARTITION OF log_events
FOR VALUES FROM (''%s'') TO (''%s'')',
to_char(d, 'YYYY_MM_DD'), d, d + 1
);
END $$;
SQL

Изменить ретенцию: TTL-константы захардкожены в cp/internal/ingest/partitions.go. Чтобы изменить — правка и пересборка qf-cp:

const (
ttlLog = 14 * 24 * time.Hour // дефолт 7 дней
ttlFlow = 30 * 24 * time.Hour // дефолт 14 дней
ttlCounter = 90 * 24 * time.Hour // дефолт 30 дней
)

4.2 Мониторинг здоровья партиций

# Список всех партиций со счётчиком строк
psql "$QF_DB_DSN" -c "
SELECT
c.relname AS partition,
pg_size_pretty(pg_relation_size(c.oid)) AS size,
pg_stat_get_live_tuples(c.oid) AS live_rows
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname IN ('log_events','flow_events','counter_snapshots')
ORDER BY c.relname;"

4.3 Тюнинг PostgreSQL

Для high-throughput-деплоев:

-- В postgresql.conf
shared_buffers = 256MB -- 25% RAM
work_mem = 16MB -- под сложные запросы
maintenance_work_mem = 128MB -- под VACUUM / сборку индексов
max_connections = 100 -- qf-cp использует pgxpool
wal_level = replica -- при streaming-репликации
checkpoint_completion_target = 0.9
random_page_cost = 1.1 -- под SSD

Гонять VACUUM ANALYZE еженедельно на write-heavy таблицах:

psql "$QF_DB_DSN" -c "VACUUM ANALYZE log_events, flow_events, system_events;"

5. Масштабирование qf-cp

qf-cp stateless (состояние в PostgreSQL + PKI-каталог). Несколько реплик могут работать одновременно.

5.1 Горизонтальное масштабирование (Kubernetes)

kubectl -n qf scale deployment qf-cp --replicas=3

Требования к multi-replica-схеме:

  • Общий PKI-том: все реплики монтируют один QF_PKI_DIR (ReadWriteMany PVC либо преднастроенный NFS/CephFS). Ключ CA read-only после первичной генерации.
  • Одинаковый QF_MASTER_KEY: должен быть идентичен на всех репликах.
  • Одинаковый QF_JWT_SECRET: должен быть идентичен, чтобы валидация JWT работала между репликами.
  • Балансировщик: распределяет gRPC (8443) и enrollment (8444) по репликам; агенты переподключаются автоматически при обрыве.

Кросс-под доставка идёт через PG LISTEN/NOTIFY (pgbus) — Redis не нужен. UI-invalidate (qf_invalidate), SSE live-tail (qf_events, coalesced + gated) и push бандлов (qf_dispatch) достигают агентов/UI-клиентов, подключённых к любой реплике. Наблюдаемые метрики — qf_pgbus_notify_total / qf_pgbus_notify_suppressed_total; путь notify сериализуется на инстанс-wide PG-локе, поэтому gate держит notify на 0, когда ни один другой под не наблюдает хост.

# override values.yaml для 3 реплик
replicaCount: 3
pki:
persistentVolume:
storageClass: nfs-client # должен поддерживать ReadWriteMany

5.2 Вертикальное масштабирование

qf-cp CPU-bound на компиляции бандлов (компиляция правил политик). Если латентность BundleApplied высока:

  • Поднять CPU-лимиты CP (Helm: resources.limits.cpu)
  • Увеличить размер пула соединений PostgreSQL: размер пула задаётся в коде (newPool в cp/cmd/qf-cp/main.go, три пула) — пересобрать, чтобы изменить

5.3 Мониторинг

qf-cp отдаёт Prometheus-метрики на GET /metrics (promhttp-хендлер; исключён из access-логов вместе с /healthz и /version). Grafana-дашборд: monitoring/grafana/qf-cp.json — импорт через Dashboards → Import → Upload JSON.

Рекомендуемые пороги алертов:

МетрикаУсловиеSeverity
up (scrape)== 0 дольше 1 минcritical
qf_grpc_active_streamsпадение >50% за 5 минwarning
qf_bundle_push_errors_total rate> 0.1/с дольше 2 минwarning
qf_bundle_push_duration_seconds p99> 5сwarning
qf_ingester_events_dropped_total rate> 0 дольше 1 минwarning
qf_http_requests_total{status=~"5.."} rate> 0.5/с дольше 2 минcritical
qf_auth_failures_total rate> 5/с дольше 5 минwarning
qf_db_pool_acquired_conns / (qf_db_pool_acquired_conns + qf_db_pool_idle_conns)> 0.9warning
qf_pgbus_notify_total{channel="qf_events"} rateрастёт без кросс-под SSE-зрителейwarning (subreg-gate нездоров)

6. Debug-чеклист

Хост завис в enrolling

# 1. Логи агента
journalctl -u qf-agent -n 50 --no-pager

# 2. Сеть: порт энролмента доступен с агента
nc -zv <cp-host> 8444

# 3. Токен не истёк
curl -s http://cp.example.com:8080/tokens \
-H "Authorization: Bearer $JWT" -H "X-Tenant-ID: $TENANT_ID" | jq '.[] | {id, expires_at, uses_count, max_uses}'

# 4. Mismatch TLS SAN: QF_ENDPOINT на агенте должен резолвиться на тот же host, что QF_CP_HOST на CP
# Агент коннектится на cp.example.com:8444, но SAN серта — для localhost → handshake падает
grep QF_ENDPOINT /etc/qf/agent.conf
# Должен совпадать с hostname в env-переменной QF_CP_HOST на CP

# 5. Фаервол: проверить, что порт 8444 открыт на хосте CP
ss -tlnp | grep 8444

Бандл не приходит на агента

# 1. gRPC агента подключён?
journalctl -u qf-agent | grep -E "connected|bundle|stream"

# 2. Серт агента валиден
openssl x509 -in /etc/qf/agent.crt -noout -dates
# если истёк → переэнроллить (см. раздел 3.2)

# 3. Логи пушера CP
# На хосте или поде CP
journalctl -u qf-cp | grep -E "bundle|push|stream" | tail -20
# kubectl -n qf logs deployment/qf-cp | grep bundle

# 4. Статус хоста в БД
psql "$QF_DB_DSN" -c "
SELECT h.id, h.hostname, s.status
FROM hosts h JOIN host_statuses s ON s.host_id = h.id
WHERE h.hostname='<hostname>';"
# Если status='stale' или 'needs_rebootstrap' → переэнроллить

# 5. Политика существует и компилируется
curl -s http://cp.example.com:8080/policies \
-H "Authorization: Bearer $JWT" -H "X-Tenant-ID: $TENANT_ID" | jq '.[] | {id, name}'

События не появляются в UI

# 1. Статус агента active?
curl -s http://cp.example.com:8080/hosts \
-H "Authorization: Bearer $JWT" -H "X-Tenant-ID: $TENANT_ID" | jq '.[] | {hostname, status}'

# 2. Агент шлёт события?
journalctl -u qf-agent | grep -E "event|batch|ingest"

# 3. Логи ingest на CP
journalctl -u qf-cp | grep -E "ingest|log_event|insert" | tail -20

# 4. PostgreSQL: строки в сегодняшней партиции
psql "$QF_DB_DSN" -c "
SELECT COUNT(*) FROM log_events
WHERE created_at >= CURRENT_DATE AND host_id='<host-uuid>';"

# 5. Сегодняшняя партиция существует
psql "$QF_DB_DSN" -c "
SELECT relname FROM pg_class
WHERE relname = 'log_events_' || to_char(CURRENT_DATE, 'YYYY_MM_DD');"
# Если отсутствует: PartitionManager ещё не отработал; см. раздел 4.1 для ручного создания

TLS / mTLS-ошибки

ОшибкаВероятная причинаФикс
certificate signed by unknown authorityУ агента неверный ca.crtПереэнроллить агента
certificate has expiredСерт агента истёкПереэнроллить агента (раздел 3.2)
connection refused :8443gRPC CP не слушаетПроверить старт CP, биндинг порта
remote error: tls: certificate requiredАгент не прислал сертПроверить, что agent.crt/agent.key есть в QF_PKI_DIR
no such hostDNS эндпоинта CP не резолвитсяПроверить QF_ENDPOINT в agent.conf

CP не стартует

journalctl -u qf-cp -n 30 --no-pager

Частые ошибки:

СообщениеПричинаФикс
QF_MASTER_KEY must be 32-byte hexКлюч отсутствует или неверной длиныЗадать корректный 64-символьный hex-ключ
migrations: ...БД недоступна или нет правПроверить QF_DB_DSN, что PostgreSQL поднят
listen ...: address already in useКонфликт портаПроверить ss -tlnp, поправить QF_HTTP_ADDR / QF_GRPC_ADDR
PKI initialized нет в логахИнициализация CA упалаПроверить, что QF_PKI_DIR доступен на запись, есть место на диске

Высокие память или CPU на CP

# Профилировать CP через pprof (если открыт)
curl http://cp.example.com:8080/debug/pprof/goroutine?debug=1

# Проверить число горутин (должно быть стабильно под нагрузкой)
# Проверить сатурацию пула соединений БД: искать "acquire timeout" в логах

Деплой из приватного GitHub-репозитория

Образ контейнера и Helm-чарт хостятся на ghcr.io. С приватным репо они требуют аутентификации.

Однократная настройка

1. Helm registry login (один раз на машину, креды кешируются в ~/.config/helm/registry/config.json):

helm registry login ghcr.io -u qzmi4meister -p <GITHUB_PAT>

Нужные scope PAT: repo + read:packages.

2. K8s imagePullSecret (уже создан в namespace qf; пересоздать при истечении):

kubectl create secret docker-registry ghcr-secret \
--docker-server=ghcr.io \
--docker-username=qzmi4meister \
--docker-password=<GITHUB_PAT> \
--docker-email=qzmi4meister2@gmail.com \
-n qf --dry-run=client -o yaml | kubectl apply -f -

Деплой агента с приватными релизами

Передать PAT через env перед запуском Ansible:

GITHUB_TOKEN=<GITHUB_PAT> make deploy-agent

Либо override на прогон:

make deploy-agent qf_github_token=<GITHUB_PAT>

7. Управление политиками

Политики применяются к хостам по label-селектору (matchLabels). Политика активна на хосте, когда все пары key-value селектора присутствуют в эффективных лейблах хоста (собственные лейблы + унаследованные лейблы групп).

7.1 Назначить политику хостам

Открыть Policies → [имя политики] → вкладка Settings и нажать «Assign to hosts».

Два режима назначения:

РежимКнопкаЭффект
Один хостВыбрать хост → AssignДобавляет лейбл host-id: <id> на хост и в селектор политики
Host-группаВыбрать группу → AssignКопирует matchLabels группы в селектор политики

«Preview impact» до сохранения показывает затронутые хосты и diff ruleset.

7.2 Снять политику с хостов или host-групп

Открыть Policies → [имя политики] → вкладка Settings и нажать «Remove from hosts».

Два режима снятия:

РежимКнопкаЭффект
Один хостВыбрать матченный хост → RemoveУбирает матчащие лейблы политики из собственных лейблов хоста
Host-группаВыбрать матченную группу → RemoveУбирает ключи matchLabels группы из селектора политики

Guard — хост матчится через группу: Если выбранный хост матчится потому, что состоит в host-группе, чьи лейблы в селекторе политики, прямое снятие с хоста заблокировано. UI покажет:

«Matched through group 'X' — remove host from the group or remove the group below.»

В этом случае либо:

  • Снять группу с политики (убирает всех членов группы разом), либо
  • Убрать хост из группы (через страницу Host Groups или Host Detail → вкладка Groups)

7.3 Посмотреть политики, применённые к хосту

Открыть Hosts → [hostname] → вкладка Policies.

  • Показывает все политики, матчащие этот хост (выводится из эффективного ruleset).
  • Поле поиска фильтрует по имени политики или лейблу селектора (key=value).
  • Клик по имени политики → переход в редактор политики.

7.4 Проверить назначение через API

# Список политик, матчащих хост (через эффективный ruleset)
curl -s https://<cp-host>/hosts/<host-id>/ruleset \
-H "Authorization: Bearer $JWT" \
| jq '[.rules[].policy_name] | unique'

# Посмотреть селектор политики
curl -s https://<cp-host>/policies/<policy-id> \
-H "Authorization: Bearer $JWT" \
| jq '.selector'

# Список лейблов хоста
curl -s https://<cp-host>/hosts/<host-id> \
-H "Authorization: Bearer $JWT" \
| jq '{labels, effective_labels}'

8. Известные ограничения

Задокументированные ограничения датапаса/CP.

matcher.h возвращает 0 для состояния CT_RELATED (conntrack-FSM не отслеживает связанные флоу, например ICMP-ошибки для TCP/UDP-сессий). Правила с related в match-спеке отвергаются API (HTTP 422). Эффект: ICMP unreachable для активных сессий не пропускаются автоматически — при нужде заводятся явные ICMP allow-правила. Для PMTUD при default-deny явно разрешается ICMPv4 type 3 code 4 / ICMPv6 type 2 (packet-too-big).

8.2 IPv6: полный энфорс (opt-in), ext-заголовки обходятся

IPv6 энфорсится наравне с IPv4 — CIDR, ipset и stateful conntrack матчат v6 (Phase 25) — при QF_DROP_IPV6=false; по умолчанию весь v6 режется целиком (opt-in гейт). При включённом v6 правила proto/port/TCP-flags/CIDR/ipset/conntrack применяются к IPv6, включая пакеты с extension-заголовками: парсер обходит цепочку extension-заголовков (Hop-by-Hop, Routing, Destination Options, AH, Mobility; до IPV6_EXT_MAX_HOPS = 6 заголовков) до реального L4-заголовка. ⚠️ На dual-stack/Cilium-нодах перед включением разрешить ICMPv6 ND/RA, иначе нода потеряет связность.

⚠️ Радиус. Дефолтный drop отбрасывает и служебный v6 (ICMPv6 Neighbor Discovery, link-local fe80::/10). Dual-stack хост с v6-mgmt потеряет v6-связность. Нужен v6 → QF_DROP_IPV6=false.

Не вычисляются (проходят без rule-eval): фрагменты (fragment-заголовки IPv4 и IPv6 устроены симметрично), цепочки длиннее 6 extension-заголовков (патологические; fail-open) и неподдерживаемые L4-протоколы (см. 8.4).

8.3 Эфемерный JWT-секрет при рестарте

Когда QF_JWT_SECRET не задан, CP генерирует случайный секрет при старте. Все активные сессии инвалидируются при рестарте. В multi-replica-деплоях каждый под использует свой секрет — кросс-под аутентификация падает. Всегда задавайте QF_JWT_SECRET стабильным значением ≥32 байт в проде. Helm-чарт отказывается деплоиться при replicaCount > 1 и пустом secrets.jwtSecret.

8.4 Неподдержанные L4 (SCTP/GRE/ESP) по умолчанию проходят без энфорсмента

Датапас парсит и энфорсит только TCP / UDP / ICMP / ICMPv6. Любой другой IP-протокол — SCTP (132), GRE (47), ESP (50, IPsec), OSPF (89) и прочие — парсер не распознаёт: пакет попадает в ветку PARSE_SKIP и пропускается безусловно, не доходя до вычисления default-action. То есть default-deny на такой трафик не распространяется, а сам трафик не виден в Verdicts/Flows/Counters (вся телеметрия — ниже rule-eval). Правило по этим протоколам завести нельзя (их нет в enum протоколов; полноценная поддержка SCTP — отдельная задача B-26).

Включение fail-closed — agent-флаг QF_DENY_UNKNOWN_PROTO=true (дефолт false). При true IP-пакет с непарсимым L4 подчиняется per-direction default-action: дропается при default = DENY, проходит при ALLOW. Как и QF_DROP_IPV6, флаг agent-local и принудительно проставляется при каждой записи конфига — CP-бандл его не переопределяет. Фрагменты, truncated-пакеты и non-IP (ARP/LLDP) остаются fail-open даже при включённом флаге.

⚠️ Раскатывать осторожно. Fail-closed может заблокировать легитимный трафик: ESP = IPsec (вплоть до management-канала/VPN), GRE = VPN-туннели, SCTP = телеком-сигнализация. Включать только после проверки — вне qf, этот трафик в телеметрии не виден, — что таких протоколов на хосте нет.

Что мониторить: непарсимый L4 не считается счётчиками, поэтому связность до и после включения флага проверяется вне qf (ss -a, flow-логи вышестоящей сети, health приложений). Отдельный aggregate drop-счётчик для unknown-proto запланирован, но пока не отдаётся.

8.5 Fail-open окно ~6с после рестарта агента

После systemctl restart qf-agent (рестарт/краш/деплой) датапас загружается с чистого состояния: правил нет, конфиг по умолчанию. CP-политики переприменяются только через ~6с (reconnect → welcome → apply). В этом окне активен лишь mgmt-guard (allow-egress к CP), остальное идёт по default-ALLOWdeny-правила ~6с не энфорсятся (fail-open). Правила не теряются — восстанавливаются на ~t+6s.

Побочно: CP помечает хост converged (по generation из reconnect-handshake) сразу, до того как датапас применил правила — поэтому converged/Active в первые ~6с после рестарта не гарантируют энфорс.

Влияние: короткое окно на каждый рестарт; для хостов, полагающихся на deny-правила, — секунды обхода при рестарте агента. Митигация: QF_FAIL_CLOSED=true (deny-all до первого bundle) для критичных хостов, ценой связности в окне. Отслеживается в бэклоге (P24-AGENT-02, низкий приоритет).

8.6 Асимметрия по IP-версии

На IPv4 брешь неподдержанного L4 (8.4) — основной радиус. На IPv6 такой трафик уже режется дефолтным QF_DROP_IPV6=true (8.2); брешь возникает только если оператор снял v6-drop.