最近 AI agent 普及得很快,扫描、找漏洞、写利用脚本,这些事情的速度比以前快了不止一点。手上几台 VPS 平时都是想起来才登录看看,心里不太踏实,就给它们加了个“巡警”:定期上去看一眼,有变化再告诉我。
这里笔记一下最后的做法。
一,设计思路
核心就一条:巡检账号只能看,不能动。
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。
说到底就是把以前“想起来才登录看看”的事,变成固定的一份清单,定期自动对一遍。能不能防住真正的高手不好说,至少有异样能早点知道。
水平有限,有不对的地方,欢迎指正。