Self-host эксплуатация¶
Команды, развёртывающие CVE Radar на внутренних серверах, Kubernetes или Docker, нуждаются в возможностях production из той же MIT-кодовой базы: audit-логи JSON, mount секретов, RBAC, мультитenant PostgreSQL, метрики Prometheus, offline-зеркала и обнаружение стека в Kubernetes.
Enterprise-уровня и лицензионных блокировок нет. Переменные окружения меняют только операционное поведение. См. Эксплуатация и Конфигурация.
flowchart TB
classDef ui fill:#e9edf5,stroke:#00baba,color:#253343
classDef api fill:#f3fcfc,stroke:#008c8c,color:#253343
classDef data fill:#fff7ed,stroke:#eda232,color:#253343
classDef ext fill:#f5f5f5,stroke:#666,color:#253343
subgraph deploy["Self-host развёртывание"]
Browser[Browser SPA]:::ui
API[Express API]:::api
PG[(PostgreSQL опционально)]:::data
Metrics["GET /metrics"]:::api
Audit[stdout audit JSON]:::ext
end
Browser --> API
API --> PG
API --> Metrics
API --> Audit
API --> Feeds[NVD / OSV / mirrors]:::ext
API --> Notify[Slack / SMTP / webhooks]:::ext
На схеме: опциональный Postgres, scrape Prometheus на /metrics, audit в stdout и исходящие уведомления после watch. В airgap публичные feeds заменяются локальными зеркалами (см. Airgap).
Audit-логирование¶
Одна JSON-строка на событие в stdout для ELK/Loki/Splunk или Docker json-file.
| action | Когда |
|---|---|
scan |
После каждого POST /api/scan |
watch |
После каждого POST /api/watch |
health |
При AUDIT_HEALTH=true и detailed health |
export |
Пока только в браузере |
{
"audit": true,
"ts": "2026-06-06T14:30:00.000Z",
"action": "scan",
"ip": "10.0.0.42",
"stack": ["redis", "nginx"],
"duration_ms": 8420,
"sources_failed": ["NVD"],
"result_count": 12,
"mode": "full"
}
TRUST_PROXY_HOPS за reverse proxy. Заголовки CVE и ключи API не логируются.
docker logs cve-radar 2>&1 | grep '"audit":true'
Секреты¶
| env | *_FILE |
|---|---|
NVD_API_KEY |
NVD_API_KEY_FILE |
GITHUB_TOKEN |
GITHUB_TOKEN_FILE |
DEEPL_API_KEY |
DEEPL_API_KEY_FILE |
ALERT_WEBHOOK_URL |
ALERT_WEBHOOK_URL_FILE |
NOTIFICATION_SLACK_WEBHOOK_URL |
— |
NOTIFICATION_DISCORD_WEBHOOK_URL |
— |
NOTIFICATION_TELEGRAM_BOT_TOKEN |
— |
NOTIFICATION_SMTP_PASS |
mount через env file в Compose |
API_SECRET |
API_SECRET_FILE |
API_SECRET_PREVIOUS |
API_SECRET_PREVIOUS_FILE |
Пример Compose: docker-compose.secrets.example.yml. В Kubernetes — mount Secret как файлы.
RBAC и API¶
С API_SECRET все /api/* кроме GET /api/health и GET /api/v1/health требуют ключ. API_ROLE:
| Роль | Права |
|---|---|
| admin | Настройки, tenant CRUD, scan/watch, translate, meta и history |
| scanner | scan/watch/validate, translate, meta и history |
| viewer | meta и history — без scan |
| auditor | history/trends и meta — без scan |
По умолчанию admin. viewer на POST /api/scan получает 403 { "code": "FORBIDDEN" }.
Пакет аудита безопасности¶
Для enterprise / GRC (#259):
| Документ | Назначение |
|---|---|
docs/security/THREAT_MODEL.md |
STRIDE-lite угрозы |
docs/security/DEPENDENCY_AUDIT.md |
Процедура аудита зависимостей |
docs/self-hosted/SECRETS.md |
Ротация API_SECRET + API_SECRET_PREVIOUS без простоя |
SECURITY.md |
OIDC и SAML через federation → OIDC (без native SP; ADR 007) |
Уведомления watch¶
Self-host часто нужны out-of-band алерты при новых CVE в watch. CVE Radar отправляет уведомления асинхронно после POST /api/watch с непустым newVulns — ответ HTTP не меняется.
| Канал | env |
|---|---|
| Slack | NOTIFICATION_SLACK_WEBHOOK_URL или legacy ALERT_WEBHOOK_URL |
| Discord | NOTIFICATION_DISCORD_WEBHOOK_URL |
| Telegram | NOTIFICATION_TELEGRAM_BOT_TOKEN, NOTIFICATION_TELEGRAM_CHAT_ID |
| SMTP | NOTIFICATION_SMTP_HOST, FROM, TO; опционально PORT, USER, PASS |
| Webhook | NOTIFICATION_WEBHOOK_URL |
| Параметр | env | По умолчанию |
|---|---|---|
| Мин. severity | NOTIFICATION_MIN_SEVERITY / ALERT_MIN_SEVERITY |
HIGH |
| Dedup (ms) | NOTIFICATION_DEDUP_MS |
900000 |
| Формат Slack | ALERT_WEBHOOK_FORMAT |
slack / generic |
Legacy ALERT_WEBHOOK_URL работает через NotificationService. NOTIFICATION_* поддерживает mounts *_FILE. Проверка: GET /api/health?detailed=true → alerts.webhookConfigured. См. Оповещения и NOTIFICATIONS.md.
Мультитenant (PostgreSQL)¶
DATABASE_URL=postgres://cve_radar:cve_radar@127.0.0.1:5432/cve_radar
Migrations при первом подключении pool. Schema в server/db/schema.ts (Drizzle ORM) для постепенной ORM-миграции; runtime-запросы tenant/stack и scan-history используют Drizzle через getDb() (общий pool pg).
X-Tenant-Id: arvancloud-sre
| Method | Path | Описание |
|---|---|---|
POST |
/api/v1/tenants |
Создать tenant |
GET |
/api/v1/tenants/stacks |
Список стеков |
POST |
/api/v1/tenants/stacks |
Создать стек |
GET |
/api/v1/tenants/stacks/:id |
Стек по UUID |
PUT |
/api/v1/tenants/stacks/:id |
Обновить стек |
DELETE |
/api/v1/tenants/stacks/:id |
Удалить стек |
GET |
/api/v1/scans/history |
History tenant |
GET |
/api/v1/scans/trends |
Trends tenant |
History фильтруется по tenant_id. Legacy /api/* пишет в default.
При неверном X-Tenant-Id API возвращает 400 TENANT_SLUG_INVALID — без тихого fallback на default.
Чек-лист мультитenant¶
| Тема | Поведение |
|---|---|
| Изоляция данных | SQL с WHERE tenant_id = … для стеков, history и prefs уведомлений. |
| Проверка header | Валидный slug при отправке; неизвестный → 404 TENANT_NOT_FOUND. |
| Зарезервированные slug | default, system, admin, root, postgres, public нельзя при создании tenant (409 TENANT_SLUG_RESERVED). |
| Auth / RBAC | API_SECRET и опциональный OIDC; admin-маршруты tenant требуют admin. |
| Rate limit | scan/watch на IP + tenant в v1. |
| Audit | Строки audit с tenant; GET /api/v1/audit только для текущего tenant. |
| Уведомления | Prefs и webhooks tenant при X-Tenant-Id; dedup включает slug. |
| Legacy | /api/* без версии пишет history в default. |
| Health | GET /api/health?detailed=true → database.multiTenant; capabilities → features.multiTenant. |
Docker Compose: docker compose --profile postgres up -d и DATABASE_URL — см. TENANTS.md.
Prometheus и Grafana¶
GET /metrics. METRICS_ENABLED (true), METRICS_PROTECT для auth.
| Метрика | Тип | Описание |
|---|---|---|
cve_radar_scans_total |
counter | scan/watch |
cve_radar_vulns_found |
gauge | последний успешный scan |
cve_radar_scan_duration_seconds |
histogram | длительность |
cve_radar_source_reachable |
gauge | upstream |
cve_radar_mirror_age_days |
gauge | mirror |
cve_radar_cache_entries_total |
gauge | размер cache |
Дашборд: docs/self-hosted/grafana/cve-radar-dashboard.json.
sum(rate(cve_radar_scans_total{status="success"}[5m]))
/ sum(rate(cve_radar_scans_total[5m]))
Airgap-развёртывание¶
AIRGAPPED=true
NVD_MIRROR_URL=http://internal-mirror/nvd
KEV_MIRROR_URL=http://internal-mirror/kev/catalog.json
# OSV — mirror или bulk:
OSV_MIRROR_URL=http://internal-mirror/osv
# OSV_BULK_PATH=/data/osv/extracted
# MIRROR_MANIFEST_PATH=/data/mirrors/.mirror-manifest.json
# MIRROR_STALE_DAYS=7
С OSV_BULK_PATH OSV читается из локального JSON (npm run mirrors:sync / make sync-osv-bulk) без query API. GitHub/RSS/перевод отключены. Нет mirror env для NVD/KEV → fail-closed.
curl -s 'http://localhost:3001/api/health?detailed=true' | jq '.airgap.mirrorHealth'
Боковая панель показывает предупреждение об устаревшем зеркале, если возраст превышает MIRROR_STALE_DAYS (по умолчанию 7). Prometheus: cve_radar_mirror_age_days.
Синхронизация зеркал (зона с интернетом)¶
npm run mirrors:sync (или make mirrors-sync) загружает KEV, страницы NVD 2.0 (MIRROR_NVD_DAYS) и OSV bulk с manifest и SHA256. Укажите MIRROR_MANIFEST_PATH на air-gap хосте. RPO/RTO и размер диска: docs/self-hosted/AIRGAP.md.
| Флаг | Эффект |
|---|---|
MIRROR_SKIP_NVD=true |
только KEV + OSV |
MIRROR_SKIP_OSV=true |
только NVD + KEV |
MIRROR_SKIP_KEV=true |
только NVD + OSV |
CI проверяет JSON-образцы по schemas/airgap/*.json (npm run check:airgap-schemas).
Helm chart¶
Официальный chart: charts/cve-radar/. Deployment, Service, опциональный Ingress, Redis, опциональный Postgres, PVC для /app/data, Secret mounts через *_FILE.
helm install cve-radar ./charts/cve-radar -n cve-radar --create-namespace
Kubernetes Operator (CVEScanStack)¶
Operator v0 согласует CR CVEScanStack с CronJob для agent-сканирования и watch. CRD: cve-radar.io/v1alpha1; манифесты в deploy/k8s/operator/. In-cluster: npm run operator:run — docs/self-hosted/OPERATOR.md.
kubectl apply -f deploy/k8s/operator/crd-cvescanstack.yaml
kubectl apply -f deploy/k8s/operator/rbac.yaml
kubectl apply -f deploy/k8s/operator/deployment.yaml
Каждый CVEScanStack задаёт stack, apiKeySecretRef, опциональный tenantId и cron для import и watch. #254.
Scan job worker (BullMQ)¶
When REDIS_URL is set, run a separate worker alongside the API: npm run worker:run (production image: node dist-server/server/worker/run.js). The API accepts POST /api/v1/scan/queue; clients poll GET /api/v1/jobs/:id and fetch paginated results from GET /api/v1/jobs/:id/results. The Web UI switches to queue + poll when /api/capabilities reports features.jobQueue: true. Without Redis, scans stay synchronous (POST /api/v1/scan/stream). See #257.
REDIS_URL=redis://127.0.0.1:6379 npm run worker:run
Scan agent (Trivy / Grype)¶
POST /api/v1/scans/import для JSON Trivy/Grype. CLI: npm run agent:scan. Пример CronJob: deploy/k8s/agent-scan-cronjob.example.yaml. docs/self-hosted/AGENT.md.
Kubernetes discovery¶
K8S_DISCOVERY_ENABLED=true
# K8S_DISCOVERY_NAMESPACES=production,staging
GET /api/v1/discovery/kubernetes — auth в production. Отключено → 503 K8S_DISCOVERY_DISABLED.
{
"enabled": true,
"images": ["haproxy", "nginx", "redis"],
"tools": ["HAProxy", "Nginx", "Redis"],
"unmapped": ["my-sidecar"]
}
SA только read на deployments. Сочетайте с tenant.
Краткий справочник¶
| Тема | env |
|---|---|
| Audit | AUDIT_HEALTH, TRUST_PROXY_HOPS |
| Secrets | *_FILE |
| RBAC | API_SECRET, API_ROLE |
| Уведомления | NOTIFICATION_*, ALERT_WEBHOOK_URL |
| Tenant | DATABASE_URL, X-Tenant-Id |
| Metrics | METRICS_ENABLED, METRICS_PROTECT |
| Airgap | AIRGAPPED, *_MIRROR_URL, OSV_BULK_PATH, MIRROR_MANIFEST_PATH, MIRROR_STALE_DAYS |
| K8s | K8S_DISCOVERY_ENABLED, K8S_DISCOVERY_NAMESPACES |
| Enrichment | EPSS_ENABLED, COMPLIANCE_ENABLED |
Maintainer-копии: docs/self-hosted/.
Домой · Назад: Эксплуатация