自托管运维¶
在内部服务器、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 |
API_SECRET_PREVIOUS |
API_SECRET_PREVIOUS_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" }。
安全审计包¶
面向企业 / 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 与 经 IdP federation 到 OIDC 的 SAML(无原生 SP;ADR 007) |
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。
若 X-Tenant-Id 格式无效,API 返回 400 TENANT_SLUG_INVALID,不会静默回退到 default。
多租户检查清单¶
| 关注点 | 行为 |
|---|---|
| 数据隔离 | 租户范围 SQL 使用 WHERE tenant_id = …(栈、history、通知 prefs)。 |
| Header 校验 | 发送 header 时需合法 slug;未知 slug → 404 TENANT_NOT_FOUND。 |
| 保留 slug | 创建 tenant 时不可用 default, system, admin, root, postgres, public(409 TENANT_SLUG_RESERVED)。 |
| Auth / RBAC | API_SECRET 与可选 OIDC;tenant 管理路由需 admin 角色。 |
| 速率限制 | v1 上 scan/watch 按 IP + tenant 分桶。 |
| Audit | audit 行含 tenant;GET /api/v1/audit 仅当前 tenant。 |
| 通知 | 设置 X-Tenant-Id 时使用租户 prefs 与 webhook;dedup 含 tenant 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 需认证。
| 指标 | 类型 | 说明 |
|---|---|---|
cve_radar_scans_total |
counter | scan/watch |
cve_radar_vulns_found |
gauge | 最近成功 scan |
cve_radar_scan_duration_seconds |
histogram | 处理时长 |
cve_radar_source_reachable |
gauge | source |
cve_radar_mirror_age_days |
gauge | mirror |
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
# MIRROR_MANIFEST_PATH=/data/mirrors/.mirror-manifest.json
# MIRROR_STALE_DAYS=7
设置 OSV_BULK_PATH 时从本地 JSON 树读取 OSV(npm run mirrors:sync / make sync-osv-bulk),不调用 query API。GitHub/RSS/外部翻译 跳过。缺少 NVD/KEV mirror env → 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。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 通过 schemas/airgap/*.json 校验镜像 JSON 样例(npm run check:airgap-schemas)。
Helm chart¶
官方 chart:charts/cve-radar/。包含 Deployment、Service、可选 Ingress、Redis、可选 Postgres、/app/data PVC,以及通过 *_FILE 挂载 Secret。
helm install cve-radar ./charts/cve-radar -n cve-radar --create-namespace
Kubernetes Operator (CVEScanStack)¶
v0 Operator 将 CVEScanStack 自定义资源协调为 agent 与 watch 的 CronJob。CRD 组:cve-radar.io/v1alpha1;清单见 deploy/k8s/operator/。集群内运行: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 及 import/watch 的 cron。#254。
扫描任务 worker (BullMQ)¶
设置 REDIS_URL 后,在 API 旁运行独立 worker:npm run worker:run(生产镜像:node dist-server/server/worker/run.js)。API 接受 POST /api/v1/scan/queue;客户端轮询 GET /api/v1/jobs/:id,并从 GET /api/v1/jobs/:id/results 获取分页结果。当 /api/capabilities 报告 features.jobQueue: true 时,Web UI 切换为队列 + 轮询。无 Redis 时扫描保持同步(POST /api/v1/scan/stream)。见 #257。
REDIS_URL=redis://127.0.0.1:6379 npm run worker:run
扫描 agent(Trivy / Grype)¶
通过 POST /api/v1/scans/import 导入 Trivy/Grype JSON。CLI:npm run agent:scan。CronJob 示例:deploy/k8s/agent-scan-cronjob.example.yaml。docs/self-hosted/AGENT.md。
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, MIRROR_MANIFEST_PATH, MIRROR_STALE_DAYS |
| K8s | K8S_DISCOVERY_ENABLED, K8S_DISCOVERY_NAMESPACES |
| Enrichment | EPSS_ENABLED, COMPLIANCE_ENABLED |
维护者副本:docs/self-hosted/。