從上這篇文章中我們知道IP地址是網絡通信中非常重要的信息,因此對于容器實例來說,為了被訪問到或者訪問其他服務,要么有自己獨立的IP地址,要么和宿主機共享相同的IP地址。特別是對于前者,由于容器實例有自己獨享的網絡命名空間,因此容器實例有獨享的網絡棧,端口號就不會和宿主機沖突,隔離性得到了極大的提升。從原理上看,Docker提供了完整的容器網絡方案,但是Kubernetes在Docker的基礎上進行了演進,因此咱們?yōu)榱饲泻蠈嶋H場景,直接先看看在Kubernetes中,IP地址是如何管理和分配的。
在Kubernetes平臺上,每個POD都會分配獨立的IP地址,并且如果POD中包含了多個容器應用,那么同一個POD中的所有容器實例共享POD的IP地址,背后的原理也不難理解,讀過筆者關于Kubernetes系列文章的同學,應該能脫口而出:因為運行在相同POD中的多個容器實例共享相同的網絡命名空間。作為集群的運維管理人員,我們在講某個Node(主機,宿主機)加入到集群的時候,會配置Node可以使用的IP地址范圍,專業(yè)的術語叫CIDR地址塊,那么當調度器(Sheduler)基于自己的調度算法將某個POD調度到主機之后,POD會從分配給Node的這個CIDR地址塊獲取到一個可用的IP地址。
注:筆者上邊的描述進行了簡化和抽象,實際上在有些托管環(huán)境中,可能并總是給Node分配一塊連續(xù)的IP地址供使用(CIDR),比如在AWS中,IP地址分配是通過一個插架的方式提供。
另外在Kubernetes平臺上,集群中的POD之間可直接進行通信而不需要做依賴于NAT機制(Network Address Translation),用大白話說NAT就是:目標IP地址可以是某個固定的IP地址,即便是提供服務的POD持有的IP地址和這個固定IP地址之間存在映射關系。另外在Kubernetes中,我們還可以通過網絡策略(Network policies)機制來約束POD之間的可訪問性。
說到Kubernertes中的POD服務訪問的問題,就不能不提Service對象,本質上來說,Service對象就是一種NAT的具體實現,Service對象是Kuberntes提供的對象資源,有自己獨享的IP地址,從這個角度看,Service也就僅僅是一個IP地址而已。因為Service對象沒有具體的接口,也沒有監(jiān)聽的概念,并且Service的IP地址主要被用來做服務調用的負載均衡之用。具體來說Service對象身后有多個POD提供具體的服務,因此當我們的訪問請求到達Service對象的IP地址后,需要轉發(fā)給身后的某個POD來進行實際的處理,我們把這種基于Layer 3的負載均衡叫做3層路由,咱們稍后會詳細介紹實現原理。
在傳統(tǒng)的組網方案中,我們一般通虛擬局域網(vlan)來對不同業(yè)務目標的系統(tǒng)進行安全隔離,在容器化部署的場景下,特別是在Docker平臺上,我們可以使用docker network來進行網絡設置,但是Kuberntes的出現為我們提出了更多的挑戰(zhàn),因為默認情況下,在同集群內,POD之間默認互通。這種默認互通也包含了管控節(jié)點(master node),因為control components(API Server,ETCD,調度器,Controller等)和運行業(yè)務負載的pod都在同一個網絡中,如果讀者有電信的背景,會覺得這簡直是開歷史倒車,因為電信系統(tǒng)中,組網的核心就是將不同級別的設備和組件劃分到不同的網絡中,這主要是解決安全問題。
那么在Kubernetes中,如何給這個大虛擬機局域網劃分不同的區(qū)域呢?Kubernetes給出的方案就是網絡策略(network pollicies)。不過在介紹網絡策略之前,咱們先來詳細的介紹一下3層負載均衡的實現機制。從網絡分層的角度看(不是很清楚的同學建議先閱讀本篇文章的“上”),3層解決的主要問題就是路由的問題,也就是數據包下一跳的問題,而決定數據包下一跳的核心是路由表,簡單說路由表中提供了到某個地址應該發(fā)送數據到哪個IP地址以及走哪個網絡接口(網卡)的信息。轉發(fā)數據包(路由)只是3層提供的多項能力的之一,我們還可以進行drop數據包,負載均衡,NAT(修改IP地址),驗證防火墻規(guī)則,驗證網絡安全策略等工作。
除了在網絡協議棧的3層可以對數據進行這些個豐富的操作,在4層(傳輸層)我們也可以基于端口做很多相同的工作,這些規(guī)則驗證重度依賴于內核提供的一種叫netfiler的功能。具體來說netfiler是Linux內核版本2.4引入的一種數據包過濾框架(packet-filtering framework),netfiler的工作原理是基于一組作用于源IP地址和目標地址的”規(guī)則“來決定數據包(packet)的下一步動作。通過上邊的描述大家應該可以體會到這套機制的核心是”規(guī)則“,這些規(guī)則是我們用來實現特定業(yè)務場景的基礎,而在Kubernetes平臺中,這個規(guī)則通過iptables和IPVS來配置。
iptables提供了多種不同的table類型來配置數據包處理的規(guī)則,通過iptables配置的規(guī)則,會被kernel中的netfiler使用并apply到符合條件的數據包處理鏈路中。具體來說,在容器網絡的上下文中,有兩種類型的table類型需要大家深入的了解:
- filter,用來決定接到到的數據包具體是drop還是forward
- nat,對接收到的數據包進行網絡地址翻譯(Network address translation)
我們可以在全新安裝的ubuntu操作系統(tǒng)上,以root賬戶運行iptables -L,筆者機器上命令返回的信息如下:
root@6ce15134fd1a:/# iptables -L
Chain INPUT (policy ACCEPT)
target? ? prot opt source? ? ? ? ? ? ? destination
Chain FORWARD (policy ACCEPT)
target? ? prot opt source? ? ? ? ? ? ? destination
Chain OUTPUT (policy ACCEPT)
target? ? prot opt source? ? ? ? ? ? ? destination
從輸出的結果可以看到多個chain,關于如何操作iptables咱們后邊詳細介紹,大家也有個感覺就行。通過iptables提供的配置netfiler規(guī)則的能力,我們可以實現很多安全目標。比如容器防火墻技術方案,Kubernetes的網絡插件等都重依賴于iptables來實現豐富的網絡安全策略。特別是在Kubernetes平臺中,kube-proxy實用iptables規(guī)則來實現service到多個后端pod的負載均衡。具體來說,在Kubernetes中,每個service都有內部DNS名稱,DNS名稱會被翻譯成具體的IP地址,當請求發(fā)送到service對應的ip地址后,通過定義在iptables規(guī)則,通過DNAT機制來講目標地址替換為service身后某個pod的ip地址,然后請求就發(fā)給具體的POD來進行后續(xù)的處理。當支持服務的pod發(fā)生變化,比如刪除了一個pod并增加了一個信息的,iptables會進行相應的更新來體現最新的映射關系。
在Kubernetes環(huán)境中,我們可以通過kubectl客戶端工具很容易看到service服務和POD以及iptables之間的關系,假設咱們有一個my-nginx的服務,服務有兩個pod,我們可以通過iptables -t nat -L來過濾只看和nat相關的table,在有較多業(yè)務應用部署的機器上運行這個命令會返回很多信息,但是也不難從這些信息中找到我們感興趣的規(guī)則。因為我們的服務的ip地址為10.100.132.10,你可以通過這個IP地址找到chain KUBE-SERVICES,如下所示:
Chain KUBE-SERVICES (2 references)
target prot opt source destination
...
KUBE-SVC-SV7AMNAGZFKZEMQ4 tcp -- anywhere 10.100.132.10 /* default/mynginx:
http cluster IP */ tcp dpt:http-alt
...
雖然說這個rule的規(guī)則并不是大白話,但是咱們這些悟性極高的科技行業(yè)從事很容易從中找到這叫KUBE-SVC-SV7AMNAGZFKZEMQ4目標chain,我們繼續(xù)往下翻就可以找到這個KUBE-SVC-SV7AMNAGZFKZEMQ4的詳細定義:
Chain KUBE-SVC-SV7AMNAGZFKZEMQ4 (2 references)
target prot opt source destination
KUBE-SEP-XZGVVMRRSKK6PWWN all -- anywhere anywhere statistic mode
random probability 0.50000000000
KUBE-SEP-PUXUHBP3DTPPX72C all -- anywhere anywhere
眼尖的同學通過probability 0.5這樣的配置應該能看出來這是負載均衡,因為請求到了這chain之后,會按照50%的幾率來平均分配到兩臺POD,分別是KUBE-SEP-XZGVVMRRSKK6PWWN和KUBE-SEP-PUXUHBP3DTPPX72C??紤]周全的同學可能會問,如果是3個POD,能除的盡嗎?筆者建議大家在自己的環(huán)境中通過kubectl scale來調整POD的數量。
那么上邊這個chain中的兩個以KUBE-SEP的chain到底是啥,相信你也能猜出來這是具體的POD信息,咱們繼續(xù)往下翻就能找到這兩個chain的定義:
Chain KUBE-SEP-XZGVVMRRSKK6PWWN (1 references)
target? ? ? ? prot opt source destination
KUBE-MARK-MASQ all -- 10.32.0.3 anywhere
DNAT tcp -- anywhere anywhere tcp to:10.32.0.3:80
...
Chain KUBE-SEP-PUXUHBP3DTPPX72C (1 references)
target? ? ? ? prot opt source destination
KUBE-MARK-MASQ all -- 10.32.0.4 anywhere
DNAT tcp -- anywhere anywhere tcp to:10.32.0.4:80
如果讀者那上邊的輸出信息來對比kubectl get svc,pods -o wide輸出的信息,會發(fā)現完全一致,這也符合預期。不過iptables有個很大的問題,如果機器上有很多應用和pod,iptables的性能會變得很差,筆者看到過一個計算,在有2000個服務以及每個服務有10個pod的機器上,會產生大概20000個iptables規(guī)則。因此Kubernetes現在主流使用的是IPVS技術,咱們繼續(xù)IPVS這個話題。
IPVS又稱IP Virtual Server,通常被稱作Layer 4負載均衡或者Layer 4 Lan switching技術。IPVS和iptables類似,是另外一種規(guī)則的實現機制,和iptables最大的不同就是使用hash表來優(yōu)化負載均衡機制。
可以說IPVS是為kube-proxy量身定做,因為我們在Kubenernetes環(huán)境下需要依賴于kube-proxy來訪問Node上運行的容器實例提供的服務。雖然說從技術方案上看,IPVS的性能要比iptables好很多,但是我們并不能直接得出這樣的結論,iptables最近幾年也設計了增量更新的機制,因此關于iptables和IPVS之爭,我們拭目以待。
至于說我們在項目上到底使用哪種方式來定制化netfiler的規(guī)則,這個本質上不是咱們應用系統(tǒng)開發(fā)人員關心的,不過有這部分知識能夠更好的幫助我們進行風險評估以及故障根因分析。由于規(guī)則的執(zhí)行在內核里,并且內核在多個容器實例之間共享,因此無論在什么情況下操作netfiler規(guī)則,大家要考慮清楚爆炸半徑。
咱們將視線拉回到Kubernetes的場景下,Kubernetes通過網絡策略(network policies)來控制POD之間的數據流動規(guī)則。策略通常由port,ip地址,服務,以及l(fā)abeled pods組成,當發(fā)送或者收到數據消息,如果消息無法通過策略,那么就拒絕或者drop,比如如下所示的NetworkPolicy對象yaml文件定義,定義了my-nginx服務的ingress訪問規(guī)則:

接下來我們在自己的集群中通過kubectl apply來部署這個網絡規(guī)則后,通過iptables -L就能看到具體增加的規(guī)則:
Chain WEAVE-NPC-INGRESS (1 references)
target prot opt source destination
ACCEPT all -- anywhere anywhere match-set weave-{U;]TI.l|Md
RzDhN7$NRn[t)d src match-set weave-vC070kAfB$if8}PFMX{V9Mv2m dst /* pods: namespace: default, selector: access=true -> pods: namespace: default, selector: app=mynginx (ingress) */
關于誰更新了宿主機上的iptables這個問題,大家一定要記住,是網絡插件而不是Kubernetes自身負責更新iptables。從上邊的輸出大家大概也能猜測出來,筆者的環(huán)境使用的網絡插件weave。
了解完Kubernetes的工作原理之后,咱們接下來通過直接操作iptables來體驗一下iptables的強大,在新創(chuàng)建的ubuntu機器上運行命令:while true; do echo "hello world" | nc -l 9000 -N; done 來啟動一個在9000端口運行的服務。
然后在另外一個窗口上我們可以通過curl localhost:9000來驗證服務可訪問性。然后我們運行命令sudo iptables -I INPUT -j REJECT -p tcp --dport=9000 來增加iptable規(guī)則,然后運行iptables -L觀察更新后的iptables,如下圖所示:

然后我們可以重新嘗試curl本地9000端口上的服務,輸出如下:
# curl localhost:9000
curl: (7) Failed to connect to localhost port 9000: Connection refused
從數據的結果可以看到,我們手動設定規(guī)則生效了。從上邊這個簡單的例子我們可以看到如何使用iptables來約束服務的流量,不過筆者不建議大家通過手動的方式來管理容器實例或者POD的流量,因為這總工作已經有非常成熟的工具支持。
Kubernetes的NetworkPolicy機制為我們提供了一種更加容易的方式來使用iptables的規(guī)則,關于如何使用Network policy大家可以參考相關的文檔或者官方文檔,筆者結合過往的實踐經驗,有如下的最佳實踐總結:
- Default Deny,默認情況下denies所有的ingress訪問流量,通過網絡策略之開放給特定客戶端,這也是最小權限安全規(guī)范的體現
- Default Deny egress,和ingress流量類似,默認情況下也拒絕所有的對外訪問流量,通過policies來對特定訪問場景開發(fā)
- 通過polices限制pod和pod之間的訪問流量
- 通過polices限制port端口的使用,讓應用只有特定端口才能對外提供服務,這其實和我們傳統(tǒng)上配置服務器遵守的安全策略類似,縮小攻擊面
到目前為止本篇文章討論都是3層4層的負載均衡機制,接下來我們來看看4層之上的情況,也就是service mesh(服務網格)機制。服務網格為我們提供了一種全新的解決應用程序相互之間連接的思路。業(yè)界主流的服務網格方案包括但不限于:Istio,Envoy,Linkerd以及阿里云提供的托管服務網格服務。
服務網格需要依賴于Kubernetes提供的環(huán)境運行,代理容器會以邊車容器的形式被注入到POD中,這樣所有流入和流出POD的流量都會通過邊車代理,那么我們就可以在邊車代理上進行安全控制,實施安全策略等。筆者要特別強調的是,服務網格提供的應用程序層的安全手段和我們在3,4層介紹的安全機制不沖突,這也是縱深防御思路的體現。
從生命周期的角度看,邊車容器的生命周期和應用程序容器實例的生命周期一致,如果應用程序容器實例被攻破,那么攻擊者可能會繞過邊車容器來訪問策略不允許的資源。特別是邊車容器和應用容器通常共享網絡命名空間,因此我們必須反復確認應用程序容器啟動的時候沒有被賦予CAP_NET_ADMIN權限,這樣即便是應用程序容器被攻破,也無法修改宿主機上的iptables,造成應用程序宕機。
注:筆者前邊的例子中,在docker中啟動ubuntu實例的時候,需要主動賦予CAP_NET_ADMIN權限,要不然無法在啟動的容器實例中查看為這個容器新建命名空間中網絡協議棧的iptables信息,具體的命名為:docker run -i -t --name qigaopan --cap-add NET_ADMIN ubuntu bash,讀者注意cap-add參數。
好了,今天這篇文章就這么多了,咱們下篇文章繼續(xù)討論連接安全,或者簡單說就是HTTPS,以及背后的SSL技術原理,敬請期待!