VPS 安全加固教程

2832 字
14 分钟
VPS 安全加固教程
Abstract

这篇是 VPS 安全加固教程,重点放在 SSH 登录面和 iptables/ip6tables 主机防火墙。它和 nftables 转发篇的分工不同:nftables 负责 DNAT、SNAT、masquerade 这类转发 NAT;iptables/ip6tables 负责主机自己的 INPUT/OUTPUT 防护。文中所有 IP、端口、服务名都做了脱敏。

加固流程图#

下面这个流程图用 PlantUML 写。Firefly 支持 plantuml 代码块渲染,语法可以参考文末的 PlantUML 示例文章。

@startuml
start
:保留当前 SSH 会话;
:备份 SSH 和防火墙配置;
:创建非 root 管理用户;
:写入 SSH 公钥;
if (新用户密钥登录成功?) then (是)
  :开放 SSH 端口并加限速;
  :写入 sshd 加固配置;
  :执行 sshd -t;
  if (sshd 语法通过?) then (是)
    :reload ssh 服务;

为什么要先加固 SSH#

VPS 的第一安全边界通常是 SSH。防火墙规则写得再好,如果 SSH 还允许 root 密码登录、弱密码爆破、新端口没验证就直接保存,风险仍然很高。

我的顺序是:

  1. 先保证有一个非 root 用户可以用 SSH key 登录。
  2. 再收紧 sshd 配置。
  3. 然后用 iptables/ip6tables 限制 SSH 新连接速率。
  4. 最后才保存持久化规则。

远程操作时不要关闭当前 SSH 会话。所有改动都要开新窗口验证成功以后再继续。

SSH 登录加固#

先创建一个普通管理用户,下面用 ops 作为示例名:

Terminal window
adduser ops
usermod -aG sudo ops

在本地生成密钥,已有密钥可以跳过:

Terminal window
ssh-keygen -t ed25519 -a 100 -C "vps-ops"

把公钥写到服务器:

Terminal window
install -d -m 700 -o ops -g ops /home/ops/.ssh
nano /home/ops/.ssh/authorized_keys
chown ops:ops /home/ops/.ssh/authorized_keys
chmod 600 /home/ops/.ssh/authorized_keys

先在新窗口验证:

Terminal window
ssh ops@203.0.113.10
sudo -v

确认 ops 可以登录且能 sudo 后,再写 sshd 加固配置。建议用 drop-in 文件,方便回滚:

Terminal window
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 22
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers ops
ClientAliveInterval 300
ClientAliveCountMax 2
EOF

如果你暂时还没有稳定的非 root key 登录,不要设置 PermitRootLogin noPasswordAuthentication no。这两项应该最后再关。

检查语法并重载:

Terminal window
sshd -t
systemctl reload ssh || systemctl reload sshd

再新开窗口测试:

Terminal window
ssh ops@203.0.113.10

如果要改 SSH 端口,先在防火墙里放行新端口,并确认新端口可以登录,再关闭旧端口:

Terminal window
NEW_SSH_PORT=22222
iptables -I INPUT 1 -p tcp --dport "$NEW_SSH_PORT" -j ACCEPT
ip6tables -I INPUT 1 -p tcp --dport "$NEW_SSH_PORT" -j ACCEPT

确认登录成功后,再把 Port 22 改成新端口,执行 sshd -t 和 reload。

SSH 防爆破限速#

SSH 面向公网时,建议保留 hashlimit。它不会替代 SSH key,但可以降低爆破和异常重连带来的噪音。

IPv4:

Terminal window
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 DROP
iptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT

IPv6:

Terminal window
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 DROP
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT

如果你用了非 22 端口,记得同步修改 SSH_PORT。保存规则前一定开新窗口验证。

fail2ban 可选补充#

如果 SSH 仍然暴露公网,可以加 fail2ban 做日志级封禁:

Terminal window
apt update
apt install -y fail2ban
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
backend = systemd
port = 22
maxretry = 5
findtime = 10m
bantime = 1h
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd

如果 SSH 端口改了,port 也要一起改。

为什么还要写 iptables#

做 IP 中转时,很容易只盯着 nftables 转发规则:入口端口有没有 DNAT,回包有没有 masquerade,forward 链有没有放行。

但 VPS 暴露在公网,真正的风险不只在“转发是否可用”,还包括:

  • SSH 是否被爆破。
  • 服务端口是否全网开放。
  • UDP 是否无意暴露。
  • 53、5353、5355 是否变成公共 DNS/mDNS/LLMNR 入口。
  • 邮件端口、扫描端口是否被代理出口滥用。
  • IPv6 是否忘记同步配置。

所以我的分工是:

层级工具职责
转发 NATnftables中转链路的 DNAT、SNAT、masquerade
主机防火墙iptables/ip6tablesINPUT、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 示例:

Terminal window
SSH_PORT=22
SERVICE_PORT=51001
TRUSTED_RELAY_IP="198.51.100.10"
iptables -P INPUT DROP
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate INVALID -j DROP
iptables -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 DROP
iptables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
# 服务端口优先只允许可信中继访问。
iptables -A INPUT -s "${TRUSTED_RELAY_IP}/32" -p tcp --dport "$SERVICE_PORT" -j ACCEPT
iptables -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 DROP
iptables -A INPUT -p udp --dport 53 -j DROP
iptables -A INPUT -p udp --dport 5353 -j DROP
iptables -A INPUT -p udp --dport 5355 -j DROP
# 兜底:其它 TCP/UDP 入站全部丢弃。
iptables -A INPUT -p tcp -j DROP
iptables -A INPUT -p udp -j DROP

如果服务明确不需要 UDP,就不要因为进程监听了 UDP socket 而默认放开。ss -lntup 只能说明进程在监听,不能说明公网真的能访问;最终还是防火墙规则说了算。

IPv6 必须同步维护#

只写 iptables 不写 ip6tables,经常会造成 IPv4 很安全、IPv6 还裸露的情况。

IPv6 示例:

Terminal window
SSH_PORT=22
SERVICE_PORT=51001
TRUSTED_RELAY_V6="2001:db8::10"
ip6tables -P INPUT DROP
ip6tables -P FORWARD ACCEPT
ip6tables -P OUTPUT ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -m conntrack --ctstate INVALID -j DROP
ip6tables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 不要粗暴封掉全部 ICMPv6;这里只挡 ping request。
ip6tables -A INPUT -p ipv6-icmp --icmpv6-type echo-request -j DROP
ip6tables -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 DROP
ip6tables -A INPUT -p tcp --dport "$SSH_PORT" -j ACCEPT
ip6tables -A INPUT -s "${TRUSTED_RELAY_V6}/128" -p tcp --dport "$SERVICE_PORT" -j ACCEPT
ip6tables -A INPUT -s "${TRUSTED_RELAY_V6}/128" -p udp --dport "$SERVICE_PORT" -j ACCEPT
ip6tables -A INPUT -p tcp --dport 53 -j DROP
ip6tables -A INPUT -p udp --dport 53 -j DROP
ip6tables -A INPUT -p udp --dport 5353 -j DROP
ip6tables -A INPUT -p udp --dport 5355 -j DROP
ip6tables -A INPUT -p tcp -j DROP
ip6tables -A INPUT -p udp -j DROP

这里的关键是不要全封 ICMPv6。IPv6 的邻居发现、路径 MTU 等机制依赖 ICMPv6,粗暴 DROP 可能导致一些很难定位的网络问题。

OUTPUT:降低 abuse 风险#

防滥用不只是入口,出口行为也很重要。一个私有代理节点即使只有自己用,也可能因为邮件、账号同步、自动化访问或历史 IP 信誉被标记。

最基础的做法是阻断邮件端口:

Terminal window
iptables -A OUTPUT -p tcp -m multiport --dports 25,110,143,465,587,993,995 -j REJECT
iptables -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 REJECT
ip6tables -A OUTPUT -p udp -m multiport --dports 25,110,143,465,587,993,995 -j REJECT

再阻断常见扫描/滥用端口:

Terminal window
iptables -A OUTPUT -p tcp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECT
iptables -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 REJECT
ip6tables -A OUTPUT -p udp -m multiport --dports 23,2323,445,3389,5900,6379,9200,11211,2375 -j REJECT

更严格的策略是只允许 NTP 和指定 DNS 的 UDP 出站,其它 UDP 先限速记录日志再 DROP:

Terminal window
DNS1="9.9.9.9"
DNS2="1.1.1.1"
iptables -A OUTPUT -p udp --dport 123 -j ACCEPT
iptables -A OUTPUT -p udp -d "$DNS1" --dport 53 -j ACCEPT
iptables -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 4
iptables -A OUTPUT -p udp -j DROP

但这个策略不能无脑套用。如果你的 SS2022 节点需要 UDP relay,或者业务依赖 QUIC、WebRTC、游戏、实时音视频,出站 UDP 全 DROP 会直接影响可用性。更稳的做法是先观察日志,再按目的地址或端口放行。

查看 UDP drop:

Terminal window
journalctl -k --no-pager | grep -E 'UDP_OUT_DROP|DROP' | tail -50

服务端口限流#

限流不是安全的全部,只是异常流量的保险丝。

connlimit 限制同一来源 IP 的同时连接数,适合防止一个来源打满文件描述符或 conntrack 表。hashlimit 限制同一来源 IP 的新连接速率,适合防爆连接、异常重连或扫描式访问。

示例:给一个公网 TCP 服务端口加限流。

Terminal window
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 DROP

IPv6 的 connlimit-mask128

Terminal window
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 链尾部已经有:

Terminal window
-A INPUT -p tcp -j DROP
-A INPUT -p udp -j DROP

就不要用 iptables -A INPUT 追加开放端口。追加出来的 ACCEPT 会排在 DROP 后面,通常不会生效。

临时开放 TCP 端口时,插到最终 DROP 前面:

Terminal window
PORT=8080
V4_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 ACCEPT

IPv6 同理:

Terminal window
PORT=8080
V6_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

删除临时规则时按行号删,但每删一条都要重新查看行号:

Terminal window
iptables -L INPUT -n -v --line-numbers
iptables -D INPUT <行号>

变更和持久化流程#

远程改防火墙时不要裸改。我的固定流程是:

Terminal window
tmux new -s fw
iptables-save > /root/iptables.manual-backup.$(date +%F-%H%M%S).v4
ip6tables-save > /root/ip6tables.manual-backup.$(date +%F-%H%M%S).v6
# 应用你的规则脚本或手动变更。
# 新开 SSH 窗口验证,确认能登录、服务端口符合预期。
netfilter-persistent save
systemctl enable netfilter-persistent

如果 apply 后还没保存,优先回滚到备份:

Terminal window
iptables-restore < /root/iptables.manual-backup.YYYY-MM-DD-HHMMSS.v4
ip6tables-restore < /root/ip6tables.manual-backup.YYYY-MM-DD-HHMMSS.v6

如果已经把自己锁在门外,只能通过服务商控制台登录,临时清空规则救 SSH:

Terminal window
iptables -P INPUT ACCEPT
iptables -P OUTPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -F
ip6tables -P INPUT ACCEPT
ip6tables -P OUTPUT ACCEPT
ip6tables -P FORWARD ACCEPT
ip6tables -F

确认 SSH 恢复后,再重新应用正确规则。不要把“清空状态”保存成长期配置。

巡检清单#

日常检查:

Terminal window
iptables -S INPUT
iptables -S OUTPUT
ip6tables -S INPUT
ip6tables -S OUTPUT
iptables -L INPUT -n -v --line-numbers
iptables -L OUTPUT -n -v --line-numbers
ip6tables -L INPUT -n -v --line-numbers
ip6tables -L OUTPUT -n -v --line-numbers
ss -lntup
journalctl -k --no-pager | grep -E 'UDP_OUT_DROP|DROP' | tail -50
systemctl 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 内部文章和外部网页:

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

VPS 安全加固教程
https://blog.idotcar.top/posts/vps-security-hardening/
作者
老鼠溺水
发布于
2026-05-11
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
老鼠溺水
事实上,我们每个人都不过是在给自己写信。
分类
标签
站点统计
文章
7
分类
2
标签
9
总字数
29,837
运行时长
0
最后活动
0 天前

目录