源地址审计:追踪 kubernetes 服务的SNAT
by 伊布
现象:无法审计的客户端地址
对于运行在kubernetes上、且允许从集群外访问的应用来说,使用nodePort是一个不错的方案。客户端可以使用集群任一节点+nodePort来访问,如果再配置一个vip,就更方便了。
但nodePort有一个问题是,在容器中运行的应用,无法正确的审计客户端的ip地址:容器中看到的socket连接的源地址,总是容器所在node的网桥cni0的地址。
举个例子。
先创建一个nginx应用。
kind: ReplicationController
apiVersion: v1
metadata:
name: nginx-controller
spec:
replicas: 1
selector:
component: nginx
template:
metadata:
labels:
component: nginx
spec:
containers:
- name: nginx
image: docker.ieevee.com/ieevee/nginx:1.11
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
component: nginx
上面的编排文件将创建一个简单的80端口的nginx服务。k8s集群外的客户端,可以通过http://{node ip}/30080
来访问nginx服务。
当集群外客户端访问nginx服务时,观察下nginx容器中的socket连接,可以看到,源地址、目的地址都做了转换,即同时做了DNAT和SNAT。
ss -t -a
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:http *:*
ESTAB 0 0 10.244.3.82:http 10.244.3.1:35410
显然,这样对容器内审计是很不友好的。
原因:不得不做的SNAT
为什么会这样呢?
原因是,为了支持从任一节点IP+NodePort都可以访问应用。
client
\ ^
\ \
v \
node 1 <--- node 2
| ^ SNAT
| | --->
v |
endpoint
假设跳过上图的SNAT,只做DNAT。报文的确可以经过 node 2 转发到 node 1上的endpoint,但是endpoint如何应答呢?endpoint内的确有默认路由指向 node 1,但是如果没有做SNAt,应答时会直接从 node 1 发送给client。
这可是一个三角流量啊!
跳过SNAT后,node 1 在转发应答流量的时候,会将应答报文的源地址替换为 node 1的地址,这样的报文,client是不会接受的,连接将无法建立。
所以,必须要做SNAT,必须要FULLNAT。
实现:iptables
明白了为什么要SNAT,来看下如果通过iptables来实现SNAT。
下面是nginx-service创建以后的iptables规则,他们在k8s集群上任一Node上完全一致。
*nat
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING /* 1 */
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000 /* 2 */
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:nginx-http" -m tcp --dport 30080 -j KUBE-MARK-MASQ /* 3 */
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:nginx-http" -m tcp --dport 30080 -j KUBE-SVC-LU77NTQGUFTDG7JB /* 4 */
-A KUBE-SVC-LU77NTQGUFTDG7JB -m comment --comment "default/nginx-service:nginx-http" -j KUBE-SEP-UCGM5KC3U2QSWPEY /* 5 */
-A KUBE-SEP-UCGM5KC3U2QSWPEY -s 10.244.3.82/32 -m comment --comment "default/nginx-service:nginx-http" -j KUBE-MARK-MASQ /* 6 */
-A KUBE-SEP-UCGM5KC3U2QSWPEY -p tcp -m comment --comment "default/nginx-service:nginx-http" -m tcp -j DNAT --to-destination 10.244.3.82:80 /* 7 */
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE /* 8 */
DNAT在谈谈kubernets的service组件的Virtual IP中有介绍,对应第4/5/6/7条;而SNAT对应第1/2/3/8。
DNAT:对于收到的目的端口30080的报文,首先匹配第2条规则,该规则会让报文继续经过第1条规则处理;而第1条规则会为报文设置mark 0x4000/0x4000;之后报文继续经过3/4/6,做DNAT,将报文的目的地址转为10.244.3.82:80;
SNAT:发生在POSTROUTING阶段。报文接收时,打了mark 0x4000/0x4000,因此在KUBE-POSTROUTING时会匹配第8条规则(-m mark –mark 0x4000/0x4000),MASQUERADE 的意思就是做SNAT,源地址自动选择,此时客户端的IP就被替换为了cni0的地址。
linux上通过/proc/net/nf_conntrack
查看iptables表项(或者称之为iptables会话)。
cat /proc/net/nf_conntrack | grep 30080
ipv4 2 tcp 6 86397 ESTABLISHED src=10.84.1.138 dst=192.168.128.149 sport=39452 dport=30080 src=10.244.3.83 dst=10.244.3.1 sport=80 dport=39452 [ASSURED] mark=0 zone=0 use=2
解决方法:externalTrafficPolicy,跳过SNAT
显然,之所以会在POSTROUTING阶段组SNAT,是因为k8s给报文打了mark(其实是打在内核里的skb上,报文并没有做任何修改),那么要跳过SNAT,很容易想到的就是只要删掉第三条规则(-j KUBE-MARK-MASQ)就可以了。
这个目标可以通过kubernetes的externalTrafficPolicy来实现。externalTrafficPolicy
的目的是保留报文的源地址,其方法也就是我们希望的跳过SNAT。
将service.spec.externalTrafficPolicy
设置为Local
(v1.7 later)之后,kubernetes将在Pod所在Node上针对nodePort下发DNAT规则,而在其他节点上针对nodePort下发DROP规则。
client
^ / \
/ / \
/ v X
node 1 node 2
^ |
| |
| v
endpoint
还是nginx的例子。
在svc的编排文件中增加externalTrafficPolicy后,重新创建nginx-service。
kind: Service
apiVersion: v1
metadata:
name: nginx-service
spec:
type: NodePort
externalTrafficPolicy: Local
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
component: nginx
Pod所在Node下发的规则:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:" -m tcp --dport 30080 -j KUBE-XLB-GKN7Y2BSGW4NJTYL
-A KUBE-XLB-GKN7Y2BSGW4NJTYL -m comment --comment "Balancing rule 0 for default/nginx-service:" -j KUBE-SEP-64YUZF3BIP6Q2HFU
-A KUBE-SEP-64YUZF3BIP6Q2HFU -s 10.244.3.83/32 -m comment --comment "default/nginx-service:" -j KUBE-MARK-MASQ
-A KUBE-SEP-64YUZF3BIP6Q2HFU -p tcp -m comment --comment "default/nginx-service:" -m tcp -j DNAT --to-destination 10.244.3.83:80
非Pod所在Node下发的规则:
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/nginx-service:" -m tcp --dport 30080 -j KUBE-XLB-GKN7Y2BSGW4NJTYL
-A KUBE-XLB-GKN7Y2BSGW4NJTYL -m comment --comment "default/nginx-service: has no local endpoints" -j KUBE-MARK-DROP
此时从k8s容器外访问nginx服务,在nginx容器内查看socket连接,可以看到对端已经是真实的客户端IP了。
ss -t -a
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:http *:*
ESTAB 0 0 10.244.3.82:http 10.84.1.138:34748
显然,客户端只能使用 Pod所在Node的IP地址 + nodePort来访问Pod内的应用。
externalTrafficPolicy
从v1.5开始支持,v1.5 ~ v1.6 版本以Annotations形式支持(叫做external-traffic
),v1.7已经正式进入spec了。
在v1.5 ~ v1.6版本上需要这样来创建externalTrafficPolicy
:
kind: Service
apiVersion: v1
metadata:
name: nginx-service
annotations:
service.beta.kubernetes.io/external-traffic: OnlyLocal
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
component: nginx
externalTrafficPolicy
的好处是,Pod内应用的确可以拿到真实的客户端地址了,但坏处是,只能某一node的地址,无法使用vrrp之类的virtual IP来实现ha,对客户端来说会麻烦一点。
Ref:
Subscribe via RSS