云服务器安全组规则收敛:从端口清点到最小暴露面

一、为什么"最小暴露面"比装什么防护都重要

大部分站点的入侵路径,事后回看都能归到一句话:某个端口本来不该对公网开着。不是防火墙挡住了攻击者,而是攻击者根本不需要"打进来"——他直接连上了你以为只有自己知道的端口。

我接手过的服务器里,最常见的一页安全组规则长这样:

  • 22,来源 0.0.0.0/0("方便,哪都能登")
  • 3306,来源 0.0.0.0/0("本地连不上数据库,先开开看")
  • 6379,来源 0.0.0.0/0(Redis,装完默认就这样)
  • 888,来源 0.0.0.0/0(面板端口)
  • 27017、9200、11211 各一条("试过,后来不用了")

这五条里,除了 22 有较强理由,其他每一条都是一个独立的入侵面。Redis 未授权、MongoDB 未授权、Elasticsearch 未授权,这些攻击已经自动化到不需要人参与,全网扫描器几分钟就能找到你。安全性跟"用了什么高级防护"关系不大,跟"暴露了多少东西"关系极大。

所以安全组规则收敛这件事,本质不是"加规则",而是删规则、收来源、拆分组。这一篇讲的是我怎么在一台已经跑了两三年的云服务器上做这件事,以及怎么在不停机的前提下验证。

二、先把现状画出来,不要直接改

云控制台里的安全组规则只是"配置面上的意图",服务器里实际监听哪些端口才是"事实"。两者经常对不上:控制台关了 3306,但 MySQL 早就改成监听 0.0.0.0 的 3307;或者控制台开着 8080,而那个服务三个月前就删了。

所以第一步是在服务器上打开端口视图,而不是在控制台里改。

实际监听清单

# 只看监听态,只保留 TCP,显示进程
ss -tlnp

# 如果 ss 版本不支持 -p 的详细信息,用这个兜底
netstat -tlnp 2>/dev/null | grep LISTEN

# 只看非本机回环的(这些才是"对外"的候选)
ss -tlnp | awk 'NR>1 && $4 !~ /^127\.|^\[::1\]/ {print}'

注意 ss 输出的第四列是本地地址,格式可能是 0.0.0.0:3306、*:3306、[::]:3306 或 127.0.0.1:3306。前三种都是监听所有网卡,最后一种只监听本机。判断"对外"就是判断它不是回环地址。

这一步会得到一个真实的端口列表,比如:

0.0.0.0:22      sshd
0.0.0.0:80      nginx
0.0.0.0:443     nginx
127.0.0.1:3306  mysqld
127.0.0.1:6379  redis-server
0.0.0.0:8888    baota-panel

与控制台规则做差集

把这两份列表对照,会得到三类差异,每一类的处置都不同:

  • 实际在监听、控制台没开:这个端口现在从公网连不通。如果业务依赖它,说明有别的路径在用(内网、CDN 回源、其他安全组),要先搞清楚再动。
  • 控制台开着、实际没监听:纯冗余规则,直接删。这类是收敛的主要收益,删了没有任何风险。
  • 两边都有,但不该对公网:这是真正要处理的风险项。典型就是面板端口、数据库端口。

这一步做完,你手里就有一张"要删的清单"和一张"要改的清单"。改之前记得先确认自己还有一条能进得去的路(比如已登录的 SSH 会话保持不断开,或者云控制台的 VNC 救援入口可用),否则改完把自己关在外面是常见事故。

三、把服务先收回到内网,再收规则

顺序非常重要:先把监听地址收回到 127.0.0.1,再关安全组端口。反过来做,中间会有一段"端口开了但服务拒绝连接"的窗口,同时你的监控和健康检查会误报,排查噪声很大。

数据库:只监听回环

MySQL/MariaDB 的默认配置经常是 bind-address = 0.0.0.0 或者干脆注释掉(等价于监听全部)。改成回环:

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 127.0.0.1
# 防止跳过 DNS 解析造成连接延迟
skip-name-resolve

改完重启,再用 ss -tlnp | grep 3306 确认变成 127.0.0.1:3306。skip-name-resolve 那一行不是安全项,但很实用:它让 MySQL 不再对每个连接做反向 DNS,连接建立时间通常能降几十毫秒;代价是授权表里不能再用主机名,只能写 IP 或 localhost。

如果确实有另一台机器要连数据库(比如备份机、另一个 Web 节点),不要开公网。走内网私有地址,并在安全组里只放行那台机器的内网 IP:

# 只允许内网备份机访问 3306
allow 3306 from 172.16.0.15/32

Redis:默认就是危险配置

Redis 默认 bind 127.0.0.1 加 protected-mode yes,看起来是安全的。但很多人为了"远程调试"把 protected-mode 关了,或者把 bind 改成 0.0.0.0,又没有设 requirepass。这个组合是全网扫描器的最爱:未授权访问拿到的不仅是数据,还可以用 CONFIG SET dir 加 SAVE 写 SSH 公钥进去,直接拿到 root。

正确姿势是三层同时成立:

  • bind 127.0.0.1 且保留 protected-mode yes
  • 设置强密码并写进应用配置,不要用 requirepass 123456
  • 把 CONFIG、DEBUG、FLUSHALL 这类命令在 rename-command 里改名或禁用
# /etc/redis/redis.conf
bind 127.0.0.1
protected-mode yes
requirepass 一个足够长的随机串
rename-command CONFIG ""
rename-command FLUSHALL ""
rename-command DEBUG ""

把 CONFIG 设为空字符串是"彻底禁用"。如果你确实需要在线改参数,改成别的名字(如 CONFIG_9f3a),至少让自动化扫描器猜不到。

面板端口:换端口不等于安全

把面板从 888 改成 38471,只是躲开了默认扫描。真正要做的还是两条:来源限定到自己的 IP,以及面板本身不要挂在公网。

如果家里或公司是动态 IP,来源限死会经常变。折中做法是:面板只允许内网访问,公网通过跳板机或 VPN 进来。我一般这样收:

# 面板只允许本机和内网
listen 38471;
allow 127.0.0.1;
allow 172.16.0.0/16;
deny all;

放在 Nginx 的 server 块里生效。注意 allow/deny 按顺序匹配,deny all 必须在最后,写成前面那条 allow 之后的兜底。

四、安全组本身的三个收敛动作

服务收回到内网之后,安全组就变成"薄薄一层"。这时候做三件事。

动作一:按角色拆安全组,不要一个大组管所有

很多人的做法是"所有机器一个安全组",规则条目堆到几十条,谁也说不清哪条是给谁的。云厂商建议按层拆分:Web 层只开 80/443,应用层只开内部端口,数据层干脆不挂公网。

拆开之后的好处是规则变短、变更影响面可预测。一台 Web 服务器加规则,不会顺带把数据层的暴露面也改了。

动作二:入方向按"默认拒绝 + 白名单"写

安全组本身是白名单语义(没匹配的默认拒绝),但很多人用"加一条拒绝 0.0.0.0/0 的 3306"来收口。这条规则在安全组内的行为容易和直觉不符:它管的是入方向流量,且当你同时有"允许某内网访问 3306"的规则时,两条的优先级关系取决于云厂商实现(多数是按规则优先级数值判断,不是按添加顺序)。

更稳的写法是干脆不加"拒绝"规则,而是只写"允许",把不需要的端口从允许列表里彻底删掉。少一条规则,就少一次优先级判断的困惑。

# 收敛后的入方向,只有这几条
80   0.0.0.0/0        HTTP
443  0.0.0.0/0        HTTPS
22   家里固定 IP/32    SSH
22   公司固定 IP/32    SSH
38471 内网段/24        面板(走 VPN 时)

注意 22 那两条:来源是具体 IP 而不是 0.0.0.0/0。这条改动通常带来的最大回报——全网每天有几十万次 22 端口的暴力破解尝试,屏蔽掉之后 /var/log/auth.log 会安静得让人不习惯。

动作三:出方向也要管,但只做最基本的

出方向默认全放行是常见配置,风险在于服务器被植入后门后,恶意进程能主动连回 C2(命令与控制)服务器。全量收口成本很高(要放行云厂商的监控、镜像源、时间同步、各种 API),单人维护的站点不值得做到很细。

但有一条低成本规则非常值得加:禁止出方向的 25 端口。绝大多数站点不需要自己发邮件,而 25 端口是垃圾邮件发送和邮件伪造的主要出口。真需要发信就走 587/465 加认证,或者用第三方邮件服务。

# 出方向拒绝 25,其余放行
deny 25/tcp 0.0.0.0/0

这条规则最大的价值不在安全,而在"发现问题":如果某天你的站点突然大量连 25 端口失败,说明有东西在尝试发垃圾邮件(多数是 PHP 表单被利用、或者被人塞了群发脚本)。日志里出现成批的 connection refused to port 25,是最早的入侵信号之一。

五、收完规则不等于收完暴露面

安全组是"网关上的一层"。它管不到服务器内部的转发、也管不到 IPv6、更管不到那些绕过网关的路径。收敛完规则之后,还要做这几项检查。

检查一:本机防火墙是否被绕过

安全组在云网关上生效,服务器本机可能还跑着 iptables/nftables/firewalld。两者叠加时,"安全组开着、本机也开着"才真的能连通;而"安全组关了、本机开着"则完全不通。反过来说,如果本机防火墙规则里有一条 ACCEPT 且顺序在 DROP 之前,你以为挡住的东西其实是通的。

# 看 nftables 完整规则
nft list ruleset

# 看 iptables(老系统)
iptables -S
iptables -t nat -S

# firewalld
firewall-cmd --list-all

重点看 nat 表的 PREROUTING。有些面板会把 80 转发到 8080、或者把 443 转发到别的内部端口,这些转发会让"看起来关了的端口"其实可达。

检查二:IPv6 是一条独立的路

这是最容易被忽略的一条。安全组规则只覆盖 IPv4 时,服务器如果同时有公网 IPv6 地址,那么 3306 在 IPv4 上不可达,在 IPv6 上可能完全敞开。

# 看是否有公网 IPv6
ip -6 addr show scope global

# 从外部用 IPv6 试(在另一台机器上执行)
nc -6 -zv 你的IPv6地址 3306

处置两个方向:要么在云控制台给安全组补 IPv6 规则(来源 ::/0 只允许 80/443,其余 ::/0 全拒),要么在服务器上直接禁用 IPv6(net.ipv6.conf.all.disable_ipv6=1),后者对不需要 IPv6 的站点更省事、更彻底。

检查三:Docker 会自己写 iptables

只要装了 Docker,-p 发布的端口就会自动插入 iptables 的 DOCKER 链,而且这条链通常在 INPUT 的拒绝规则之前。结果是:你在本机防火墙里把 8080 关了,容器发布的 8080 依然对外可达。

# 看 Docker 插进去的规则位置
iptables -t nat -L DOCKER -n -v
iptables -L DOCKER -n -v

# 更好的做法:只发布到回环
docker run -d -p 127.0.0.1:8080:80 your-image

第三种写法是关键:-p 127.0.0.1:8080:80 让 Docker 只监听回环,外层用 Nginx 反代。这样容器端口永远不会出现在公网监听列表里,也就不依赖任何人记得去关它。

顺带一个 Docker 的坑:iptables=false 加 --userland-proxy=false 的组合看起来"干净",但会让端口发布完全依赖外部防火墙配置,一旦某个规则写漏,暴露面立刻扩大。新手不要碰这两个开关。

六、一次真实收敛的复盘

说一个具体的。一台跑了两年多的阿里云 ECS,Ubuntu 22.04,跑着一个 Typecho 站点加一个自建的小 API。老板给的诉求是"最近日志里好多奇怪的连接尝试,帮我看看"。

打开控制台,安全组入方向 11 条规则,其中 22、3306、6379、8888、9200 都是 0.0.0.0/0。服务器上 ss -tlnp 的实际结果是:MySQL 监听 0.0.0.0:3306,Redis 监听 0.0.0.0:6379 且没有密码,9200 没在监听(Elasticsearch 早卸了),8888 是面板。

处置顺序:

  1. 先加密码,再收回监听。Redis 加 requirepass 并 rename-command CONFIG "",然后改 bind 127.0.0.1 重启。这里之所以先加密码,是为了万一重启失败、有人在我们处理的窗口期扫到,也不至于裸奔。
  2. MySQL 改 bind-address。改完从应用侧跑一轮完整的读写测试,确认没有遗漏的连接串写死公网 IP。这一步发现了一个后台导出脚本用的是公网 IP 连库,改成 127.0.0.1 后正常。
  3. 面板加来源限制,然后安全组上删掉 8888 的公网放行,改为内网段。
  4. 删掉 9200 的规则,服务器上本来就没监听。
  5. 22 端口来源收窄到两个固定 IP,同时保留云控制台的 VNC 入口作为后路。
  6. 出方向加拒绝 25。
  7. 检查 IPv6,发现这台机器没有公网 IPv6,跳过。
  8. 检查 Docker,发现有一个容器用了 -p 0.0.0.0:8080:80,改成回环发布,外层加了一条 Nginx 反代。

做完之后,ss -tlnp 的对外监听只剩 22、80、443 三项(面板改到内网)。auth.log 里 22 的失败登录记录从每天几千条降到个位数。整个过程没有停机,每一步都有单独的验证。

有一个细节值得记住:那天在检查 /var/log/auth.log 时,发现除了暴力破解,还有几行是 Accepted password for root 来自一个陌生 IP。这已经不是"暴露面"问题,而是"凭证已泄露"。后来查明是面板的一个弱密码被撞库。这条发现比收敛规则本身重要得多——收敛暴露面能让攻击者找不到门,但如果门已经被人进过,收敛只是把痕迹盖住,不解决根本问题。所以每次收敛完,都要顺手做一次登录审计:

# 成功登录的历史,重点看陌生 IP 和非预期用户
grep 'Accepted' /var/log/auth.log* | awk '{print $1,$2,$3,$9,$11}' | sort | uniq -c | sort -rn | head -20

# 新增的用户和提权记录
grep -E 'new user|useradd|sudo:.*COMMAND' /var/log/auth.log* | tail -30

七、把它做成月度动作

暴露面是会自己长回来的。装个监控开了新端口、临时调试开了个规则忘了关、换服务商顺手全开一次——这些都在几个月内把收敛成果还给市场。所以我把它固定成每月一次,二十分钟能做完:

  • 对账:ss -tlnp 的对外监听,与控制台入方向规则做差集,多出来的一律处置
  • 看来源:所有 0.0.0.0/0 的条目逐条问"这条为什么需要全网",答不出来的就收窄
  • 查 IPv6:ip -6 addr show scope global 有输出就必须有对应规则
  • 查 Docker:docker ps --format 看有没有 0.0.0.0: 开头的端口映射
  • 查登录:成功登录的 IP 列表、新增用户、sudo 记录,三样各看一次
  • 留记录:这次删了哪条、为什么,写进笔记

最后一条比看起来重要。安全组规则收敛最怕的是"删了之后半年发现某个业务不通",而没有记录就无从追溯。一行"2026-11-03 删除 9200 全放行,Elasticsearch 已于 2025-08 下线",能省掉未来一次长达一天的排查。

八、小结:收敛的三条判据

这个题目讲到底就三句话:

  • 对外监听应该短到能用眼睛数清。一台 Web 服务器只需要 22/80/443,加上管理和内网端口,超过七八项就该重新审视。
  • 能收回到回环的,就不要收回到内网;能收到内网的,就不要留公网。层层往回退,每退一层就少一整类攻击者。
  • 规则是意图,监听是事实,两者必须每月对一次账。只改一项、不做对账,等于自我安慰。

安全这件事在个人站点上很难做出"专业感",但暴露面收敛是少数几个投入产出比极高的动作:不需要买设备、不需要懂渗透、不需要改代码,只是把不该开的东西关掉。做完之后你会发现,日志安静了、CPU 降了、备份也快了——因为每天有几十万个机器人正在替你消耗带宽。

Last modification:November 3rd, 2026 at 08:26 pm

Leave a Comment