Эксплуатация¶
Резервное копирование¶
Что бэкапить¶
| Что | Где | Критичность |
|---|---|---|
| PostgreSQL (Hub) | volume postgres_data |
CRITICAL — основная БД |
| PostgreSQL (DomainScope) | свой volume | HIGH — теряем discovery |
./storage/ (артефакты, отчёты) |
bind mount | MEDIUM — можно перезалить |
| Helm secrets / Vault | внешнее хранилище | CRITICAL — без них Hub не стартует |
Конфиги (.env, values.yaml) |
git | LOW — версионируются |
Ежедневный бэкап БД¶
Docker Compose¶
# /etc/cron.daily/hub-backup.sh
#!/bin/bash
set -euo pipefail
BACKUP_DIR=/backup/hub
DATE=$(date +%Y%m%d-%H%M)
mkdir -p "$BACKUP_DIR"
# Hub
docker exec sshub-postgres pg_dump -U securityhub securityhub | \
gzip > "$BACKUP_DIR/hub-$DATE.sql.gz"
# DomainScope (если рядом)
docker exec ds-postgres pg_dump -U domainscope domainscope | \
gzip > "$BACKUP_DIR/ds-$DATE.sql.gz"
# Storage (отчёты)
tar -czf "$BACKUP_DIR/storage-$DATE.tar.gz" /opt/hub/storage/
# Очистка старых (>30 дней)
find "$BACKUP_DIR" -name "*.gz" -mtime +30 -delete
Kubernetes¶
Используйте CronJob с pgbackrest / Velero / k8s-snapshot:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hub-postgres-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: pg-dump
image: postgres:15
envFrom:
- secretRef: { name: hub-postgres-credentials }
command:
- /bin/sh
- -c
- |
pg_dump -h hub-postgres -U $POSTGRES_USER $POSTGRES_DB | gzip > /backup/hub-$(date +%F).sql.gz
volumeMounts:
- name: backup, mountPath: /backup
volumes:
- name: backup
persistentVolumeClaim: { claimName: backup-pvc }
restartPolicy: OnFailure
Off-site¶
Бэкапы только локально на той же VM — не бэкапы. Регулярно перекладывайте:
- S3 (s3cmd, rclone)
- Yandex Object Storage
- Защищённый bastion-host
Восстановление¶
# Создать пустую БД (если ещё нет)
docker exec sshub-postgres psql -U securityhub -c 'CREATE DATABASE securityhub;'
# Залить
gunzip -c hub-backup-20260601.sql.gz | docker exec -i sshub-postgres psql -U securityhub securityhub
# Перезапустить backend (миграции должны быть прокатанные)
docker compose restart backend worker
Тестируйте восстановление раз в квартал. Бэкап без отрепетированного restore — не бэкап.
Мониторинг¶
Адреса проверки состояния¶
| Сервис | Адрес | Что показывает |
|---|---|---|
| Backend Hub | GET /health |
Живость процесса |
| Backend Hub | GET /version |
Версия компонента |
| Backend Hub | GET /api/v1/version |
То же, внутри пространства API |
| DomainScope | GET /health |
Живость |
| DomainScope | GET /ready |
Готовность: база доступна, циклы не устарели |
| DomainScope | GET /healthz |
Живость приёмника команд перепроверки |
Эти адреса подходят для внешнего наблюдения — Zabbix, Uptime Kuma, blackbox-exporter и любых аналогов.
У worker нет собственного адреса проверки
Worker не обслуживает HTTP. Его состояние оценивается косвенно — по тому, разбирается ли очередь задач (см. ниже), и по журналу процесса. Живой процесс с неразбираемой очередью выглядит здоровым для оркестратора, но таковым не является.
Метрики Prometheus¶
С 0.33 каждый долгоживущий процесс — backend, worker, worker автономного
тестирования — отдаёт метрики на отдельном порту (METRICS_PORT, по
умолчанию 9090), не на порту API.
# Compose: порт намеренно не проброшен на хост
docker compose exec backend wget -qO- http://127.0.0.1:9090/metrics
# Kubernetes: сбор идёт прямо с пода, ingress этот порт не маршрутизирует
kubectl -n hub port-forward deploy/hub-backend 9090:9090
Этот порт не публикуют наружу
Аутентификации у него нет — сбор метрик не носит пользовательскую сессию, — а ряды описывают внутреннее состояние: объёмы находок по критичности, вердикты сканеров, заблокированные исходящие обращения. Сетевая граница здесь и есть способ защиты.
Реестр у каждого процесса свой: worker считает свои задания сам, и без собственного порта его счётчики читать было бы негде.
Что публикуется:
| Метрика | Тип | Про что |
|---|---|---|
http_requests_total |
счётчик | Запросы по методу, маршруту и коду ответа |
http_request_duration_seconds |
гистограмма | Длительность запроса (корзины доведены до 60 с — загрузка отчёта легально долгая) |
http_requests_in_flight |
измеритель | Запросы в обработке |
securityhub_build_info |
измеритель | Версия и коммит компонента |
securityhub_findings |
измеритель | Находки по критичности и статусу |
securityhub_sla_violations_open |
измеритель | Открытые нарушения сроков по критичности |
securityhub_river_jobs |
измеритель | Задания очереди по виду и состоянию |
securityhub_db_connections |
измеритель | Пул соединений: открытые, занятые, простаивающие |
securityhub_metrics_last_collect_success_timestamp_seconds |
измеритель | Когда бизнес-показатели обновлялись в последний раз |
securityhub_metrics_collect_failures_total |
счётчик | Сбои сбора, по виду запроса |
Плюс стандартные go_* и process_*.
Две особенности, важные для настройки оповещений:
- бизнес-показатели собираются по таймеру
(
METRICS_COLLECT_INTERVAL_SECONDS, по умолчанию 60 с), а не на каждый сбор: иначе нагрузка на базу росла бы с числом сборщиков, а медленный запрос выглядел бы как «цель недоступна»; - при сбое сбора значения остаются прежними, а не обнуляются: ноль
нарисовался бы как «находок не стало». Устаревание ловится по возрасту
securityhub_metrics_last_collect_success_timestamp_seconds— на него и стоит настраивать оповещение.
Бизнес-показатели считает только backend: это состояние базы, а не процесса, и в воркерах получились бы те же цифры за тройную цену. Метрики пула соединений и Go-рантайма собираются во всех процессах.
Трейсинг¶
Проброс контекста (traceparent/tracestate, B3, x-request-id) работает
всегда и настройки не требует. Настраивается только экспорт:
| Переменная | Значение |
|---|---|
OTEL_EXPORTER_OTLP_ENDPOINT |
Адрес коллектора. Пусто — экспорт выключен, спаны всё равно создаются и контекст переносят |
OTEL_TRACES_SAMPLER_ARG |
Доля трейсов, начинаемых здесь. Решение вышестоящего сервиса сохраняется |
Схема адреса решает транспорт: http:// — без TLS, https:// — с TLS. Адрес
без схемы роняет старт — это заметнее, чем экспортёр, который молча уходит в
тайм-аут.
Очередь задач — главный показатель¶
Самый содержательный признак того, что обработка идёт, — состояние очереди:
| Запрос | Что даёт |
|---|---|
GET /api/v1/jobs/stats |
Сводка по состояниям задач |
GET /api/v1/jobs |
Перечень задач с фильтрами |
POST /api/v1/jobs/<id>/retry |
Повторить сбойную задачу |
Растущее число задач в ожидании при неизменном числе выполненных означает, что worker не работает или не справляется. Задачи, исчерпавшие попытки, остаются в состоянии ошибки — их видно и в интерфейсе, и через этот API.
Журналы¶
Уровень подробности задаётся LOG_LEVEL (debug, info, warn, error) и
меняется независимо от APP_ENV — подробный вывод можно включить в
промышленной среде для диагностики, не меняя формат записей.
Что наблюдать¶
| Что | Как проверять |
|---|---|
| Backend жив | Периодический запрос /health |
| Очередь разбирается | GET /api/v1/jobs/stats, сравнение с предыдущим значением |
| База доступна | Журнал backend без ошибок соединения |
| Место на томе отчётов | Переполнение прекращает приём новых отчётов |
| Место на томе базы | Переполнение останавливает запись |
| Сроки устранения | GET /api/v1/sla/violations и сводки на дашборде |
| Свободное место на диске | стандартный node_exporter / системный мониторинг |
| Размер БД | SELECT pg_size_pretty(pg_database_size('securityhub')); |
| Свежесть бизнес-показателей | Возраст securityhub_metrics_last_collect_success_timestamp_seconds |
| Сбои сбора метрик | Рост securityhub_metrics_collect_failures_total |
Логирование¶
Где логи¶
- Compose:
docker compose logs <service>, ротация через docker-daemon (/etc/docker/daemon.json→log-opts) - K8s: stdout pods → cluster logging (Loki, Elastic, etc.)
В Kubernetes выключайте файловый приёмник логов¶
Помимо стандартного вывода Hub по умолчанию пишет журнал ещё и в файл
(logs/app.log, logs/error.log) с ротацией — LOG_FILE_SINK_ENABLED=true.
В контейнере этот каталог обычно лежит на смонтированном томе, владелец
которого не совпадает с непривилегированным пользователем контейнера.
Тогда первая же ротация падает:
Дальше эта строка печатается на каждую запись в журнал. Замер на рабочем стенде: 78 таких строк за три минуты в воркере — шум удваивает объём журнала и топит в себе настоящие сообщения.
Хуже то, что отказ тихий: стандартный вывод рядом продолжает работать, снаружи всё выглядит нормально, и файловый приёмник может месяцами не писать ничего — пока кто-нибудь не полезет за файлом и не обнаружит, что его нет.
В Kubernetes журналы и так собираются со стандартного вывода, поэтому файловый приёмник здесь — чистый риск (права, диск, ротация) без пользы:
Для bare-metal и виртуальных машин без централизованного сбора умолчание
true правильное — там файл и есть основной канал.
Формат¶
В APP_ENV=production — JSON (zap), легко парсится:
{
"level": "info",
"ts": "2026-06-02T12:34:56.789Z",
"msg": "report uploaded",
"report_id": "rpt-...",
"engine": "gosec",
"findings_count": 47,
"user_id": "user-...",
"trace_id": "abc..."
}
Полезные grep'ы¶
# Все upload-операции
docker compose logs backend | jq -r 'select(.msg=="report uploaded")'
# Ошибки за последний час
docker compose logs --since 1h backend | jq 'select(.level=="error")'
# Кто логинился сегодня
docker compose logs backend | jq -r 'select(.msg=="user login") | "\(.ts) \(.email)"'
Логи DomainScope¶
# Compose
docker compose -f docker/docker-compose.yml logs -f domain-scope
# systemd
sudo journalctl -u domain-scope -f
# По циклам
docker compose logs domain-scope | grep "cycle"
Retention¶
- Docker default: бесконечно (заполнит диск). Настройте:
- Логи в
./logs/— ротируйте черезlogrotate - K8s —
kubelet log-rotation(включён по умолчанию, проверьте/etc/kubernetes/)
Очистка старых данных¶
Reports (storage)¶
Очистка запускается ежечасно. Расписание задано в коде и переменными
окружения не управляется: CLEANUP_SCHEDULE — устаревшая переменная, она
только вызывает предупреждение в журнале, если задана.
Что удаляется:
| Что | Переменная | По умолчанию |
|---|---|---|
| Обработанные отчёты | CLEANUP_RETENTION_COMPLETED_DAYS |
старше 7 суток |
| Отчёты с ошибкой разбора | CLEANUP_RETENTION_FAILED_DAYS |
старше 30 суток |
| Необработанные отчёты | CLEANUP_RETENTION_PENDING_DAYS |
0 — не удаляются |
| Файлы без записи в базе | CLEANUP_RETENTION_ORPHAN_HOURS |
старше 24 часов |
Удаляются и файл, и запись в базе. Проверить, что именно будет удалено, не
удаляя, можно через CLEANUP_DRY_RUN=true.
Конфиг — см. Конфигурация: обзор и соглашения → CLEANUP_*.
Refresh tokens¶
Worker refresh_token_cleanup чистит revoked tokens старше REFRESH_CLEANUP_RETENTION_DAYS=30.
Audit log¶
В Hub нет автоматической очистки audit_logs — растут навсегда. Если БД распухает, добавьте cron:
(Только после согласования с security/compliance — audit-log может быть обязателен по политике.)
Безопасность сервера¶
Firewall¶
# Минимум для production
ufw default deny incoming
ufw allow 22/tcp # SSH (или нестандартный порт)
ufw allow 443/tcp # HTTPS
ufw allow 80/tcp # HTTP → 443
ufw enable
Никогда не выставляйте наружу 5432 (PostgreSQL) и 8082 (backend). Только через nginx + auth.
fail2ban¶
Добавьте jail для nginx 401/403:
# /etc/fail2ban/jail.d/nginx-auth.conf
[nginx-auth]
enabled = true
filter = nginx-auth
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 600
bantime = 3600
Регулярные обновления¶
- OS:
unattended-upgrades(Ubuntu/Debian) илиdnf-automatic(RHEL) - Docker base-images: пересобирайте раз в месяц с свежим
apk upgrade/apt upgrade - Hub: следите за релизами, обновляйте раз в 2-4 недели (см. Обновления)
Производительность¶
Postgres: параметры сервера под объём находок¶
Hub не управляет настройками сервера базы — их задаёт тот, кто разворачивает PostgreSQL. Умолчания официального образа рассчитаны на «просто запустилось», а не на рабочий объём, и на инсталляции с сотнями тысяч находок это становится главным источником тормозов.
Больнее всего shared_buffers с умолчанием 128 МБ. Для масштаба — замер
на стенде с 471 тыс. находок: таблица findings занимала 583 МБ плюс около
1 ГБ индексов, очередь заданий — ещё 822 МБ. То есть ~2,4 ГБ горячих данных
обслуживались кэшем меньше десятой доли от них: попадание в кэш держалось на
67 % при норме для OLTP выше 95 %, и каждый третий блок читался с диска.
Проверить у себя:
SELECT round(100.0*sum(heap_blks_hit)/nullif(sum(heap_blks_hit)+sum(heap_blks_read),0),1) AS cache_hit_pct
FROM pg_statio_user_tables;
SHOW shared_buffers;
Разумная отправная точка — 25 % от памяти, выделенной контейнеру базы:
postgres -c shared_buffers=1GB \
-c effective_cache_size=3GB \
-c work_mem=16MB \
-c maintenance_work_mem=256MB
shared_buffers— собственный кэш PostgreSQL, 25 % памяти контейнера.effective_cache_size— не выделение памяти, а обещание планировщику, сколько данных реально закэшировано. Внутри контейнера с лимитом памяти обещать больше лимита значит систематически занижать стоимость чтения и получать неверные планы.work_mem— на каждый узел сортировки или хэша в каждом соединении, а не на запрос. Считайте худший случай: произведениеwork_mem × соединения × узлыне должно приближаться к лимиту памяти.maintenance_work_mem— скоростьVACUUMи построения индексов.
Если том базы на HDD, в том числе сетевом, оставьте random_page_cost=4
и effective_io_concurrency=1: их умолчания описывают именно вращающийся
диск. Выставлять значения для SSD (1.1 и 200) без реального SSD значит
соврать планировщику — он начнёт считать случайное чтение дешёвым и выбирать
обход по индексу там, где выгоднее последовательное чтение.
На HDD рестарт пода базы — дорогая операция
Кэш обнуляется, и первые запросы читают всё с диска. На упомянутом стенде первый запрос после пересоздания пода занял 90 секунд против ~1 секунды на прогретом кэше. Планируйте рестарты соответственно и не принимайте эту картину сразу после выката за неисправность.
Postgres: регулярные проверки¶
Регулярно проверяйте:
-- Размер таблиц
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC LIMIT 10;
-- Bloat
SELECT schemaname,tablename,n_dead_tup,n_live_tup FROM pg_stat_user_tables
WHERE n_dead_tup > 1000 ORDER BY n_dead_tup DESC;
-- Vacuum status
SELECT relname, last_vacuum, last_autovacuum FROM pg_stat_user_tables;
Если bloat > 30% — VACUUM FULL <table> в окно maintenance.
Индексы¶
Hub при миграциях создаёт нужные индексы автоматически. Если кастомные запросы тормозят:
Disaster Recovery план¶
Минимальный набор шагов:
- Identify: какой компонент упал (backend/DB/worker/etc.)
- Stabilize: переключите на staging если есть; иначе announce maintenance
- Restore data: из последнего бэкапа (см. выше)
- Restore service: redeploy с известно-рабочей версии
- Verify: smoke-tests (login, list находки, upload SARIF)
- Resume traffic: переключайте обратно
- Postmortem: что пошло не так, как избежать впредь
RTO целевой: < 4 часа. RPO: < 24 часа (с ежедневными бэкапами).
Связанные документы¶
- Обновления — обновления и миграции
- Диагностика — типовые проблемы
Что именно переживает отказ, а что нет, и какие резервные копии обязательны — Отказоустойчивость. Расчёт мощности и настройки, влияющие на потребление, — Расчёт ресурсов.