跳至内容
返回

为什么用 iptables 封禁 k8s NodePort 不生效

发布于:  at  11:31 上午

最近我们的一个 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。



下一篇
不要修改 Java 中带有 @Cacheable 注解的方法参数