最近我们的一个 k8s 组件的 NodePort 被扫描出漏洞,但是修复该漏洞比较麻烦,而且这个 NodePort 并不是必须的,所以我们打算直接使用 iptables 封禁对于这个 NodePort 的访问流量,结果却发现 iptables 的封禁规则无法生效。
使用 filter 表封禁不生效
我们的第一想法是在 iptables filter 表的 INPUT 链添加对于 NodePort 的 DROP 规则来拦截访问:
iptables -I INPUT -p tcp --dport 31002 -j DROP
但是却发现这个规则没有生效,对于 NodePort 访问并没有被拦截。
为了排除 DROP 规则自身的问题,我们用 nc
在本地起了一个服务监听 30146 端口:
nc -l -p 30146 <<<'{"status":"ok"}'
然后封禁它:
iptables -I INPUT -p tcp --dport 30146 -j DROP
再尝试访问:
curl localhost:30146
发现不通。这说明 DROP 规则是没问题的,那么对于 NodePort 不生效的原因可能是访问 NodePort 和访问本地进程的机制有所不同。
打开 iptables 日志
为了确认访问 NodePort 命中的具体 iptables 规则,需要打开 iptables 的日志,CentOS 的打开方式如下:
# load the nf_log_ipv4 kernel module
modprobe nf_log_ipv4
# use the nf_log_ipv4 logger for IPv4 traffic
sysctl net.netfilter.nf_log.2=nf_log_ipv4
# update /etc/rsyslog.conf to include config: kern.* /var/log/kern.log
vi /etc/rsyslog.conf
# restart rsyslog service
systemctl restart rsyslog
然后给 NodePort 添加 TRACE 规则:
iptables -t raw -I OUTPUT -p tcp --dport 31002 -j TRACE
再触发访问:
curl localhost:31002
随后便可在 /var/log/kern.log
中找到请求命中的全部 iptables 规则。我摘录了开头的部分,仅保留了 SRC、DST 和 DPT 三个重要字段,并把 SRC 和 DST 中出现的 ip 用 <ip1>
和 <ip2>
来表示:
Sep 30 10:04:04 [localhost] kernel: TRACE: raw:OUTPUT:policy:3 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: mangle:OUTPUT:policy:1 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:OUTPUT:rule:1 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:cali-OUTPUT:rule:1 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:cali-fip-dnat:return:1 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:cali-OUTPUT:return:2 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:OUTPUT:rule:2 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-SERVICES:rule:458 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-NODEPORTS:rule:64 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-SVC-EANZGUQV3HZVGERW:rule:2 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-MARK-MASQ:rule:1 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-MARK-MASQ:return:2 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-SVC-EANZGUQV3HZVGERW:rule:3 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: nat:KUBE-SEP-E6KGZ7LKAMZFQR62:rule:2 SRC=<ip1> DST=<ip1> DPT=31002
Sep 30 10:04:04 [localhost] kernel: TRACE: filter:OUTPUT:rule:1 SRC=<ip1> DST=<ip2> DPT=8200
可以看到 nat 表中有许多 k8s kube-proxy 生成的 KUBE 开头的链,其中 KUBE-SEP-E6KGZ7LKAMZFQR62 链中的规则对 DST 和 DPT 进行了 DNAT 操作,将节点 IP 和 NodePort 修改为了 pod IP 和端口。这也就解释了为何在 filter 表封禁 NodePort 不生效,因为请求命中 filter 表时端口已经被改为了 pod 端口。
在 DNAT 规则之前进行封禁
在了解了 iptables 处理 NodePort 访问的规则之后,解决这个问题的方法也就很明显了,只要在 DNAT 发生之前封禁 NodePort 即可。iptables 处理请求的流程图如下:
network -> PREROUTING -> routing decision -> INPUT -------> process
raw | mangle
mangle | filter
nat | nat
V
FORWARD
filter
|
|
V
process -> OUTPUT -----> POSTROUTING -> network
raw mangle
mangle nat
nat
filter
这与我们在日志中看到的规则命中顺序也是一致的。因此,可以在 mangle 表的 OUTPUT 链加上 DROP 规则:
iptables -t mangle -I OUTPUT -p tcp --dport 31002 -j DROP
然后再尝试访问 NodePort 就会发现终于不通了。
总结
k8s 的 kube-proxy 会通过给 iptables 添加 DNAT 规则来实现将 NodePort 的访问流量转发到 pod,所以如果想要封禁 NodePort,需要在 DNAT 规则之前进行封禁。比如,可以在 mangle 表的 OUTPUT 链加上 DROP 规则,来禁止本地访问 NodePort。