Skip to content

自托管运维

在内部服务器、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/healthGET /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

默认 adminviewer 调用 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=truealerts.webhookConfigured。详见 告警NOTIFICATIONS.md

多租户 (PostgreSQL)

DATABASE_URL=postgres://cve_radar:cve_radar@127.0.0.1:5432/cve_radar

首次连接 pool 时运行 migration。server/db/schema.tsDrizzle 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, public409 TENANT_SLUG_RESERVED)。
Auth / RBAC API_SECRET 与可选 OIDC;tenant 管理路由需 admin 角色。
速率限制 v1 上 scan/watch 按 IP + tenant 分桶。
Audit audit 行含 tenantGET /api/v1/audit 仅当前 tenant。
通知 设置 X-Tenant-Id 时使用租户 prefs 与 webhook;dedup 含 tenant slug。
Legacy 路由 无版本 /api/* 将 history 写入 default
Health GET /api/health?detailed=truedatabase.multiTenant;capabilities → features.multiTenant

Docker Compose:docker compose --profile postgres up -d 并配置 DATABASE_URL(见 TENANTS.md)。

Prometheus 与 Grafana

GET /metricsMETRICS_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

docs/self-hosted/HELM.md

Kubernetes Operator (CVEScanStack)

v0 OperatorCVEScanStack 自定义资源协调为 agent 与 watch 的 CronJob。CRD 组:cve-radar.io/v1alpha1;清单见 deploy/k8s/operator/。集群内运行:npm run operator:rundocs/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 定义 stackapiKeySecretRef、可选 tenantId 及 import/watch 的 cron。#254

扫描任务 worker (BullMQ)

设置 REDIS_URL 后,在 API 旁运行独立 workernpm 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.yamldocs/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/

返回首页 · 上一章:运维