一条规则写在最前面,把后面的全挡了。这次云服务器安全组规则优先级的排查,起点不是报错信息,是控制台上那几行看着像队列的东西。
机器在杭州区,一台 ECS,对外接口只开了 22、80、443 三个口。
上周有两个地址段在扫这个接口的探测路径,日志里一分钟几十条 404,值班的人想把来源先挡掉。
规则加完,它在列表里排到了第一行。回车之后 443 全断,公司自己那条放行也一起失效。
第一反应是顺序问题:拒绝排在第一条,先命中先返回,后面的放行自然轮不到。
这个解释在 iptables 里是成立的。放在安全组上,它只对了一半。

一、现象里的第一个误导:列表位置
控制台的规则列表按优先级从小到大排,第一行就是数字最小的那条。
"排在最前面"和"优先级最高"在界面上长得一样。看的人容易把显示顺序当成因果关系。
真正决定谁先生效的是那个数字,列表只是把数字排了一遍。
这一点会带来一个很具体的偏差:改动方式变成拖顺序,而不是改数字。
拖到第一行,数字跟着变成 1。看起来位置变了,实际是数字变了。
那天加的这条 Drop 就是数字 1。它挡住后面的原因,一半在这个数字,另一半在源地址那一栏。
二、云服务器安全组规则优先级,在三层里指三件事
有优先级的层看数字,只有放行的层不看位置,iptables 看行号,三种判据不能互相搬。
安全组是挂在实例网卡外侧的一组有状态过滤规则,决定哪些包能被送到这块网卡。
"有状态"管的是回程,放过去的连接,回来的包自动放行,不必另写一条。"网卡外侧"管的是位置,包还没进协议栈,先在这里被筛一遍。
按这层的意思,规则之间要比出先后,靠的是优先级数字。数字小的先判,判中即停,后面的不再进入匹配。
阿里云的安全组走这个模型,入方向和出方向各一套,每条规则带一个 1 到 100 的优先级。
AWS 的安全组走另一个模型,只有允许,没有拒绝,所有规则一起评估,评估完给一个结论。
iptables 走第三个模型,规则按写入的行号排。第一条命中就返回,后面的不再看。
三种模型放在一起,"顺序"这个词就散了。它在第一层是数字,在第二层不存在,在第三层是行号。
三、把规则打成字段,位置不在里面
查出来的规则只有字段,没有位置这一项,界面上的先后是它按优先级排出来的结果。
一条规则在这层由三个字段决定:优先级、策略、源地址。端口和协议是筛选条件,不是判据。
aliyun ecs DescribeSecurityGroupAttribute \
--RegionId cn-hangzhou \
--SecurityGroupId sg-bp1a2b3c \
--Direction ingress --output json
# {
# "Permissions": {"Permission": [
# {"Priority": 1, "Policy": "Drop", "IpProtocol": "tcp",
# "PortRange": "443/443", "SourceCidrIp": "0.0.0.0/0"},
# {"Priority": 100, "Policy": "Accept", "IpProtocol": "tcp",
# "PortRange": "443/443", "SourceCidrIp": "203.0.113.0/24"},
# {"Priority": 100, "Policy": "Accept", "IpProtocol": "tcp",
# "PortRange": "22/22", "SourceCidrIp": "198.51.100.7/32"}
# ]}
# }
第一条是优先级 1 的 Drop,源写成 0.0.0.0/0,端口 443。
这个源覆盖所有地址,对 443 上的一切流量都成立,判断在第一行就结束了。
第二条是优先级 100 的 Accept,源 203.0.113.0/24,公司自己那条。它排在下一位,永远轮不到。
把 Drop 的源改成 198.51.100.9/32,优先级仍是 1,203.0.113.0/24 就能过去,被挡的只有那一个地址。
这一组对比把位置摘了出去。挡不挡,看的是那条规则匹配面有多大,与它写在第几行没有关系。
匹配面这个词在这里能量出来,源是 /32 就只管一个地址,源是 /0 就管全部。当晚那条规则的匹配面是全部,优先级又最小,两件事叠在一起才成了全断。
四、换到只有放行的那一层,这句话写不出来
同一句"挡掉某个来源",在没有拒绝规则的那一层没有对应写法。
项目另一边还有一台 AWS 的机器,同一份规则思路搬过去,那条 Drop 就没有地方可写。控制台的入方向里没有拒绝这一项,只有允许。
aws ec2 describe-security-groups \
--group-ids sg-0a1b2c3d4e \
--query 'SecurityGroups[].IpPermissions' --output json
# [
# [
# {"IpProtocol": "tcp", "FromPort": 443, "ToPort": 443,
# "IpRanges": [{"CidrIp": "203.0.113.0/24"}]},
# {"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22,
# "IpRanges": [{"CidrIp": "198.51.100.7/32"}]}
# ]
# ]
数组里只有协议、端口、源,没有策略字段,也没有优先级字段。
这一层能表达的只有允许谁进来。想挡住一段地址,做法是干脆不放行,而不是写一条拒绝。
"拒绝排在第一条"在这里无从发生,不是被排到了后面,是根本不存在这条规则。
网卡上挂多张组的时候,判据是并集。任意一张组放行,包就进来,组与组之间没有先后。
aws ec2 describe-network-interfaces \
--network-interface-ids eni-0a1b2c3d4e \
--query 'NetworkInterfaces[].Groups' --output json
# [
# [
# {"GroupName": "web-tier", "GroupId": "sg-0a1b2c3d4e"},
# {"GroupName": "bastion", "GroupId": "sg-0f6e5d4c3b"}
# ]
# ]
两张组挂在一起,暴露面等于两张组的放行范围相加。
组内规则之间同样没有先后,因为评估时全部一起看,不存在先命中这一说。
这一层能调的只有范围本身。收窄一条放行、少挂一张组,都是改集合,不是改位置。
五、行序真正生效的地方
行号是那种"写在前面就一定先生效"的判据,iptables 用的就是它。
iptables -S INPUT
# -P INPUT ACCEPT
# -A INPUT -p tcp -m tcp --dport 443 -j DROP
# -A INPUT -s 203.0.113.0/24 -p tcp -m tcp --dport 443 -j ACCEPT
# -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
第一条 DROP 443 写在最前,第二条对 203.0.113.0/24 的 ACCEPT 就永远收不到包。
两条的顺序反了才是原意。改动落在链上的位置,用 -I INPUT 2 插到第二条之前,规则内容一个字不用动。
这一层看起来最像队列,因为它就是按行号逐条走,命中即停。
团队里那句"第一条挡后面"大概来自这台机器的 iptables。同一个项目两种环境,两套判据,句子从一处搬到另一处,只剩一半成立。
同一句在两种层上的命运不同:有优先级的层靠数字仍成立,靠位置不成立,只有放行的层整句失效。
六、上线前怎么定规则
顺序这个词在定规则时要先落到判据上,判据不同,同一份意图的写法完全不同。
有优先级字段的层,规则集要按数字读,不是按界面顺序读。最宽的那条排在数字最大的一位,让它判在具体规则之后。
没有优先级字段的层,位置不表达任何意图,要挡一段地址只能把放行范围收窄到不含它。
同一台机器挂多张组、多张网卡时,暴露面按并集算。任意一处开着的口,就是对外开着的口。
规则落地前的那一遍核对,是把每条的源范围与端口抄成一行,与业务清单对,0.0.0.0/0 那几条单独看,它们决定的是整台机器的暴露面。
那天真正的问题在源那一栏,不在它排第几行。顺序是一个更容易被记住的替罪羊。
判断一套安全组规则是否可用,看的是每条规则的匹配面,以及它与业务清单对不对得上。列表里的名次不提供这个信息。
