最近 AI agent 普及得很快,扫描、找漏洞、写利用脚本,这些事情的速度比以前快了不止一点。手上几台 VPS 平时都是想起来才登录看看,心里不太踏实,就给它们加了个“巡警”:定期上去看一眼,有变化再告诉我。

这里笔记一下最后的做法。

VPS 巡检流程:定时触发、SSH 受限账号、采集 JSON、对比基线,无异常一行 OK,有异常交给 AI 分析

一,设计思路

核心就一条:巡检账号只能看,不能动。

受限只读账号:专用密钥 SSH 登录,只能运行采集脚本和几个只读 wrapper,拿不到 shell、不能转发

1,专用钥匙:巡检端生成一把专用的 ed25519 密钥,私钥只留在巡检端,装到 VPS 上的只有公钥。

2,受限只读账号:每台 VPS 上建一个系统用户(下面用 patrol-user 代替),密码锁死,不进 docker / adm / wheel 这些特权组。

3,forced command:authorized_keys 里把这把钥匙绑死到采集脚本上,登录进来只能跑它,输出一份 JSON:

restrict,command="/usr/local/bin/vps-patrol-collect" ssh-ed25519 AAAA... patrol

# 老版本 OpenSSH 不认识 restrict,安装脚本会自动回退成
no-pty,no-port-forwarding,no-agent-forwarding,no-X11-forwarding,command="/usr/local/bin/vps-patrol-collect" ssh-ed25519 AAAA... patrol

不管 ssh 后面跟什么命令,都只会跑采集脚本。拿不到 shell,不能端口转发,sftp / scp 也用不了。authorized_keys 归 root 所有,账号自己改不了。

4,固定的无参数 sudo wrapper:有些东西要 root 才读得到,就写几个固定的小脚本,sudoers 只放行这几个,并用 "" 禁止带任何参数:

# /etc/sudoers.d/ 下的思路(示意)
patrol-user ALL=(root) NOPASSWD: /usr/local/lib/<wrapper目录>/docker-ps "", /usr/local/lib/<wrapper目录>/ssh-fails "", /usr/local/lib/<wrapper目录>/pkg-status "", /usr/local/lib/<wrapper目录>/security-audit ""

wrapper 只输出计数、状态、哈希、名字这类结果,不给日志原文和文件内容。sudoers 片段装之前先用 visudo -cf 校验。

5,其它:

  • 纯 shell,不依赖 jq / python,CentOS 和 Ubuntu 都能用,x86 和 arm64 同一份脚本;
  • 来源 IP 限制(authorized_keys 里的 from=)做成可选项,巡检端出口 IP 固定的话可以打开;
  • sshd 配了 AllowUsers 的话,安装脚本只打印需要加的那一行,不自动改 sshd_config。

整套东西全部只读,在目标发行版的容器环境里都验证过。

二,部署

1,单文件安装,scp 到 /tmp,sudo 跑一下就行:

scp install-oneshot.sh your-user@<VPS-A>:/tmp/
ssh your-user@<VPS-A> 'sudo bash /tmp/install-oneshot.sh'

2,装完在 VPS 上自检:

sudo runuser -u patrol-user -- /usr/local/bin/vps-patrol-collect   # 输出一段 JSON
sudo -l -U patrol-user                                            # 只应看到那几个 wrapper

3,卸载:

sudo bash /tmp/install-oneshot.sh --uninstall

4,巡检端放一个主机列表,一行一台:名字、地址、可选端口。巡检脚本挨个 SSH 过去,结果按机器名和时间存成 JSON,有异常就 exit 1:

# hosts.txt
vps-a   <VPS-A>   <port>
vps-b   <VPS-B>

# 连接方式大致如下
LC_ALL=C ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 \
    -p <port> -i <巡检私钥> patrol-user@<VPS-A>
# 结果存到 data/<name>/<时间>.json

三,巡检项

分组列一下:

  • 运行状态:系统 / 内核、运行时间、负载、CPU、内存和 swap、磁盘和 inode、失败的服务、Top5 进程、是否需要重启、待更新包数、NTP;
  • 端口和公网暴露:监听端口,公网监听和本机监听分开看;
  • 外连:ESTABLISHED 连接的远端地址和基线比较,陌生地址的高位端口长连接要报;
  • 账号和权限:用户列表、UID 0、登录 shell、sudo / wheel / docker 组成员、sudoers 哈希、authorized_keys 指纹;
  • 持久化:cron、timer、启用的服务和 unit 文件、rc.local、profile.d、用户的 .bashrc / .profile、lsmod;ld.so.preload 存在就直接报警;
  • 可疑进程:可执行文件已删除还在跑的、从 /tmp 或 /dev/shm 跑起来的、像挖矿的进程名、ps 和 /proc 对不上的隐藏进程;
  • 关键文件:约 15 个关键二进制的哈希,系统 bin 目录下的 SUID 文件;
  • 认证和日志:24 小时 SSH 失败次数、密码登录成功、sudo 使用、登录来源、日志是否被截断;
  • SSH 配置:sshd 实际生效的设置,外加 sshd_config 的哈希;
  • web 目录:7 天内新增的 .php / .jsp / .sh / .py 文件(只列文件名和时间);
  • Docker 风险:容器状态,特权容器、挂载 docker.sock、host 网络、映射到 0.0.0.0 的端口;
  • 加固状态:防火墙、SELinux / AppArmor、fail2ban、自动安全更新。

四,基线对比

光把状态列出来意义不大,端口、用户一多,每次看一遍也看不出哪里变了。所以关键是对比:

  • 第一次巡检的结果记为基线;
  • 之后每次和基线 diff,报 ADDED / REMOVED / CHANGED;
  • 确认是自己做的正常变更(装了新服务、加了用户之类),用 --accept 把当前状态接受为新基线。

多了一个端口、多了一个用户、多了一把 key,比“现在有哪些端口”更值得看。

五,轻量

小 VPS 本来就没多少余量,巡检不能把机器拖慢。所以几个重的检查没有做:

  • 不做 rpm -Va / dpkg --verify 全量校验,只算约 15 个关键二进制的哈希;
  • 不全盘 find SUID,只看系统 bin 目录(maxdepth 1);
  • 不 grep web 目录的文件内容,只列 7 天内新增的脚本文件名和时间。

目标是整轮 5 秒左右跑完,全部只读,不改动机器。

六,省 token

巡检结果最后是交给 AI 看的,次数多了 token 就是钱。所以加了个 --quiet 模式:

  • 没异常:只输出一行 OK;
  • 有异常:才把详细结果交给 AI 分析。

大部分时候机器都是好好的,没必要每次都把一大段 JSON 喂给模型。

总结

只读账号,钥匙绑死脚本;
先记基线,再比变化;
重的不做,轻的常跑;
没事一行 OK,有事才叫 AI。

说到底就是把以前“想起来才登录看看”的事,变成固定的一份清单,定期自动对一遍。能不能防住真正的高手不好说,至少有异样能早点知道。

水平有限,有不对的地方,欢迎指正。

最后修改日期: 2026年10月10日

作者