一、为什么"最小暴露面"比装什么防护都重要
大部分站点的入侵路径,事后回看都能归到一句话:某个端口本来不该对公网开着。不是防火墙挡住了攻击者,而是攻击者根本不需要"打进来"——他直接连上了你以为只有自己知道的端口。
我接手过的服务器里,最常见的一页安全组规则长这样:
- 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/32Redis:默认就是危险配置
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 是面板。
处置顺序:
- 先加密码,再收回监听。Redis 加
requirepass并rename-command CONFIG "",然后改bind 127.0.0.1重启。这里之所以先加密码,是为了万一重启失败、有人在我们处理的窗口期扫到,也不至于裸奔。 - MySQL 改 bind-address。改完从应用侧跑一轮完整的读写测试,确认没有遗漏的连接串写死公网 IP。这一步发现了一个后台导出脚本用的是公网 IP 连库,改成
127.0.0.1后正常。 - 面板加来源限制,然后安全组上删掉 8888 的公网放行,改为内网段。
- 删掉 9200 的规则,服务器上本来就没监听。
- 22 端口来源收窄到两个固定 IP,同时保留云控制台的 VNC 入口作为后路。
- 出方向加拒绝 25。
- 检查 IPv6,发现这台机器没有公网 IPv6,跳过。
- 检查 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 降了、备份也快了——因为每天有几十万个机器人正在替你消耗带宽。