
VPS 自动化漏洞响应:从 CVE 情报到无人值守热修补
最近一次安全事件复盘时,我们的 VPS 运维流程暴露出一个典型的"时间差"问题:内核与软件包供应链安全通告已经发布,但真正落地到生产环境平均仍要 4~7 天。其中大量时间消耗在人工阅读公告、评估影响、编写补丁脚本和逐台验证上。
在单兵/小团队运维 VPS 的场景下,这种"响应时差"本身就构成了安全敞口。因此,本文基于 本地 LLM(Ollama)+ Python 自动化,构建了一套 VPS 漏洞自动响应管道:从 CVE 情报采集、本地模型研判到 4 阶段热修补,全程无需人工介入,漏洞暴露窗口可以压缩到 20 分钟以内。
一、整体架构:一条"情报 → 处置 → 验证"的自动化管道
整个方案的核心思路是:把"人"从信息搬运工的角色中解放出来,只保留"审批与兜底"两个角色。
CVE/NVD feed Ollama LLM Patch pipeline VPS
┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ ┌────────┐
│ NVD/Debian │──▶│ 研判/分级 │──▶│ 1. Dry-run │──▶│ 目标机 │
│ 安全通告源 │ │ 生成补丁指令 │ │ 2. 沙箱热重载 │ │ 执行 │
│ (每日拉取) │ │ (JSON 协议) │ │ 3. 冒烟测试 │ │ 验证 │
└──────────────┘ └──────────────┘ │ 4. 原子提交/回滚 │ └────────┘
└───────────────────┘ ▲
审计日志
(SQLite 全量留痕)
四个关键组件:
| 组件 | 职责 | 说明 |
|---|---|---|
| 情报采集器 | 每日拉取 NVD / 发行版安全通告 | 按 VPS 在装软件包过滤,避免"误报轰炸" |
| LLM 研判引擎 | Ollama + 本地 7B 模型 | 判断"这台 VPS 是否受影响、影响等级、处置策略",输出结构化指令 |
| 热修补管道 | 4 阶段执行 | Dry-run → 沙箱 → 冒烟 → 原子提交/自动回滚 |
| 审计层 | SQLite + 哈希链 | 每次判定、执行、回滚全量留痕,供事后追溯 |
二、本地 LLM 研判:不把所有 CVE 都推到人面前
直接订阅 NVD 全量通告,一天可能有几十上百条,绝大多数与"我这几台 VPS"无关。LLM 在这一步的价值是做相关性过滤与影响评估:
Input(给 LLM 的上下文):
- VPS 指纹:OS、内核版本、在装软件包清单、暴露端口
- CVE 条目:编号、描述、CVSS 分数、已知 PoC 情况
Output(结构化指令):
{
"cve": "CVE-2026-12345",
"affected": true,
"severity": "critical",
"strategy": "hot-reload", // 可选: hot-reload / rolling-restart / full-restart
"target_services": ["nginx", "openssl"],
"rollback_window_s": 30,
"confidence": 0.92
}
关键点:输出是受约束的 JSON 协议(JSON Schema + 枚举值校验),而不是自由文本。这样下游管道可以安全解析,LLM 幻觉被限制在"可控的字段"里。
对 confidence 低于阈值(例如 0.7)的条目,管道不会自动执行,而是推送到运维者手机(Telegram/邮件)做人工审批——LLM 负责 80% 的确定性研判,人只兜底 20% 的模糊地带。
三、4 阶段热修补:原子化执行 + 30 秒自动回滚
这是整个方案里工程含量最高的部分。目标不是"打个补丁",而是 “打补丁失败也不会让服务挂掉”。
阶段 1:Dry-run(零副作用)
- 在影子环境(隔离容器或离线挂载)先跑一遍完整流程
- 校验:补丁文件完整性、依赖版本冲突、磁盘余量、系统单元状态
- 任何一项不通过 → 直接终止,不触碰生产
阶段 2:沙箱热重载
- 在独立沙箱实例上应用补丁并启动服务
- 跑核心业务探针(健康检查、关键 API 冒烟请求)
- 沙箱通过 → 进入生产执行;沙箱失败 → 生产不受影响
阶段 3:生产冒烟测试
- 生产目标上执行补丁与热重载(nginx reload / 服务平滑重启)
- 立即跑生产冒烟:
curl健康端点、关键路径探测、连接池/延迟指标 - 冒烟失败 → 30 秒内自动触发回滚(恢复到上一版本的服务单元 + 配置快照)
阶段 4:原子提交 + 回滚窗口
- 所有变更通过
systemd单元 + 配置快照 + 包版本记录一起"原子提交" - 回滚窗口默认 30 秒(可配置),窗口内一旦监控告警(错误率、延迟突增),自动回滚到补丁前状态
- 窗口结束后正式固化,进入审计记录
热重载 vs 重启:能热重载的服务(nginx、部分 runtime)优先
reload,服务不中断;必须重启的走平滑重启 + 回滚窗口兜底。策略由 LLM 研判阶段的strategy字段决定。
四、在 4C8G VPS 上实测
实测环境:单台 4C8G VPS,Nginx + Python 应用 + 本地 Ollama(Qwen2.5-7B, ~4GB 显存/内存占用可接受)。
| 指标 | 人工流程 | 本方案 |
|---|---|---|
| 从 CVE 发布到生产处置完成 | 4~7 天 | ~20 分钟 |
| 高峰期 LLM 研判 100 条/日耗时 | — | ~6 分钟(7B 本地推理) |
| 热重载期间服务中断 | 重启型补丁 1~5 分钟 | 0(reload 型)/ < 30s(重启型含回滚) |
| 误判导致服务挂起 | 常见(人工疏漏) | 0(沙箱 + 回滚窗口兜底) |
| 审计追溯 | 聊天记录/脚本 | SQLite 哈希链,全量可回放 |
成本侧:本地 7B 模型一次性部署,推理成本为 0(不依赖云端 API);相比"每天订阅云端 LLM 研判",长期运维成本基本可以忽略。
五、完整脚本(可直接落地)
以下是核心调度脚本 cve_triage_patch.py(简化版,生产可补全重试/限流/队列):
#!/usr/bin/env python3
"""VPS CVE auto-triage + 4-stage hot patch pipeline.
Stages: dry-run -> sandbox smoke -> prod smoke -> atomic commit w/ 30s rollback."""
import json, subprocess, time, sqlite3, os, hashlib, urllib.request, sys
OLLAMA = "http://127.0.0.1:11434"
MODEL = "qwen2.5:7b"
AUDIT = "/var/lib/vps-ops/audit.db"
ROLLBACK_WINDOW = 30 # seconds
CONFIDENCE_THRESHOLD = 0.70 # below this -> human approval, no auto-patch
def vps_fingerprint():
"""OS/kernel/installed packages/ports -> context for LLM relevance check."""
os_ver = open("/etc/os-release").read().split("\n")[0]
kernel = os.uname().release
pkgs = subprocess.check_output(
"dpkg -l | awk '{print $2\" \"$3}' | grep -v 'ii' || rpm -qa",
shell=True).decode(errors="ignore")
ports = subprocess.check_output("ss -tlnp | awk '{print $4}'",
shell=True).decode(errors="ignore")
return {"os": os_ver, "kernel": kernel, "packages": pkgs.strip(),
"listen_ports": ports.strip()}
def llm_judge(cve_entry, fingerprint):
"""Local LLM: relevance + severity + patch strategy, constrained JSON out."""
prompt = (
"You are a VPS security triage engine. Given the VPS fingerprint and "
"CVE below, respond ONLY with JSON: {cve, affected, severity, strategy, "
"target_services, rollback_window_s, confidence}. strategy one of: "
"hot-reload | rolling-restart | full-restart | no-action."
"\nFingerprint: " + json.dumps(fingerprint) +
"\nCVE: " + json.dumps(cve_entry)
)
r = urllib.request.urlopen(OLLAMA + "/api/generate",
data=json.dumps({"model": MODEL, "prompt": prompt,
"stream": False}).encode())
out = json.loads(r.read())["response"].strip()
return json.loads(out) # caller validates against schema
def run_stage(cmd, label, timeout=300):
"""Run one pipeline stage; return (ok, output)."""
p = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
ok = p.returncode == 0
audit(f"{label} ok={ok} cmd={cmd}")
return ok, p.stdout + p.stderr
def audit(msg, stage="unknown"):
"""Hash-chained SQLite audit log (tamper-evident)."""
conn = sqlite3.connect(AUDIT)
prev = conn.execute(
"SELECT hash FROM audit ORDER BY ts DESC, id DESC LIMIT 1").fetchone()
prev_hash = prev[0] if prev else "0" * 64
payload = f"{time.time()}|{stage}|{msg}|{prev_hash}"
h = hashlib.sha256(payload.encode()).hexdigest()
conn.execute("INSERT INTO audit (ts, stage, msg, prev_hash, hash) "
"VALUES (?,?,?,?,?)", (time.time(), stage, msg, prev_hash, h))
conn.commit(); conn.close()
def execute_patch(decision):
"""4-stage pipeline. Returns 'patched' | 'rolled-back' | 'pending-approval'."""
cve, strategy = decision["cve"], decision["strategy"]
audit(f"start {cve} strategy={strategy}", "plan")
if decision.get("confidence", 0) < CONFIDENCE_THRESHOLD:
return "pending-approval" # push to human via Telegram/email
# Stage 1: dry-run (zero side effects)
ok, out = run_stage(
"vps-patch-tool dry-run --cve " + cve + " --strategy " + strategy,
"stage1-dryrun")
if not ok:
audit("dry-run failed, aborting", "stage1"); return "rolled-back"
# Stage 2: sandbox smoke (isolated instance, prod untouched)
ok, out = run_stage("vps-patch-tool sandbox --cve " + cve, "stage2-sandbox")
if not ok:
audit("sandbox failed, prod untouched, aborting", "stage2")
return "rolled-back"
# Stage 3: prod apply + immediate smoke
ok, out = run_stage("vps-patch-tool apply --cve " + cve
+ " --strategy " + strategy, "stage3-prod")
ok2, _ = run_stage("curl -fsS http://127.0.0.1:8080/health && "
"vps-patch-tool smoke --cve " + cve, "stage3-smoke")
if not (ok and ok2):
subprocess.run("vps-patch-tool rollback --cve " + cve, shell=True)
audit("prod smoke failed -> auto rollback", "stage3")
return "rolled-back"
# Stage 4: rollback window, then atomic commit
time.sleep(ROLLBACK_WINDOW)
ok3, _ = run_stage("vps-monitor check --since "
+ str(int(time.time()) - ROLLBACK_WINDOW), "stage4-verify")
if not ok3:
subprocess.run("vps-patch-tool rollback --cve " + cve, shell=True)
audit("post-window anomaly -> rollback", "stage4")
return "rolled-back"
audit("committed", "stage4")
return "patched"
def main():
cves = json.load(open(sys.argv[1])) # fetched CVE list
fp = vps_fingerprint()
for cve in cves:
decision = llm_judge(cve, fp)
result = execute_patch(decision)
print(f"{decision['cve']}: {result} "
f"(sev={decision.get('severity')}, "
f"conf={decision.get('confidence')})")
if __name__ == "__main__":
main()
对应的 systemd 定时器(每日 02:00 拉取通告并跑管道):
# /etc/systemd/system/vps-cve-triage.service
[Unit]
Description=VPS CVE auto-triage & hot patch pipeline
After=network-online.target ollama.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/vps-ops/cve_triage_patch.py \
/var/lib/vps-ops/latest_cves.json
Environment=OLLAMA=127.0.0.1:11434
TimeoutStartSec=900
Restart=on-failure
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/vps-cve-triage.timer
[Unit]
Description=Run VPS CVE triage daily
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
六、生产落地的关键原则
- LLM 只做研判,不做执行——它输出的是受约束的 JSON 指令,真正动手的是确定性管道。这样即使模型"说错话",也不会直接破坏系统。
- 沙箱永远先行——生产环境一次都不应该在"未知补丁"上直接试错。
- 回滚窗口是硬性兜底——30 秒不嫌短,宁可误回滚(服务短暂降级)也不要让坏补丁固化。
- 低置信度永远走人工——自动化追求的是"确定性高的部分全托管",模糊地带交回给人,别为了 100% 自动化而冒兜底风险。
- 审计必须防篡改——哈希链 + 时间戳,事后可完整回放"当时为什么这么打、打了什么、有没有回滚"。
七、小结
这套方案把 VPS 漏洞响应从"人工接力"变成了"情报驱动 + 本地 LLM 研判 + 4 阶段热修补 + 自动回滚"的闭环,暴露窗口从以天计压缩到 20 分钟级,且几乎不增加基础设施成本(本地 7B 模型即可)。
关键不是"让 AI 直接打补丁",而是把 AI 放在研判层,把确定性管道和兜底机制放在执行层——这才是能在生产环境真正长期运行的形态。后续可以扩展的方向:接入供应链(pip/PyPI、npm)告警、按业务重要度分级处置策略、以及把回滚窗口从固定 30 秒改成基于指标动态判定。