一、先算账:一套加固流程到底要花多少钱

韩国VPS的安全加固本身不花钱,花钱的是加固前被入侵后的重建成本,以及你为了防住攻击而多买的配置。先把这笔账算清楚,再谈怎么配。
看产品库里真实在售的机型:云途香港 DS.ES.C 是每月 700 美元、64GB 内存、800GB SSD、2000Mbps 端口、40000GB 月流量;同系列的 HK.DS.S 是每月 800 美元、128GB 内存、6000GB 存储、2000Mbps、40000GB 流量。两者的内存单价分别是 10.94 美元/GB 和 6.25 美元/GB,硬盘单价分别是 0.875 美元/GB 和 0.133 美元/GB。
换算成加固这件事上:
| 算账维度 | 具体算法 | 结果 |
|---|---|---|
| 单台月成本 | 700 美元/月 ÷ 1 台 | 700 美元 |
| 每 GB 内存成本 | 700 ÷ 64 | 10.94 美元/GB |
| 每 GB 硬盘成本 | 700 ÷ 800 | 0.875 美元/GB |
| 每 TB 流量成本 | 700 ÷ (40000÷1024) | 17.92 美元/TB |
| 超流量重购单价 | 产品库 traffic_reset_price 字段值为 500,超流量按次重购 | 每次重购 500(美元计) |
| 一年安全重建成本 | 700 × 12 ÷ 2 次重建 | 4200 美元 |
这行数字才是重点:一年 4200 美元的重建成本,换来的是不到一小时的人工加固。 一台机器被挖矿或植入后门后,重装系统、清数据、重配业务、重新验证,这个过程通常要花掉几小时到半天,而且清不干净的概率不低。
第二个更实用的算法是:加固投入对比攻击收益。 一次典型的 SSH 暴力破解,成功后攻击者会在你的机器上做什么?挂挖矿、塞 SSH 公钥、架设代理。挖矿是最常见的变现方式,因为它不需要你的机器有业务价值,只要 CPU 一直开着就能自动产生收益。所以加固的第一优先级永远是 CPU 消耗异常。
第三个算法决定你该买哪一档配置:算你的加固盲区。 一台 1GB 内存的机器,装完 fail2ban 加 auditd 加 AIDE 之后,常驻内存大约在 200MB 上下,安全工具链本身就要吃掉五分之一的内存。1GB 内存跑加固套件会很吃力,更别说还要跑业务。 这就是为什么产品库最低档的 RosCloud-01(每月 19.99 美元、1GB 内存)不适合承载需要完整加固的生产环境。
第四个算法:把加固成本摊进带宽预算。 以云途 HK.DS.S 为例,2000Mbps 端口加 40000GB 月流量,单价 20.48 美元/TB。一次成功的劫持,机器消耗的流量由你的账单承担。 挖矿程序的挖矿池通信流量不大,但代理类的会大量吃流量。假设被架代理后每月额外跑掉 2000GB 流量,按 20.48 美元/TB 算就是 40 美元。这笔钱不多,但它证明了一件事:机器被入侵后,账单异常是一个有效的告警信号。
二、验收阈值:加固完必须跑通的六个数字

网上大部分加固文章只说「要改 SSH 端口」「要装 fail2ban」,但读完你还是不知道做到什么程度算合格。这一节给的是可以直接抄进检查脚本的阈值。
阈值一:SSH 密码认证必须为 no,且实测有效。
# 查最终生效配置(比读配置文件可靠,Include 片段会覆盖)
sudo sshd -T | grep -E "^(passwordauthentication|permitrootlogin|pubkeyauthentication|maxauthtries|logingracetime|maxstartups)"
# 阈值:passwordauthentication no
# 阈值:maxauthtries 3
# 阈值:logingracetime 20
# 阈值:maxstartups 10:30:60
实测方式:从另一台机器用密码方式尝试登录,必须失败。
# 预期结果:Permission denied (publickey),不是提示输入密码
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@你的服务器IP
阈值二:fail2ban 必须真的在解析日志。
# 阈值:fail2ban-regex 的匹配数必须 > 0,匹配数为 0 等于没装
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 阈值:当前 jail 状态必须是 running,不是 ERROR
sudo fail2ban-client status
# 阈值:10 分钟窗口内 5 次失败即封禁,封禁时长 86400 秒
sudo fail2ban-client status sshd
阈值三:对外开放的端口数量必须与业务清单一致。
# 列出所有对外监听的端口
sudo ss -tulnp | grep -E "0\.0\.0\.0|::"
# 阈值:对外监听端口数量 ≤ 4(SSH + 80 + 443 + 1 个管理端口)
# 阈值:3306 / 6379 / 27017 等数据库端口不得出现在对外列表里
这一条是最容易失败的阈值。 很多人装完 ufw 就以为端口关了,但 ufw 只管 iptables 的 INPUT 链,如果数据库服务本身绑定在 0.0.0.0,而 ufw 的规则又写错了,端口照样在外面。判定标准是 ss 的输出,不是 ufw 的输出。
# 数据库必须只监听回环
sudo ss -tlnp | grep 3306
# 阈值:输出只出现 127.0.0.1:3306,出现 0.0.0.0:3306 即为不合格
阈值四:待更新的安全包数量必须为 0。
# 阈值:apt list --upgradable 里不能有带 -security 后缀的包
apt list --upgradable 2>/dev/null | grep -c security
# 阈值:结果为 0
阈值五:CPU 空闲占用必须低于 15%,且没有任何非系统目录的进程。
# 阈值:top 的 Cpu(s) 空闲行,idle 必须 ≥ 85%
top -bn1 | grep "Cpu(s)"
# 阈值:任何进程的 exe 落在 /tmp、/dev/shm、/var/tmp 的一律视为异常
for p in /proc/[0-9]*; do
exe=$(readlink "$p/exe" 2>/dev/null)
case "$exe" in */tmp/*|*/dev/shm/*|*/var/tmp/*) echo "VIOLATION: $p -> $exe";; esac
done
阈值六:必须存在一份可用的异地备份,且最近一次恢复演练在 30 天内。
# 阈值:restic check 返回无错误
restic -r sftp:user@backup:/var/backups/myvps check
# 阈值:restore 到临时目录后文件数与源一致
restic -r sftp:user@backup:/var/backups/myvps restore latest --target /tmp/restore-test
ls /tmp/restore-test
这六个阈值可以全部写成一个脚本,每周自动跑一次,输出 PASS/FAIL 清单。 这比每天手动检查成本低得多,而且有记录可查。
三、第一步:改 SSH 是整套加固里性价比最高的一环

五分钟的 SSH 加固能挡掉绝大部分自动化攻击。 你开通实例后,扫描器在几小时内就会扫到你的 22 端口,这是必然事件,不是意外。
顺序不能错:先配密钥、确认能登进去,再关密码,最后才重启服务。
# 1. 生成密钥(ed25519 比 RSA 更短更快)
ssh-keygen -t ed25519 -C "my-kr-vps"
# 2. 首次用密码登录并上传公钥
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@你的服务器IP
# 3. 确认公钥已在位
cat ~/.ssh/authorized_keys
改配置。
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo nano /etc/ssh/sshd_config
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
Port 10022
AllowUsers root deploy
LoginGraceTime 20
MaxAuthTries 3
MaxStartups 10:30:60
MaxStartups 10:30:60 的含义是:未认证连接达到 10 个开始丢弃,超过 30 个时随机丢弃 60%。这个参数直接限制了攻击者能同时维持的爆破连接数,是真正的技术性门槛。
改端口到底防什么,必须说清楚。 现在的扫描器是一把扫 1 到 65535 全端口的,改端口不会让机器从扫描里消失,几小时内照样会被扫到。改端口真正的作用只有两个:一是让没有针对性配置的自动化工具漏掉你,二是大幅减少日志噪音——改完后你的 auth.log 里每天几千条失败尝试会变成几十条,真实攻击反而更容易被看见。
更有效的替代方案是限制来源 IP,前提是你有固定出口 IP:
curl -s https://api.ipify.org # 先确认自己的公网出口
# /etc/ssh/sshd_config.d/allow.conf
AllowUsers [email protected].*
AllowUsers [email protected]
这个方案比改端口强得多,因为它是从访问控制层面直接拒绝。但家宽 IP 会变,用云主机做跳板机的话跳板机本身要先加固好。
排查 SSH 问题永远先跑 sudo sshd -T。 配置文件可能有多个片段互相覆盖,最终生效的未必是你改的那份,sshd -T 打印的是合并后的真实配置,比翻文件快十倍。
自锁是这一步最大的风险。 预防办法有三条:一是改配置前先在另一个终端建一条保活长连接;二是改完不要立刻关掉当前会话,开新窗口测试能登上再关旧的;三是确认云厂商的 VNC 或网页控制台可用——这是前提,不是可选项。
# 保活会话
ssh -o ServerAliveInterval 60 root@你的服务器IP
# 改完先测语法再重启
sudo sshd -t && sudo systemctl restart ssh
sudo ss -tlnp | grep 10022
注意 Ubuntu 24.04 起服务名从 ssh 变成了 sshd。 不确定就先查:systemctl list-units | grep ssh。
如果真的锁在外面了,按顺序恢复:控制台登录,然后 sudo sshd -t 查语法,再查防火墙规则,最后查公钥权限。
chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
sudo sshd -T | grep -i authorizedkeysfile
顺手删掉用不上的账号并查一遍 UID。 每个多余的带 shell 账号都是爆破目标,隐藏后门的常见手法是建一个 UID 0 或 UID 1000 的账号冒充正常用户:
grep -vE "(nologin|false)" /etc/passwd
cut -d: -f3 /etc/passwd | sort | uniq -d # 重复 UID
awk -F: '$3 == 0 {print $1}' /etc/passwd # 所有 root 等价账号
密钥本身也要保护,别把私钥裸放在服务器上。 私钥应该只存在于你自己的电脑上,服务器上只放公钥。很多人的失误是把 id_ed25519 整个传到了服务器上,等于把钥匙和锁一起交给了别人。
给私钥加 passphrase,这样即使私钥文件泄露,攻击者也无法直接使用。
# 生成时设置 passphrase
ssh-keygen -t ed25519 -C "my-kr-vps"
# 日常使用用 ssh-agent 缓存,避免每次输 passphrase
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
再给 SSH 加上连接数上限,防止攻击者用大量并发连接耗尽你的进程数。 很多 VPS 的默认 SSH 配置没有这一条,攻击者用几百个并发连接就能把机器的进程表占满,让你的正常运维连不上。
# /etc/ssh/sshd_config.d/limits.conf
MaxSessions 10
ClientAliveInterval 300
ClientAliveCountMax 2
ClientAliveInterval 300 加 ClientAliveCountMax 2 的意思是:客户端连续 600 秒没有任何响应,服务端就断开这条连接。 这能清理掉大量半开连接,这些连接平时看着无害,但会持续占用文件描述符和进程槽位。
四、第二步:防火墙只放行业务必需的端口

VPS 默认状态下所有端口对外敞开。数据库 3306、Redis 6379、面板端口只要有一个暴露,就是现成的入侵入口。
sudo apt update && sudo apt install ufw
# 顺序不能反:先加规则,再启用
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 10022/tcp # SSH(改过端口就用这个)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
「只允许特定网段访问管理端口」这个用法很多人不知道:
# 只允许办公网访问 SSH
sudo ufw allow from 203.0.113.0/24 to any port 10022 proto tcp
# 只允许容器网段访问数据库
sudo ufw allow from 172.16.0.0/16 to any port 3306 proto tcp
但更好的做法是让数据库根本不监听公网。
# /etc/mysql/mysql.conf.d/mysqld.cnf
[ mysqld ]
bind-address = 127.0.0.1
# /etc/redis/redis.conf
bind 127.0.0.1 -::1
protected-mode yes
requirepass 你的强密码
sudo systemctl restart redis-server
sudo ss -tlnp | grep -E "3306|6379"
# 阈值:只出现 127.0.0.1
Redis 是最容易被忽视的入口。 它默认不带密码,一旦暴露在公网,几分钟内就会被写入 SSH 公钥或挖矿程序,直接拿下 root。
每个季度做一次「实际在监听什么」的检查。 很多人几个月前跑过临时测试服务,之后忘了还开着:
sudo ss -tulnp
sudo ss -tulnp | grep -E "0\.0\.0\.0|::"
sudo lsof -i -P -n | grep LISTEN
看到不认识的进程,查它的真实路径:
ps aux | grep -v grep | grep -E "defunct|\./"
ls -l /proc/$(pgrep -f 可疑进程名 | head -1)/exe
cat /proc/$(pgrep -f 可疑进程名 | head -1)/cmdline | tr '\0' ' '
正常的进程路径在 /usr/bin、/usr/sbin、/bin;路径在 /tmp、/dev/shm、/var/tmp 或者文件名是一串乱码,那就是恶意程序。
五、第三步:fail2ban 自动封禁与无人值守补丁

手工封 IP 追不上扫描器速度。fail2ban 的机制是监控日志,发现某 IP 在统计周期内失败次数超阈值就加进封禁列表。
sudo apt install fail2ban fail2ban-systemd
sudo cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 86400
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 ::1
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
[sshd]
enabled = true
port = 10022
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 86400
bantime.increment = true 值得单独说:开启后被反复尝试封禁的 IP,封禁时长按 factor 递增——第一次 1 天,第二次 2 天,第三次 4 天,最长 7 天。这对付换 IP 试探的分布式攻击特别有效。bantime.maxtime = 604800 则保证不会变成永久封禁,你不必手动解封。
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client set sshd unbanip 1.2.3.4
装完必须验证它在解析日志,而不是静默空转。
# 阈值:匹配数必须 > 0
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# 从另一台机器故意输错密码 6 次
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no root@你的服务器IP
# 然后回来确认封禁列表
sudo fail2ban-client status sshd
fail2ban-regex 是最实用的自检命令,它直接告诉你扫了多少行、匹配了多少行。匹配数为 0 说明 filter 和你的日志格式对不上,fail2ban 装了等于没装。
再给 Web 服务加 jail,并开 recidive 抓换号回来的攻击者:
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
maxretry = 3
bantime = 604800
补丁策略:只自动打安全更新,不自动做版本升级。
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
# /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1" 是关键的一行。 很多人装了 unattended-upgrades 却没开这个开关,结果它什么都没干。改完要重启服务才生效:
sudo systemctl restart unattended-upgrades
sudo systemctl list-timers | grep -i unattended
限定「只打安全补丁」,需要配置 origins 列表:
# 发行版代号用 lsb_release -cs 查,下面以 Ubuntu 24.04 (noble) 为例
Unattended-Upgrade::Allowed-Origins {
"ubuntu:noble-security";
"ubuntuESMApps:noble-apps-security";
"ubuntuESM:noble-infra-security";
};
这个取舍很重要。 PHP 从 8.1 跳到 8.3 这种大版本变化必须人工确认,跳过去可能让你的网站直接报错;安全补丁则基本无副作用。生产服务器上,稳定性比新特性重要。
改完必须验证它真的更新了,很多机器上无人值守更新会静默失败:
sudo tail -30 /var/log/unattended-upgrades/unattended-upgrades.log
apt list --upgradable | wc -l
WordPress 站点要单独管插件,插件漏洞比核心漏洞多:
wp plugin list --update=available --format=table
0 3 * * * cd /var/www/html && wp core update --quiet && wp plugin update --all --quiet
最后是删掉不用的软件,每一份多装的软件都是潜在漏洞面:
apt-mark showmanual
apt-get autoremove --dry-run
再补一条很多人会忽略的:数据库的默认账号。 MySQL 和 PostgreSQL 安装完都会带一个默认超级用户,它的密码在安装时通常是随机生成或干脆是空的。如果这个账号的密码为空或者你从没改过,它就是一个挂在数据库端口上的后门。
# MySQL 检查有哪些用户、哪些没有密码
sudo mysql -e "SELECT user, host, authentication_string FROM mysql.user;"
# PostgreSQL 检查
sudo -u postgres psql -c "\du"
# 删掉不需要的默认账号
sudo mysql -e "DROP USER 'test'@'%';"
同时确认数据库服务没有绑定到公网网卡。 前面说过 bind-address = 127.0.0.1 是最有效的一层,但很多人改完没有重启,或者改错了配置文件位置。
# 找出 MySQL 真正读取的配置文件位置
sudo mysqld --verbose --help 2>/dev/null | grep -A1 "Default options are read from"
# 确认最终生效的绑定地址
sudo mysql -e "SHOW VARIABLES LIKE 'bind_address';"
这个检查能排掉一大类低级失误。 确认结果是 127.0.0.1 才对,如果显示 0.0.0.0 或者 *,说明绑定没生效。
还有一个容易被忽略的入口:面板自带的数据库管理端口。 宝塔、1Panel、aaPanel 这类面板为了方便管理,会开一个数据库管理端口(常见是 888、33060 之类),这个端口在装防火墙时最容易被漏掉,因为它不写在你部署的任何服务配置里。
# 把所有对外监听的端口和进程一行行列出来,逐个对照你的业务清单
sudo ss -tulnp | grep -E "0\.0\.0\.0|::" | awk '{print $1, $5, $7}'
# 如果确认某个面板端口用不上,直接停掉对应服务
sudo systemctl stop mysql && sudo systemctl disable mysql
停掉比屏蔽更好。 一个不运行的服务不占端口、不占内存、不产生补丁面,比加一条防火墙规则干净得多。「不用的服务就不装」永远是安全的第一原则,加固只是补救。
配置防火墙时还有一条经常被忽略的规则:限制出站。 默认的 ufw 策略是允许所有出站连接,也就是你的机器可以主动连接任意地址。这条规则在机器被入侵后是致命的,因为攻击者需要把你的机器连到他的 C2 服务器才能远程控制,而你的机器不主动连出去,他就只能等。
# 危险做法:完全禁止出站
# sudo ufw default deny outgoing
# 后果:apt 装不了包、证书续不了、监控系统上报不了
# 正确做法:允许常用出站,其余拒绝
sudo ufw allow out to 8.8.8.8/32
sudo ufw allow out to 151.101.0.0/16
但不建议一上来就禁出站。 更稳妥的做法是保持默认允许,但在监控里盯住异常出站连接——第二节阈值五里那个 /proc 检查就是配套的。
六、第四步:入侵迹象排查,怎么知道自己有没有被入侵

加固是预防,这一步是确认预防有没有效。VPS 被入侵后通常不立刻发作,而是潜伏几天到几周,等到被当作跳板或矿机才暴露。主动排查比等它发作便宜得多。
先明确判断标准,分三档:
| 档次 | 典型迹象 | 该做什么 |
|---|---|---|
| 可疑 | 陌生 cron 项、陌生 systemd 服务、异常对外连接 | 逐条核对,确认来源,删掉来路不明的 |
| 明确 | 陌生账号、webshell、异常 sudo 免密记录 | 立即备份数据,准备重装 |
| 确证 | 挖矿进程、文件校验失败、C2 连接 | 直接重装,不要试图清理 |
关键原则:一旦落到「明确」这一档,就不要清理,直接重装。 清理后门的成本远高于重装一次,而漏掉一个后门意味着重装也白搭。
第一组:账号和登录记录。
# 所有 UID 0 的账号
awk -F: '$3 == 0 {print $1" (uid=0)"}' /etc/passwd
# UID 1000 以上且有 shell 的账号,和你自己的清单对比,多出来的都要查
awk -F: '$3 >= 1000 && $7 !~ /nologin|false/ {print $1, $3, $7}' /etc/passwd
# 最近的成功登录
last -n 30
# 失败的密码尝试按 IP 统计
sudo grep "Failed password" /var/log/auth.log | awk '{print $9}' | sort | uniq -c | sort -rn | head -20
# 成功但用了密码的方式(说明有人在用密码登进来)
sudo grep "Accepted password" /var/log/auth.log | tail -20
第二组:计划任务,攻击者藏身的地方。
sudo crontab -l -u root
for u in $(cut -f1 -d: /etc/passwd); do sudo crontab -l -u $u 2>/dev/null; done
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
sudo systemctl list-timers --all
# 常见隐藏方式
sudo grep -R "curl\|wget\|base64\|/dev/shm\|nc " /etc/cron* 2>/dev/null
现代恶意程序更爱注册成 systemd 服务而不是 cron:
systemctl list-unit-files --type=service | grep enabled
ls -la /etc/systemd/system/*.service
sudo grep -R "ExecStart" /etc/systemd/system/*.service 2>/dev/null | grep -vE "/usr/bin|/usr/sbin|/bin/|/usr/lib"
第三组:进程和异常文件。
# 挖矿程序会霸榜
ps aux --sort=-%cpu | head -15
ps aux --sort=-%mem | head -15
# 跑在临时目录里的进程
for p in /proc/[0-9]*; do
exe=$(readlink "$p/exe" 2>/dev/null)
case "$exe" in */tmp/*|*/dev/shm/*|*/var/tmp/*) echo "SUSPICIOUS: $p -> $exe";; esac
done
# 挖矿通常会下载大文件
sudo du -sh /tmp /var/tmp /dev/shm 2>/dev/null
sudo find / -xdev -type f -size +100M -exec ls -lh {} \; 2>/dev/null | head -20
第四组:网络连接,C2 连接是最容易确认的证据。
sudo ss -tunap | grep ESTAB
sudo ss -tunap | grep -v "127.0.0.1\|::1\|你的服务器IP"
# 路由表有没有被加恶意的路由
ip route
ip rule
第五组:文件完整性。
# 最近 7 天被改过的系统配置文件
sudo find /etc /usr/bin /usr/sbin -mtime -7 -type f 2>/dev/null
# 校验系统文件(Debian/Ubuntu 用 dpkg,CentOS 系用 rpm)
sudo dpkg -V
sudo rpm -Va | grep -E "^5"
rpm -Va 输出的第五列是校验结果,5 代表 MD5 不匹配,也就是这个文件被改过,这是入侵最直接的证据之一。
第六组:shell 启动脚本里的后门。
sudo grep -nE "curl|wget|base64|nc |/dev/tcp" /root/.bashrc /root/.profile /etc/profile /etc/bash.bashrc 2>/dev/null
sudo grep -E "base64|curl.*\|.*sh|wget.*\|.*sh|nc -e|/dev/tcp" /root/.bash_history 2>/dev/null | tail -20
第七组:sudo 权限有没有被篡改。 攻击者进来后最想做的一件事就是给自己加免密 sudo:
sudo grep -R "NOPASSWD" /etc/sudoers /etc/sudoers.d/ 2>/dev/null
输出必须逐条核对。 你自己配的那几条应该记得,别人加的就该问清楚是谁加的。
这七项加起来十分钟左右,建议每月跑一次。 关键不是每次手工看,而是把输出存下来做基线,之后 diff,新增的行就是变化点:
sudo { cat /etc/passwd; ss -tulnp; systemctl list-unit-files --type=service; crontab -l; } > ~/baseline_$(date +%F).txt
# 下次检查
sudo ss -tulnp > /tmp/now.txt
sudo ss -tulnp | diff - ~/baseline_2026-11-05.txt
七、Web 服务的攻击面:PHP 站点最容易出事的地方

很多 VPS 被入侵,起点不是 SSH,而是跑在上面的网站,尤其是 WordPress。
改任何配置之前先做摸底,看你的网站到底被怎么打。 这一步能告诉你该优先堵哪个口:
# 访问最多的路径
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 404 最多的请求(针对不存在路径的探测)
sudo awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
# 针对敏感文件的探测
sudo grep -E "\.env|\.git|wp-config|composer\.(json|lock)|\.sql|\.bak" /var/log/nginx/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -20
# 打到登录接口的 POST 尝试
sudo awk '$6 == "POST" && $7 ~ /wp-login|admin|login|xmlrpc/ {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
最后一条命令的输出如果集中在一个 IP 上,那就是正在爆破你的后台。 这比读十篇安全文章都直接。
关掉 XML-RPC,禁掉 wp-content 下的 PHP 执行:
location ~* ^/xmlrpc\.php$ {
deny all;
}
location ~* wp-config\.php\.(bak|old|save|swp|orig)$ {
deny all;
}
location ~* ^/wp-content/.*\.php$ {
deny all;
}
autoindex off;
PHP 配置层面有几个必做项:
; /etc/php/8.2/fpm/php.ini
open_basedir = /var/www/html:/tmp:/var/www
disable_functions = exec,passthru,shell_exec,system,popen,proc_open,pcntl_exec
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
expose_php = Off
upload_max_filesize = 20M
post_max_size = 22M
open_basedir 是防 webshell 横向扩散最有效的一道锁。 webshell 最典型的行为就是去读 /etc/passwd、去读别的站点的配置文件,open_basedir 会直接把它挡掉。
sudo systemctl restart php8.2-fpm
安全响应头成本几乎为零:
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
server {
listen 80;
server_name 你的域名;
return 301 https://$host$request_uri;
}
includeSubDomains 要谨慎加,它会影响所有子域名,如果你有子域名还没上 HTTPS,加了会导致它们无法访问。不确定就先不加。
TLS 配置:
# /etc/nginx/snippets/ssl-options.conf
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
ssl_protocols 里绝对不要留 TLSv1 和 TLSv1.1,留着只是给扫描器送漏洞报告。ssl_session_cache 能显著改善性能,访客第二次连接时省掉一次完整握手往返。
证书续期必须验证,这是最常见的静默故障:
sudo certbot renew --dry-run
sudo systemctl status certbot.timer
Let's Encrypt 证书只有 90 天有效期,续期失败站点直接不可用。 很多手动装的 certbot 只装了包没装定时器。
再加一条限速规则做双保险:
limit_req_zone $binary_remote_addr zone=wp_limit:10m rate=5r/m;
location ~* /wp-login\.php {
limit_req zone=wp_limit burst=5 nodelay;
}
八、Webshell 检测与文件完整性监控

前面几节是预防,这一节是发现。webshell 的特点是:文件被上传到某个目录,权限改成可执行,然后通过 HTTP 请求触发。
手工排查可疑的 PHP 文件:
# 最近 7 天新增或修改的 PHP 文件
sudo find /var/www -name "*.php" -mtime -7 -exec ls -la {} \; 2>/dev/null | head -30
# 含常见恶意函数的小文件(webshell 特征)
sudo grep -rlE "eval\s*\(|assert\s*\(|base64_decode\s*\(|system\s*\(|passthru\s*\(|shell_exec\s*\(|preg_replace\s*\(.*\/e" /var/www --include="*.php" 2>/dev/null
# 图片目录里的 PHP 文件(最常见的藏身处)
sudo find /var/www -path "*/uploads/*" -name "*.php" -o -path "*/images/*" -name "*.php" 2>/dev/null
部署 AIDE 做完整性监控,这是最省事的长期方案。
sudo apt install aide
sudo aideinit
# Debian/Ubuntu 会生成 /var/lib/aide/aide.db.new
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
sudo systemctl enable --now aide-check.timer
sudo aide --check
Tripwire 是替代方案,配置更简单:
sudo apt install tripwire
sudo tripwire-init
sudo tripwire --local-options
再补一条日志集中化,攻击者拿到 root 后第一件事就是清日志,日志必须有机器之外的副本。
# /etc/rsyslog.d/remote.conf
*.* @203.0.113.10:514
或者用 auditd 记录所有 sudo 操作:
sudo auditctl -w /usr/bin/sudo -p x -k sudo_exec
sudo ausearch -k sudo_exec -i | tail -50
这个记录的价值在于它能回答「谁在什么时候用了 sudo」,而这个信息在事后往往查不到,普通日志会被轮转清掉,auditd 的记录相对独立。
持久化规则,让 auditd 重启后不丢:
# /etc/audit/rules.d/audit.rules
-w /etc/passwd -p wa -k identity_change
-w /etc/shadow -p wa -k shadow_change
-w /etc/sudoers -p wa -k sudoers_change
-w /usr/bin/sudo -p x -k sudo_exec
-w /var/www -p wa -k webdir_change
-w /etc/ssh/sshd_config -p wa -k sshd_config_change
-w /etc/crontab -p wa -k cron_change
sudo augenrules --load
sudo auditctl -l
webdir_change 会记录网站目录里每一次文件写入。如果哪天发现一个可疑的 PHP 文件,这条记录能精确告诉你它是什么时候、怎么被写进来的。
再补一个很多人没做的:给关键二进制挂不可变属性。
sudo chattr +i /usr/bin/sshd
sudo chattr +i /usr/bin/passwd
sudo chattr +i /usr/bin/sudo
# 需要更新时先解除
sudo chattr -i /usr/bin/sshd
但 chattr +i 有代价:它会让正常的软件包升级失败,也会让你自己改配置时报错。建议只对最关键的几个二进制文件用,并把解除锁定的步骤写进运维手册。
内核参数加固,能直接挡掉一大类常见攻击:
# /etc/sysctl.d/99-hardening.conf
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
sudo sysctl --system
注意不要顺手加上 net.ipv4.icmp_echo_ignore_all = 1,禁 ping 会让你自己的排查变得很麻烦。
九、什么情况下不该按这套流程做

这一节比前面所有命令都重要。 加固是通用的,但有些约束会让这套流程的收益大幅下降甚至变成负担。以下判断只依据公开的机房位置、线路类型、计费方式和 IP 资源,不涉及对任何商家的评价。
场景一:你只有一台机器,且主要靠 SSH 密码登录,没有密钥可用。
强行关掉密码登录会直接锁死你。替代方案:保留密码登录,装 fail2ban 并把 maxretry 设为 3,同时把 SSH 端口从 22 改到高端口。 这样虽然安全性不如密钥,但比什么都不做强得多,而且不会把你锁在外面。
场景二:你的机器是按小时计费的临时开发机,跑完就删。
完整加固要花一到两小时,而机器生命周期只有几小时。替代方案:只做三件事,改端口、装 ufw 只放行 22 和 80/443、装 fail2ban。 这三步加起来十分钟,能覆盖绝大部分扫描噪声。
场景三:你的机器跑的是实时音视频或游戏服务端,客户端需要直接连进来。
这种情况下「只放行业务端口」的原则不适用,因为业务端口本身就是给公网用的。替代方案:把防护重心从主机层转到网络层,用带清洗能力的线路,针对你的业务端口做限速和连接数限制,而不是做全端口封锁。
场景四:你要跑的负载本身需要高内存,而你的配置档位内存不足。
安全工具链的常驻开销是实打实的。产品库最低档的 RosCloud-01 是每月 19.99 美元、1GB 内存、20GB 硬盘、500Mbps、1024GB 流量,1GB 内存跑完 fail2ban 加 auditd 加 AIDE 之后留给业务的空间不到 500MB。替代方案:选内存更大的档位,而不是硬塞。 云途 HK.DS.P 是每月 750 美元、64GB 内存、1024GB SSD、2000Mbps、40000GB 流量,内存单价 11.72 美元/GB,硬盘单价 0.732 美元/GB,流量单价 19.2 美元/TB;云途 DS.ES.C 每月 700 美元、64GB 内存、800GB SSD、流量单价 17.92 美元/TB,流量单价更低,适合流量消耗大的场景。
场景五:你的业务依赖固定 IP 白名单,机器要频繁更换。
加固配置每次重装都要重做,加密备份的恢复流程也要跟着变。替代方案:机器上不存业务数据,只做反向代理或转发层,数据放在有持久盘的对象存储上。 这样换机器时的数据迁移成本接近零。
场景六:你判断机器已经确认被入侵。
这时候做加固是错的,应该直接重装。 花两小时加固一台已经被植入后门的机器,不如花二十分钟重装。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 只想搭个站、预算优先:先看价格最低那档够不够用,别为用不上的配置付钱。看 justg 当前机型与价格
- 做跨境或店群、需要多 IP:优先看 IP 类型与换 IP 政策,别只看配置。看 vpckr 当前机型与价格
- 跑游戏服或实时业务:看上行抖动与防御能力,普通套餐往往不达标。看 zji 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 恒创科技 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十、结论:四类人各自该怎么做

第一类:个人用户,只有 1 台机器跑个人博客或小站。
你的合理默认值是按月付费的最低配置即可,不要买年付,年付在被入侵后你要赔进去更多沉没成本。做四件事:改 SSH 端口到 10022、生成 ed25519 密钥并关掉密码登录、ufw 只放行 22/80/443、装 fail2ban 且用 fail2ban-regex 验证它真的在跑。验收看第二节的阈值三:对外监听端口数量不超过 4。 升级配置的时机很明确:当你的监控发现内存谷值超过 70% 或待更新安全包数不为 0 时再升。
第二类:做外贸或跨境电商的公司站,跑在 2 台以上机器上。
你需要的是可追溯而不是不可访问。给每台机器配独立的 fail2ban jail、开全量 auditd 审计、把每台机器的基线文件定期 diff、备份做异地加密。验收看第二节的阈值六:最近一次恢复演练在 30 天内。 配置档位上流量单价是关键指标,按 17.92 美元/TB 的档位比 20.48 美元/TB 的档位在相同流量消耗下成本更低。
第三类:做 API 服务或数据处理的后端团队。
你们的暴露面是 API 端口而不是 SSH,所以加固重点要放在限流和异常检测上,而不只是关入口。做法是 Nginx 侧对 API 路径配 limit_req、对异常状态码配告警、数据库只监听回环、Redis 强制密码且只监听回环。验收看第二节的阈值五:CPU 空闲必须达到 85% 以上,且没有进程跑在临时目录里。 对这类场景来说,一台机器上跑多台实例加独立审计,比多开几台低配更省。
第四类:托管客户服务器的运维团队。
客户服务器不能直接禁 root 也不能限制来源 IP,因为你的客户要能登进去。正确做法是每个客户一个独立系统账号并用 sudo 提权、每个客户一条独立的 fail2ban jail、按客户隔离目录权限、全量 auditd 记录所有 sudo。核心原则是可追溯而不是不可访问。 这种模式下配置档位要留余量,因为多客户共享一台机器时,单个客户的流量峰值会挤占其他人的配额,内存单价高但资源隔离好的档位反而更合适。
通用的一句话收尾:加固的成本是一个小时,收益是把「被入侵后重建」的概率从「迟早」变成「小概率」。但别把它当成买了保险就万事大吉,配置一台机器之前先花十分钟确认云厂商控制台可用,这是所有加固操作的前提条件,不是可选项。
关于本文提到的所有具体配置:文中引用的价格与配置均来自渠道库快照(2026-11-05 22:39 抓取),以官网当日显示为准;没有给出结构化套餐数据的商家,本文不涉及具体报价,请以下单页为准。文中所有安全命令都是通用操作,是否适合你的具体环境需要你自己判断后再执行。

喜欢
高兴
鬼脸
呵呵
无聊
伤心