VPS 安全加固教程
这篇是 VPS 安全加固教程,重点放在 SSH 登录面和 iptables/ip6tables 主机防火墙。它和 nftables 转发篇的分工不同:nftables 负责 DNAT、SNAT、masquerade 这类转发 NAT;iptables/ip6tables 负责主机自己的 INPUT/OUTPUT 防护。文中所有 IP、端口、服务名都做了脱敏。
加固流程图
下面这个流程图用 PlantUML 写。Firefly 支持 plantuml 代码块渲染,语法可以参考文末的 PlantUML 示例文章。
为什么要先加固 SSH
VPS 的第一安全边界通常是 SSH。防火墙规则写得再好,如果 SSH 还允许 root 密码登录、弱密码爆破、新端口没验证就直接保存,风险仍然很高。
我的顺序是:
- 先保证有一个非 root 用户可以用 SSH key 登录。
- 再收紧 sshd 配置。
- 然后用 iptables/ip6tables 限制 SSH 新连接速率。
- 最后才保存持久化规则。
远程操作时不要关闭当前 SSH 会话。所有改动都要开新窗口验证成功以后再继续。
SSH 登录加固
先创建一个普通管理用户,下面用 ops 作为示例名:
adduser opsusermod -aG sudo ops在本地生成密钥,已有密钥可以跳过:
ssh-keygen -t ed25519 -a 100 -C "vps-ops"把公钥写到服务器:
install -d -m 700 -o ops -g ops /home/ops/.sshnano /home/ops/.ssh/authorized_keyschown ops:ops /home/ops/.ssh/authorized_keyschmod 600 /home/ops/.ssh/authorized_keys先在新窗口验证:
ssh ops@203.0.113.10sudo -v确认 ops 可以登录且能 sudo 后,再写 sshd 加固配置。建议用 drop-in 文件,方便回滚:
cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F-%H%M%S)
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'Port 22PubkeyAuthentication yesPasswordAuthentication noKbdInteractiveAuthentication noPermitEmptyPasswords noPermitRootLogin noMaxAuthTries 3LoginGraceTime 30X11Forwarding noAllowUsers opsClientAliveInterval 300ClientAliveCountMax 2EOF如果你暂时还没有稳定的非 root key 登录,不要设置 PermitRootLogin no 和 PasswordAuthentication no。这两项应该最后再关。
检查语法并重载:
sshd -tsystemctl reload ssh || systemctl reload sshd再新开窗口测试:
ssh ops@203.0.113.10如果要改 SSH 端口,先在防火墙里放行新端口,并确认新端口可以登录,再关闭旧端口:
NEW_SSH_PORT=22222iptables -I INPUT 1 -p tcp --dport "$NEW_SSH_PORT" -j ACCEPTip6tables -I INPUT 1 -p tcp --dport "$NEW_SSH_PORT" -j ACCEPT确认登录成功后,再把 Port 22 改成新端口,执行 sshd -t 和 reload。
SSH 防爆破限速
SSH 面向公网时,建议保留 hashlimit。它不会替代 SSH key,但可以降低爆破和异常重连带来的噪音。
IPv4:
SSH_PORT=22
iptables -A INPUT -p tcp --dport "$SSH_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 6/minute --hashlimit-burst 12 \ --hashlimit-mode srcip --hashlimit-name ssh_v4_rate \ -j DROPiptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPTIPv6:
SSH_PORT=22
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 6/minute --hashlimit-burst 12 \ --hashlimit-mode srcip --hashlimit-name ssh_v6_rate \ -j DROPip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT如果你用了非 22 端口,记得同步修改 SSH_PORT。保存规则前一定开新窗口验证。
fail2ban 可选补充
如果 SSH 仍然暴露公网,可以加 fail2ban 做日志级封禁:
apt updateapt install -y fail2ban
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'[sshd]enabled = truebackend = systemdport = 22maxretry = 5findtime = 10mbantime = 1hEOF
systemctl enable --now fail2banfail2ban-client status sshd如果 SSH 端口改了,port 也要一起改。
为什么还要写 iptables
做 IP 中转时,很容易只盯着 nftables 转发规则:入口端口有没有 DNAT,回包有没有 masquerade,forward 链有没有放行。
但 VPS 暴露在公网,真正的风险不只在“转发是否可用”,还包括:
- SSH 是否被爆破。
- 服务端口是否全网开放。
- UDP 是否无意暴露。
- 53、5353、5355 是否变成公共 DNS/mDNS/LLMNR 入口。
- 邮件端口、扫描端口是否被代理出口滥用。
- IPv6 是否忘记同步配置。
所以我的分工是:
| 层级 | 工具 | 职责 |
|---|---|---|
| 转发 NAT | nftables | 中转链路的 DNAT、SNAT、masquerade |
| 主机防火墙 | iptables/ip6tables | INPUT、OUTPUT、SSH 限速、服务端口白名单、防滥用 |
| 持久化 | netfilter-persistent | 保存 iptables/ip6tables |
| nft 持久化 | nftables service | 加载 /etc/nftables.conf |
不要把 netfilter-persistent save 当成 nftables 转发表的保存工具。它主要保存 /etc/iptables/rules.v4 和 /etc/iptables/rules.v6。
INPUT:默认拒绝,只放行必要入口
基础策略是 INPUT 默认 DROP,明确放行 lo、已建立连接、SSH 和必要服务端口。
脱敏 IPv4 示例:
SSH_PORT=22SERVICE_PORT=51001TRUSTED_RELAY_IP="198.51.100.10"
iptables -P INPUT DROPiptables -P FORWARD ACCEPTiptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPTiptables -A INPUT -m conntrack --ctstate INVALID -j DROPiptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# SSH 新连接限速,降低爆破风险。iptables -A INPUT -p tcp --dport "$SSH_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 6/minute --hashlimit-burst 12 \ --hashlimit-mode srcip --hashlimit-name ssh_v4_rate \ -j DROPiptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
# 服务端口优先只允许可信中继访问。iptables -A INPUT -s "${TRUSTED_RELAY_IP}/32" -p tcp --dport "$SERVICE_PORT" -j ACCEPTiptables -A INPUT -s "${TRUSTED_RELAY_IP}/32" -p udp --dport "$SERVICE_PORT" -j ACCEPT
# 不做公共 DNS,就明确阻断公网 DNS/mDNS/LLMNR。iptables -A INPUT -p tcp --dport 53 -j DROPiptables -A INPUT -p udp --dport 53 -j DROPiptables -A INPUT -p udp --dport 5353 -j DROPiptables -A INPUT -p udp --dport 5355 -j DROP
# 兜底:其它 TCP/UDP 入站全部丢弃。iptables -A INPUT -p tcp -j DROPiptables -A INPUT -p udp -j DROP如果服务明确不需要 UDP,就不要因为进程监听了 UDP socket 而默认放开。ss -lntup 只能说明进程在监听,不能说明公网真的能访问;最终还是防火墙规则说了算。
IPv6 必须同步维护
只写 iptables 不写 ip6tables,经常会造成 IPv4 很安全、IPv6 还裸露的情况。
IPv6 示例:
SSH_PORT=22SERVICE_PORT=51001TRUSTED_RELAY_V6="2001:db8::10"
ip6tables -P INPUT DROPip6tables -P FORWARD ACCEPTip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -i lo -j ACCEPTip6tables -A INPUT -m conntrack --ctstate INVALID -j DROPip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 不要粗暴封掉全部 ICMPv6;这里只挡 ping request。ip6tables -A INPUT -p ipv6-icmp --icmpv6-type echo-request -j DROPip6tables -A INPUT -p ipv6-icmp -j ACCEPT
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 6/minute --hashlimit-burst 12 \ --hashlimit-mode srcip --hashlimit-name ssh_v6_rate \ -j DROPip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
ip6tables -A INPUT -s "${TRUSTED_RELAY_V6}/128" -p tcp --dport "$SERVICE_PORT" -j ACCEPTip6tables -A INPUT -s "${TRUSTED_RELAY_V6}/128" -p udp --dport "$SERVICE_PORT" -j ACCEPT
ip6tables -A INPUT -p tcp --dport 53 -j DROPip6tables -A INPUT -p udp --dport 53 -j DROPip6tables -A INPUT -p udp --dport 5353 -j DROPip6tables -A INPUT -p udp --dport 5355 -j DROP
ip6tables -A INPUT -p tcp -j DROPip6tables -A INPUT -p udp -j DROP这里的关键是不要全封 ICMPv6。IPv6 的邻居发现、路径 MTU 等机制依赖 ICMPv6,粗暴 DROP 可能导致一些很难定位的网络问题。
OUTPUT:降低 abuse 风险
防滥用不只是入口,出口行为也很重要。一个私有代理节点即使只有自己用,也可能因为邮件、账号同步、自动化访问或历史 IP 信誉被标记。
最基础的做法是阻断邮件端口:
iptables -A OUTPUT -p tcp -m multiport --dports 25,110,143,465,587,993,995 -j REJECTiptables -A OUTPUT -p udp -m multiport --dports 25,110,143,465,587,993,995 -j REJECT
ip6tables -A OUTPUT -p tcp -m multiport --dports 25,110,143,465,587,993,995 -j REJECTip6tables -A OUTPUT -p udp -m multiport --dports 25,110,143,465,587,993,995 -j REJECT再阻断常见扫描/滥用端口:
iptables -A OUTPUT -p tcp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECTiptables -A OUTPUT -p udp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECT
ip6tables -A OUTPUT -p tcp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECTip6tables -A OUTPUT -p udp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECT更严格的策略是只允许 NTP 和指定 DNS 的 UDP 出站,其它 UDP 先限速记录日志再 DROP:
DNS1="9.9.9.9"DNS2="1.1.1.1"
iptables -A OUTPUT -p udp --dport 123 -j ACCEPTiptables -A OUTPUT -p udp -d "$DNS1" --dport 53 -j ACCEPTiptables -A OUTPUT -p udp -d "$DNS2" --dport 53 -j ACCEPT
iptables -A OUTPUT -p udp \ -m limit --limit 6/minute --limit-burst 20 \ -j LOG --log-prefix "V4_UDP_OUT_DROP " --log-level 4iptables -A OUTPUT -p udp -j DROP但这个策略不能无脑套用。如果你的 SS2022 节点需要 UDP relay,或者业务依赖 QUIC、WebRTC、游戏、实时音视频,出站 UDP 全 DROP 会直接影响可用性。更稳的做法是先观察日志,再按目的地址或端口放行。
查看 UDP drop:
journalctl -k --no-pager | grep -E 'UDP_OUT_DROP|DROP' | tail -50服务端口限流
限流不是安全的全部,只是异常流量的保险丝。
connlimit 限制同一来源 IP 的同时连接数,适合防止一个来源打满文件描述符或 conntrack 表。hashlimit 限制同一来源 IP 的新连接速率,适合防爆连接、异常重连或扫描式访问。
示例:给一个公网 TCP 服务端口加限流。
SERVICE_PORT=51001
iptables -I INPUT 1 -p tcp --dport "$SERVICE_PORT" \ -m connlimit --connlimit-above 2000 --connlimit-mask 32 --connlimit-saddr \ -j REJECT --reject-with tcp-reset
iptables -I INPUT 2 -p tcp --dport "$SERVICE_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 300/sec --hashlimit-burst 600 \ --hashlimit-mode srcip --hashlimit-name svc_v4_new_conn \ -j DROPIPv6 的 connlimit-mask 用 128:
SERVICE_PORT=51001
ip6tables -I INPUT 1 -p tcp --dport "$SERVICE_PORT" \ -m connlimit --connlimit-above 2000 --connlimit-mask 128 --connlimit-saddr \ -j REJECT --reject-with tcp-reset
ip6tables -I INPUT 2 -p tcp --dport "$SERVICE_PORT" \ -m conntrack --ctstate NEW \ -m hashlimit --hashlimit-above 300/sec --hashlimit-burst 600 \ --hashlimit-mode srcip --hashlimit-name svc_v6_new_conn \ -j DROP如果限流规则的 pkts/bytes 持续上涨,同时客户端出现断流或频繁重连,就说明阈值可能偏紧。可以先提高阈值,再考虑只保留 connlimit。
规则顺序和临时开放端口
如果 INPUT 链尾部已经有:
-A INPUT -p tcp -j DROP-A INPUT -p udp -j DROP就不要用 iptables -A INPUT 追加开放端口。追加出来的 ACCEPT 会排在 DROP 后面,通常不会生效。
临时开放 TCP 端口时,插到最终 DROP 前面:
PORT=8080V4_LINE=$(iptables -L INPUT -n --line-numbers | awk '$2=="DROP" && $3=="tcp" {line=$1} END{print line}')iptables -I INPUT "$V4_LINE" -p tcp --dport "$PORT" -j ACCEPTIPv6 同理:
PORT=8080V6_LINE=$(ip6tables -L INPUT -n --line-numbers | awk '$2=="DROP" && $3=="tcp" {line=$1} END{print line}')ip6tables -I INPUT "$V6_LINE" -p tcp --dport "$PORT" -j ACCEPT删除临时规则时按行号删,但每删一条都要重新查看行号:
iptables -L INPUT -n -v --line-numbersiptables -D INPUT <行号>变更和持久化流程
远程改防火墙时不要裸改。我的固定流程是:
tmux new -s fw
iptables-save > /root/iptables.manual-backup.$(date +%F-%H%M%S).v4ip6tables-save > /root/ip6tables.manual-backup.$(date +%F-%H%M%S).v6
# 应用你的规则脚本或手动变更。
# 新开 SSH 窗口验证,确认能登录、服务端口符合预期。netfilter-persistent savesystemctl enable netfilter-persistent如果 apply 后还没保存,优先回滚到备份:
iptables-restore < /root/iptables.manual-backup.YYYY-MM-DD-HHMMSS.v4ip6tables-restore < /root/ip6tables.manual-backup.YYYY-MM-DD-HHMMSS.v6如果已经把自己锁在门外,只能通过服务商控制台登录,临时清空规则救 SSH:
iptables -P INPUT ACCEPTiptables -P OUTPUT ACCEPTiptables -P FORWARD ACCEPTiptables -F
ip6tables -P INPUT ACCEPTip6tables -P OUTPUT ACCEPTip6tables -P FORWARD ACCEPTip6tables -F确认 SSH 恢复后,再重新应用正确规则。不要把“清空状态”保存成长期配置。
巡检清单
日常检查:
iptables -S INPUTiptables -S OUTPUTip6tables -S INPUTip6tables -S OUTPUT
iptables -L INPUT -n -v --line-numbersiptables -L OUTPUT -n -v --line-numbersip6tables -L INPUT -n -v --line-numbersip6tables -L OUTPUT -n -v --line-numbers
ss -lntupjournalctl -k --no-pager | grep -E 'UDP_OUT_DROP|DROP' | tail -50systemctl status netfilter-persistent --no-pager我主要看这些状态:
- IPv4 和 IPv6 的 INPUT 默认策略都是 DROP。
- SSH 有 hashlimit,新 SSH 窗口可以登录。
- 服务端口只放行必要来源,或至少有合理限流。
- 不需要的 UDP 入站被 DROP。
- 53、5353、5355 没有对公网开放。
- 邮件端口和常见扫描端口出站被 REJECT。
- 规则已保存到
/etc/iptables/rules.v4和/etc/iptables/rules.v6。
参考文档
这篇的公开参考只保留 Astro 内部文章和外部网页:
- IP 中转学习:从 DNAT、SNAT 到 nftables 单跳转发
- [[PO0 到 RFC 再到多落地:一次 nftables 转发链路学习记录]]
- NodeSeek SSH 加固教程
- Markdown PlantUML 图表示例
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!