自托管运维¶
在内部服务器、Kubernetes 或 Docker 上运行 CVE Radar 的团队,需要超出默认单用户浏览器工作流的生产能力。本章记录同一 MIT 代码库中的 生产向功能:结构化 audit 日志、密钥文件挂载、RBAC、PostgreSQL 多租户、Prometheus 指标、离线镜像与 Kubernetes 栈发现。
无 enterprise 层级或许可证门控。 环境变量仅切换 运维 行为。日常排障见 运维;基础 env 见 配置。
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["自托管部署"]
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 / 镜像]:::ext
API --> Notify[Slack / SMTP / webhook]:::ext
图示可选 Postgres、Prometheus 抓取 /metrics、stdout audit 与 watch 后的出站通知。airgap 安装用本地镜像替代公网 feed(见 airgap 部署)。
Audit 日志¶
CVE Radar 可在 stdout 输出 每个 audit 事件一行 JSON,供 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。不记录 CVE 标题或 API 密钥。
docker logs cve-radar 2>&1 | grep '"audit":true'
密钥与生产环境¶
| 直接 env | 文件 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 |
通过 Compose env file 挂载 |
API_SECRET |
API_SECRET_FILE |
示例:docker-compose.secrets.example.yml。Kubernetes 建议 Secret 文件挂载。
RBAC 与 API 认证¶
设置 API_SECRET 后,除 GET /api/health 与 GET /api/v1/health 外,所有 /api/* 需 X-Api-Key 或 Bearer。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" }。
Watch 通知¶
自托管运维常需 watch 发现新 CVE 时的 带外告警。POST /api/watch 返回非空 newVulns 后,CVE Radar 异步 派发服务端通知,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 | 默认 |
|---|---|---|
| 最低严重度 | NOTIFICATION_MIN_SEVERITY / ALERT_MIN_SEVERITY |
HIGH |
| 去重窗口 (ms) | NOTIFICATION_DEDUP_MS |
900000 |
| Slack 格式 | ALERT_WEBHOOK_FORMAT |
slack / generic |
legacy ALERT_WEBHOOK_URL 仍经 NotificationService 工作。NOTIFICATION_* 支持 *_FILE 挂载。验证:GET /api/health?detailed=true → alerts.webhookConfigured。详见 告警 与 NOTIFICATIONS.md。
多租户 (PostgreSQL)¶
DATABASE_URL=postgres://cve_radar:cve_radar@127.0.0.1:5432/cve_radar
首次连接 pool 时运行 migration。server/db/schema.ts(Drizzle ORM)用于渐进式 ORM 迁移;tenant/stack 与 scan-history 的运行时查询通过 getDb() 使用 Drizzle(共享 pg pool)。
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 |
GET |
/api/v1/scans/trends |
租户 trends |
history 按 tenant_id 过滤。legacy /api/* 写入 default tenant。
Prometheus 与 Grafana¶
GET /metrics。METRICS_ENABLED(默认 true),METRICS_PROTECT 需认证。
| 指标 | 类型 | 说明 |
|---|---|---|
cve_radar_scans_total |
counter | scan/watch |
cve_radar_vulns_found |
gauge | 最近成功 scan |
cve_radar_scan_duration_seconds |
histogram | 处理时长 |
cve_radar_source_reachable |
gauge | 上游可达 |
cve_radar_cache_entries_total |
gauge | 缓存大小 |
仪表盘: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
设置 OSV_BULK_PATH 时从本地 JSON 树读取 OSV(scripts/sync-osv-bulk.sh / make sync-osv-bulk),不调用 query API。GitHub/RSS/外部翻译 跳过。缺少 NVD/KEV mirror env → fail-closed。curl …/api/health?detailed=true | jq .airgap。参考 scripts/sync-mirrors.sh 与 scripts/sync-osv-bulk.sh。
Kubernetes 栈发现¶
K8S_DISCOVERY_ENABLED=true
# K8S_DISCOVERY_NAMESPACES=production,staging
GET /api/v1/discovery/kubernetes — 生产环境需认证。禁用时 503 K8S_DISCOVERY_DISABLED。
{
"enabled": true,
"images": ["haproxy", "nginx", "redis"],
"tools": ["HAProxy", "Nginx", "Redis"],
"unmapped": ["my-sidecar"]
}
ServiceAccount 仅对 deployments 只读。与 租户隔离 组合使用。
快速参考¶
| 主题 | env |
|---|---|
| Audit | AUDIT_HEALTH, TRUST_PROXY_HOPS |
| 密钥 | *_FILE 挂载 |
| RBAC | API_SECRET, API_ROLE |
| 通知 | NOTIFICATION_*, ALERT_WEBHOOK_URL |
| Tenant | DATABASE_URL, X-Tenant-Id |
| 指标 | METRICS_ENABLED, METRICS_PROTECT |
| Airgap | AIRGAPPED, *_MIRROR_URL, OSV_BULK_PATH |
| K8s | K8S_DISCOVERY_ENABLED, K8S_DISCOVERY_NAMESPACES |
| Enrichment | EPSS_ENABLED, COMPLIANCE_ENABLED |
维护者副本:docs/self-hosted/。