本文分享自華為雲社群《Istio Egress 出口閘道器使用》,作者:k8s技術圈。
前面我們瞭解了位於服務網格內部的應用應如何訪問網格外部的 HTTP 和 HTTPS 服務,知道如何透過 ServiceEntry
物件配置 Istio 以受控的方式訪問外部服務,這種方式實際上是透過 Sidecar 直接呼叫的外部服務,但是有時候我們可能需要透過專用的 Egress Gateway 服務來呼叫外部服務,這種方式可以更好的控制對外部服務的訪問。
Istio 使用 Ingress 和 Egress Gateway 配置執行在服務網格邊緣的負載均衡,Ingress Gateway 允許定義網格所有入站流量的入口。Egress Gateway 是一個與 Ingress Gateway 對稱的概念,它定義了網格的出口。Egress Gateway 允許我們將 Istio 的功能(例如,監視和路由規則)應用於網格的出站流量。
使用場景
另一個使用場景是叢集中的應用節點沒有公有 IP,所以在該節點上執行的網格服務都無法訪問網際網路,那麼我們就可以透過定義 Egress gateway,將公有 IP 分配給 Egress Gateway 節點,用它引導所有的出站流量,可以使應用節點以受控的方式訪問外部服務。
準備工作
如果你使用的 demo
這個配置檔案安裝 Istio,那麼 Egress Gateway 已經預設安裝了,可以透過下面的命令來檢視:
$ kubectl get pod -l istio=egressgateway -n istio-system NAME READY STATUS RESTARTS AGE istio-egressgateway-556f6f58f4-hkzdd 1/1 Running 0 14d
如果沒有 Pod 返回,可以透過下面的步驟來部署 Istio Egress Gateway。如果你使用 IstioOperator
安裝 Istio,請在配置中新增以下欄位:
spec: components: egressGateways: - name: istio-egressgateway enabled: true
否則使用如下的 istioctl install
命令來安裝:
$ istioctl install <flags-you-used-to-install-Istio> \ --set components.egressGateways[0].name=istio-egressgateway \ --set components.egressGateways[0].enabled=true
同樣我們還是使用 sleep 示例做為傳送請求的測試源,如果啟用了自動 Sidecar 注入,執行以下命令部署示例應用程式:
kubectl apply -f samples/sleep/sleep.yaml
否則,在使用以下命令部署 sleep 應用程式之前,手動注入 Sidecar:
kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)
為了傳送請求,您需要建立 SOURCE_POD 環境變數來儲存源 Pod 的名稱:
export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
用 Egress gateway 發起 HTTP 請求
首先建立一個 ServiceEntry
物件來允許流量直接訪問外部的 edition.cnn.com
服務。
apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: https protocol: HTTPS resolution: DNS
傳送 HTTPS 請求到 https://edition.cnn.com/politics
驗證 ServiceEntry
是否已正確應用。
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics # 輸出如下內 HTTP/1.1 301 Moved Permanently # ...... location: https://edition.cnn.com/politics # ...... HTTP/2 200 Content-Type: text/html; charset=utf-8 # ......
然後為 edition.cnn.com
的 80 埠建立一個 egress Gateway,併為指向 Egress Gateway 的流量建立一個 DestinationRule
規則,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway # 匹配 Egress Gateway Pod 的標籤 servers: - port: number: 80 name: http protocol: HTTP hosts: - edition.cnn.com # 也支援萬用字元 * 的形式 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-cnn spec: host: istio-egressgateway.istio-system.svc.cluster.local # 目標規則為 Egress Gateway subsets: - name: cnn # 定義一個子集 cnn,沒有指定 labels,則 subset 會包含所有符合 host 欄位指定的服務的 Pod
在上面的物件中我們首先定義了一個 Gateway
物件,不過這裡我們定義的是一個 Egress Gateway,透過 istio: egressgateway
匹配 Egress Gateway Pod 的標籤,並在 servers
中定義了 edition.cnn.com
服務的 80 埠。然後定義了一個 DestinationRule
物件,指定了目標規則為 istio-egressgateway.istio-system.svc.cluster.local
,並定義了一個子集 cnn
。
這裡的子集名稱是
cnn
,但沒有指定labels
。這意味著,這個 subset 會涵蓋所有屬於istio-egressgateway.istio-system.s
vc.cluster.local
服務的 Pod。這種情況下,subset 的作用主要是為了在其他 Istio 配置中提供一個方便的引用名稱,而不是為了區分不同的 Pod 子集。
如何再定義一個 VirtualService
物件將流量從 Sidecar 引導至 Egress Gateway,再從 Egress Gateway 引導至外部服務,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-cnn-through-egress-gateway spec: hosts: - edition.cnn.com gateways: - istio-egressgateway # Egress Gateway - mesh # 網格內部的流量 http: - match: - gateways: - mesh # 這條規則適用於從服務網格內發出的流量 port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local # 流量將被路由到 egress gateway subset: cnn port: number: 80 weight: 100 - match: - gateways: - istio-egressgateway # 這條規則適用於透過 istio-egressgateway 的流量 port: 80 route: - destination: host: edition.cnn.com # 流量將被路由到外部服務 port: number: 80 weight: 100
在上面的 VirtualService
物件中透過 hosts
指定 edition.cnn.com
,表示該虛擬服務用於該服務的請求,gateways
欄位中定義了 istio-egressgateway
和 mesh
兩個值,istio-egressgateway
是上面我們定義的 Egress Gateway,mesh
表示該虛擬服務用於網格內部的流量,也就是說這個虛擬服務指定了如何處理來自服務網格內部以及透過 istio-egressgateway
的流量。
mesh
是一個特殊的關鍵字,在 Istio 中表示服務網格內的所有 Sidecar 代理。當使用mesh
作為閘道器時,這意味著VirtualService
中定義的路由規則適用於服務網格內的所有服務,即所有裝有 Istio sidecar 代理的服務。
http
欄位中定義了兩個 match
,第一個 match
用於匹配 mesh
閘道器,第二個 match
用於匹配 istio-egressgateway
閘道器,然後在 route
中定義了兩個 destination
,第一個 destination
用於將流量引導至 Egress Gateway 的 cnn 子集,第二個 destination
用於將流量引導至外部服務。
總結來說,這個 VirtualService
的作用是控制服務網格內部到 edition.cnn.com
的流量。當流量起始於服務網格內時,它首先被路由到 istio-egressgateway
,然後再路由到 edition.cnn.com
,這種配置有助於統一和控制從服務網格內部到外部服務的流量,可以用於流量監控、安全控制或實施特定的流量策略。
應用上面的資源物件後,我們再次向 edition.cnn.com
的 /politics
端點發出 curl 請求:
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics # ...... HTTP/1.1 301 Moved Permanently location: https://edition.cnn.com/politics # ...... HTTP/2 200 Content-Type: text/html; charset=utf-8 # ......
正常和前面的一次測試輸出結果是一致的,但是這次在請求是經過 istio-egressgateway
Pod 發出的,我們可以檢視日誌來驗證:
kubectl logs -l istio=egressgateway -c istio-proxy -n istio-system | tail
正常會看到一行類似於下面這樣的內容:
[2023-11-15T08:48:38.683Z] "GET /politics HTTP/2" 301 - via_upstream - "-" 0 0 204 203 "10.244.1.73" "curl/7.81.0-DEV" "6c2c4550-92d4-955c-b6cb-83bf2b0e06f4" "edition.cnn.com" "151.101.3.5:80" outbound|80||edition.cnn.com 10.244.2.184:46620 10.244.2.184:8080 10.244.1.73:49924 - -
因為我們這裡只是將 80 埠的流量重定向到 Egress Gateway 了,所以重定向後 443 埠的 HTTPS 流量將直接進入 edition.cnn.com
,所以沒有看到 443 埠的日誌,但是我們可以透過 SOURCE_POD
的 Sidecar 代理的日誌來檢視到:
$ kubectl logs "$SOURCE_POD" -c istio-proxy | tail # ...... [2023-11-15T08:55:55.513Z] "GET /politics HTTP/1.1" 301 - via_upstream - "-" 0 0 191 191 "-" "curl/7.81.0-DEV" "12ce15aa-1247-9b7e-8185-4224f96f5ea0" "edition.cnn.com" "10.244.2.184:8080" outbound|80|cnn|istio-egressgateway.istio-system.svc.cluster.local 10.244.1.73:49926 151.101.195.5:80 10.244.1.73:41576 - - [2023-11-15T08:55:55.753Z] "- - -" 0 - - - "-" 839 2487786 1750 - "-" "-" "-" "-" "151.101.195.5:443" outbound|443||edition.cnn.com 10.244.1.73:45246 151.101.67.5:443 10.244.1.73:42998 edition.cnn.com -
用 Egress gateway 發起 HTTPS 請求
上面我們已經學習瞭如何透過 Egress Gateway 發起 HTTP 請求,接下來我們再來學習下如何透過 Egress Gateway 發起 HTTPS 請求。
原理都是一樣的,只是我們需要在相應的 ServiceEntry
、Egress Gateway
和 VirtualService
中指定 TLS
協議的埠 443。
首先為 edition.cnn.com
定義 ServiceEntry
服務:
apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: cnn spec: hosts: - edition.cnn.com ports: - number: 443 name: tls protocol: TLS resolution: DNS
應用該資源物件後,傳送 HTTPS 請求到 https://edition.cnn.com/politics
,驗證該 ServiceEntry
是否已正確生效。
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics ... HTTP/2 200 Content-Type: text/html; charset=utf-8 ...
接下來同樣的方式為 edition.cnn.com
建立一個 Egress Gateway。除此之外還需要建立一個目標規則和一個虛擬服務,用來引導流量透過 Egress Gateway,並透過 Egress Gateway 與外部服務通訊。
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway servers: - port: number: 443 name: tls protocol: TLS hosts: - edition.cnn.com tls: mode: PASSTHROUGH # 透傳 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-cnn spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: cnn --- apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-cnn-through-egress-gateway spec: hosts: - edition.cnn.com gateways: - mesh - istio-egressgateway tls: - match: - gateways: - mesh port: 443 sniHosts: - edition.cnn.com route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: cnn port: number: 443 - match: - gateways: - istio-egressgateway port: 443 sniHosts: - edition.cnn.com route: - destination: host: edition.cnn.com port: number: 443 weight: 100
上面物件中定義的 Gateway
物件和前面的一樣,只是將埠改為了 443,然後在 tls
中指定了 mode: PASSTHROUGH
,表示該 Gateway
物件用於 TLS 協議的請求。然後在後面的 VirtualService
物件中就是配置 spec.tls
屬性,用於指定 TLS 協議的請求的路由規則,配置方法和前面 HTTP 方式類似,只是注意要將埠改為 443,並且在 match
中指定 sniHosts
為 edition.cnn.com
,表示該虛擬服務用於處理 edition.cnn.com
的 TLS 請求。
應用上面的資源物件後,我們現在傳送 HTTPS 請求到 https://edition.cnn.com/politics
,輸出結果應該和之前一樣。
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics ... HTTP/2 200 Content-Type: text/html; charset=utf-8 ...
檢查 Egress Gateway 代理的日誌,則列印日誌的命令是:
kubectl logs -l istio=egressgateway -n istio-system
應該會看到類似於下面的內容:
[2023-11-15T08:59:55.513Z] "- - -" 0 - 627 1879689 44 - "-" "-" "-" "-" "151.101.129.67:443" outbound|443||edition.cnn.com 172.30.109.80:41122 172.30.109.80:443 172.30.109.112:59970 edition.cnn.com
到這裡我們就實現了透過 Egress Gateway 發起 HTTPS 請求。最後記得清理上面建立的資源物件:
$ kubectl delete serviceentry cnn $ kubectl delete gateway istio-egressgateway $ kubectl delete virtualservice direct-cnn-through-egress-gateway $ kubectl delete destinationrule egressgateway-for-cnn
需要注意的是,Istio 無法強制讓所有出站流量都經過 Egress Gateway, Istio 只是透過 Sidecar 代理實現了這種流向。攻擊者只要繞過 Sidecar 代理, 就可以不經 Egress Gateway 直接與網格外的服務進行通訊,從而避開了 Istio 的控制和監控。出於安全考慮,叢集管理員和雲供應商必須確保網格所有的出站流量都要經過 Egress Gateway。這需要透過 Istio 之外的機制來滿足這一要求。例如,叢集管理員可以配置防火牆,拒絕 Egress Gateway 以外的所有流量。Kubernetes NetworkPolicy 也能禁止所有不是從 Egress Gateway 發起的出站流量,但是這個需要 CNI 外掛的支援。此外,叢集管理員和雲供應商還可以對網路進行限制,讓執行應用的節點只能透過 gateway 來訪問外部網路。要實現這一限制,可以只給 Gateway Pod 分配公網 IP,並且可以配置 NAT 裝置, 丟棄來自 Egress Gateway Pod 之外的所有流量。
Egress TLS Origination
接下來我們將學習如何透過配置 Istio 去實現對發往外部服務的流量的 TLS Origination(TLS 發起)。若此時原始的流量為 HTTP,則 Istio 會將其轉換為 HTTPS 連線。TLS Origination 的概念前面我們也已經介紹過了。
TLS Origination
假設有一個傳統應用正在使用 HTTP 和外部服務進行通訊,如果有一天突然有一個新的需求,要求必須對所有外部的流量進行加密。此時,使用 Istio 便可透過修改配置實現此需求,而無需更改應用中的任何程式碼。該應用可以傳送未加密的 HTTP 請求,由 Istio 為請求進行加密。
從應用源頭髮起未加密的 HTTP 請求,並讓 Istio 執行 TLS 升級的另一個好處是可以產生更好的遙測併為未加密的請求提供更多的路由控制。
下面我們就來學習下如何配置 Istio 實現 TLS Origination。
同樣我們這裡使用 sleep 示例應用來作為測試源,如果啟用了自動注入 Sidecar,那麼可以直接部署 sleep 應用:
kubectl apply -f samples/sleep/sleep.yaml
否則在部署 sleep 應用之前,必須手動注入 Sidecar:
kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)
建立一個環境變數來儲存用於將請求傳送到外部服務 Pod 的名稱:
export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
▍配置對外部服務的訪問
首先使用 ServiceEntry
物件來配置對外部服務 edition.cnn.com
的訪問。這裡我們將使用單個 ServiceEntry
來啟用對服務的 HTTP 和 HTTPS 訪問。建立一個如下所示的 ServiceEntry
物件:
apiVersion: networking.istio.io/v1alpha3 kind: ServiceEntry metadata: name: edition-cnn-com spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP - number: 443 name: https-port protocol: HTTPS resolution: DNS
上面的 ServiceEntry
物件中我們指定了 edition.cnn.com
服務的主機名,然後在 ports
中指定了需要暴露的埠及其屬性,表示該 ServiceEntry
物件代表對 edition.cnn.com
的訪問,這裡我們定義了 80
和 443
兩個埠,分別對應 http
和 https
服務,resolution: DNS
定義瞭如何解析指定的 hosts
,這裡我們使用 DNS 來解析。
直接應用該資源物件,然後向外部的 HTTP 服務傳送請求:
$ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics # 輸出如下結果 HTTP/1.1 301 Moved Permanently # ...... location: https://edition.cnn.com/politics HTTP/2 200 content-type: text/html; charset=utf-8 # ......
上面我們在使用 curl
命令的時候新增了一個 -L
標誌,該標誌指示 curl
將遵循重定向。在這種情況下,伺服器將對到 http://edition.cnn.com/politics
的 HTTP 請求進行重定向響應,而重定向響應將指示客戶端使用 HTTPS 向 https://edition.cnn.com/politics
重新傳送請求,對於第二個請求,伺服器則返回了請求的內容和 200 狀態碼。
儘管 curl
命令簡明地處理了重定向,但是這裡有兩個問題。第一個問題是請求冗餘,它使獲取 http://edition.cnn.com/politics
內容的延遲加倍,第二個問題是 URL 中的路徑(在本例中為 politics
)被以明文的形式傳送。如果有人嗅探你的應用與 edition.cnn.com
之間的通訊,他將會知曉該應用獲取了此網站中哪些特定的內容。出於隱私的原因,我們可能希望阻止這些內容被嗅探到。透過配置 Istio 執行 TLS 發起,則可以解決這兩個問題。
▍用於 egress 流量的 TLS 發起
為 edition.cnn.com
建立一個出口閘道器,埠為 80,接收 HTTP 流量,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway servers: - port: number: 80 name: tls-origination-port protocol: HTTP hosts: - edition.cnn.com
然後為 istio-egressgateway
建立一個 DestinationRule
物件,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-cnn spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: cnn
接著我們只需要建立一個 VirtualService
物件,將流量從 Sidecar 引導至 Egress Gateway,再從 Egress Gateway 引導至外部服務,如下所示:
apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-cnn-through-egress-gateway spec: hosts: - edition.cnn.com gateways: - istio-egressgateway # Egress Gateway - mesh # 網格內部的流量 http: - match: - gateways: - mesh port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: cnn port: number: 80 weight: 100 - match: - gateways: - istio-egressgateway port: 80 route: - destination: host: edition.cnn.com port: number: 443 # 443 埠 weight: 100 --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: originate-tls-for-edition-cnn-com spec: host: edition.cnn.com trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: SIMPLE # initiates HTTPS for connections to edition.cnn.com
需要注意的是上面最後針對 edition.cnn.com
的 DestinationRule
物件,在 trafficPolicy
中指定了 portLevelSettings
用於對不同的埠定義不同的流量策略,這裡我們定義了 443
埠的 tls
模式為 SIMPLE
,表示當訪問 edition.cnn.com
的 HTTP 請求時執行 TLS 發起。
應用上面的資源物件,然後再次向 http://edition.cnn.com/politics
傳送 HTTP 請求:
$ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics # 直接輸出200狀態碼 HTTP/1.1 200 OK content-length: 2474958 content-type: text/html; charset=utf-8 # ......
這次將會收到唯一的 200 OK 響應,因為 Istio 為 curl 執行了 TLS 發起,原始的 HTTP 被升級為 HTTPS 並轉發到 edition.cnn.com
。伺服器直接返回內容而無需重定向,這消除了客戶端與伺服器之間的請求冗餘,使網格保持加密狀態,隱藏了你的應用獲取 edition.cnn.com
中 politics
端點的資訊。
如果我們在程式碼中有去訪問外部服務,那麼我們就可以不用修改程式碼了,只需要透過配置 Istio 來獲得 TLS 發起即可,而無需更改一行程式碼。
到這裡我們就學習瞭如何透過配置 Istio 實現對外部服務的 TLS 發起。
▍TLS 與 mTLS 基本概念
前面我們學習瞭如何透過配置 Istio 實現對外部服務的 TLS 發起,這裡其實還有一個 mTLS 的概念呢,由於 TLS 本身就比較複雜,對於雙向 TLS(mTLS)就更復雜了。
TLS 是一個連線層協議,旨在為 TCP 連線提供安全保障。TLS 在連線層工作,可以與任何使用 TCP 的應用層協議結合使用。例如,HTTPS 是 HTTP 與 TLS 的結合(HTTPS 中的 S 指的是 SSL,即 TLS 的前身),TLS 認證的流程大致如下所示:
-
首先向第三方機構 CA 提交組織資訊、個人資訊(域名)等資訊並申請認證。
-
CA 透過多種手段驗證申請者提供資訊的真實性,如組織是否存在、企業是否合法,是否擁有域名的所有權等。如資訊稽核透過,CA 會向申請者簽發認證檔案-證書。
-
證書包含以下資訊:申請者公鑰、申請者的組織資訊和個人資訊、簽發機構 CA 的資訊、有效時間、證書序列號等資訊的明文,同時包含一個簽名。其中籤名的產生演算法:首先,使用雜湊函式計算公開的明文資訊的資訊摘要,然後,採用 CA 的私鑰對資訊摘要進行加密,密文即簽名。
-
客戶端向服務端發出請求時,服務端返回證書檔案。
-
客戶端讀取證書中的相關的明文資訊,採用相同的雜湊函式計算得到資訊摘要,然後,利用對應 CA 的公鑰解密簽名資料,對比證書的資訊摘要,如果一致,則可以確認證書的合法性。
-
客戶端還會驗證證書相關的域名資訊、有效時間等資訊; 客戶端會內建信任 CA 的證書資訊(包含公鑰),比如瀏覽器基本上都帶有知名公共 CA 機構的證書,如
Verisign
、Digicert
等,這些證書在釋出時被打包在一起,當我們下載瀏覽器時,就經把正確的證書放進了瀏覽器,如果 CA 不被信任,則找不到對應 CA 的證書,證書也會被判定非法。
認證過程
當然 HTTPS 的工作流程和這個過程基本就一致了:
-
1.客戶端發起一個 HTTPS 請求。
-
2.服務端把配置好的證書返回給客戶端。
-
3.客戶端驗證證書:比如是否在有效期內,證書的用途是不是匹配 Client 請求的站點,是不是在 CRL 吊銷列表裡面,它的上一級證書是否有效等。
-
4.客戶端使用偽隨機數生成對稱金鑰,並透過證書裡伺服器的公鑰進行加密,後續使用該對稱金鑰進行傳輸資訊。
-
5.服務端使用自己的私鑰解密這個訊息,得到對稱金鑰。至此,客戶端和服務端都持有了相同的對稱金鑰。
-
6.服務端使用對稱金鑰加密明文內容 A,傳送給客戶端。
-
7.客戶端使用對稱金鑰解密響應的密文,得到明文內容 A。
-
8.客戶端再次發起 HTTPS 的請求,使用對稱金鑰加密請求的明文內容 B,然後伺服器使用對稱金鑰解密密文,得到明文內容 B。
HTTPS 工作流程
當然雙向 TLS 就更為複雜了,Mutual TLS(雙向 TLS),或稱 mTLS,對於常規的 TLS,只需要服務端認證,mTLS 相對來說有一個額外的規定:客戶端也要經過認證。在 mTLS 中,客戶端和伺服器都有一個證書,並且雙方都使用它們的公鑰/私鑰對進行身份驗證。
TLS 保證了真實性,但預設情況下,這隻發生在一個方向上:客戶端對伺服器進行認證,但伺服器並不對客戶端進行認證。為什麼 TLS 的預設只在一個方向進行認證?因為客戶端的身份往往是不相關的。例如我們在訪問優點知識的時候,你的瀏覽器已經驗證了要訪問的網站服務端的身份,但服務端並沒有驗證你的瀏覽器的身份,它實際上並不關心你的瀏覽器的身份,這對於網際網路上的 Web 專案來說足夠了。但是在某些情況下,伺服器確實需要驗證客戶端的身份,例如,當客戶端需要訪問某些敏感資料時,伺服器可能需要驗證客戶端的身份,以確保客戶端有權訪問這些資料,這就是 mTLS 的用武之地,mTLS 是保證微服務之間跨服務通訊安全的好方法。
-
首先,你想要安全的通訊。當我們把我們的應用程式拆分為多個服務時,我們最終會在這些服務之間的網路上傳送敏感資料。任何能夠進入網路的人都有可能讀取這些敏感資料並偽造請求。
-
其次,你關心客戶端的身份。首先,你要確保你能知道呼叫是什麼時候發生的,以便進行診斷,並正確記錄指標等事項。此外,你可能想對這些身份進行授權(允許 A 呼叫 B 嗎)。當然授權是另外的話題了。
接下來我們就來測試下如何透過 egress 閘道器發起雙向 TLS 連線。
▍透過 egress 閘道器發起雙向 TLS 連線
首先使用 openssl
命令生成客戶端和伺服器的證書與金鑰,為你的服務簽名證書建立根證書和私鑰:
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt # 生成 CA 證書和私鑰
為 my-nginx.mesh-external.svc.cluster.local
建立證書和私鑰:
# 為 my-nginx.mesh-external.svc.cluster.local 建立私鑰和證書籤名請求 $ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:2048 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization" # 使用 CA 公鑰和私鑰以及證書籤名請求為 my-nginx.mesh-external.svc.cluster.local 建立證書 $ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in my-nginx.mesh-external.svc.cluster.local.csr -out my-nginx.mesh-external.svc.cluster.local.crt
然後生成客戶端證書和私鑰:
# 為 client.example.com 建立私鑰和證書籤名請求 $ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization" # 使用相同的 CA 公鑰和私鑰以及證書籤名請求為 client.example.com 建立證書 $ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in client.example.com.csr -out client.example.com.crt
接著我們來部署一個雙向 TLS 伺服器,為了模擬一個真實的支援雙向 TLS 協議的外部服務,我們在 Kubernetes 叢集中部署一個 NGINX 服務,該服務執行在 Istio 服務網格之外,比如執行在一個沒有開啟 Istio Sidecar proxy 注入的名稱空間中。
建立一個名稱空間 mesh-external
表示 Istio 網格之外的服務,注意在這個名稱空間中,Sidecar 自動注入是沒有開啟的,不會在 Pod 中自動注入 Sidecar proxy。
kubectl create namespace mesh-external
然後建立 Kubernetes Secret,儲存伺服器和 CA 的證書。
$ kubectl create -n mesh-external secret tls nginx-server-certs --key my-nginx.mesh-external.svc.cluster.local.key --cert my-nginx.mesh-external.svc.cluster.local.crt $ kubectl create -n mesh-external secret generic nginx-ca-certs --from-file=example.com.crt
生成 NGINX 伺服器的配置檔案:
$ cat <<\EOF > ./nginx.conf events { } http { log_format main '$remote_addr - $remote_user [$time_local] $status ' '"$request" $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; error_log /var/log/nginx/error.log; server { listen 443 ssl; root /usr/share/nginx/html; index index.html; server_name my-nginx.mesh-external.svc.cluster.local; ssl_certificate /etc/nginx-server-certs/tls.crt; ssl_certificate_key /etc/nginx-server-certs/tls.key; ssl_client_certificate /etc/nginx-ca-certs/example.com.crt; ssl_verify_client on; } } EOF
生成 Kubernetes ConfigMap 儲存 NGINX 伺服器的配置檔案:
kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
然後就可以部署 NGINX 服務了:
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: name: my-nginx namespace: mesh-external labels: run: my-nginx spec: ports: - port: 443 protocol: TCP selector: run: my-nginx --- apiVersion: apps/v1 kind: Deployment metadata: name: my-nginx namespace: mesh-external spec: selector: matchLabels: run: my-nginx template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx ports: - containerPort: 443 volumeMounts: - name: nginx-config mountPath: /etc/nginx readOnly: true - name: nginx-server-certs mountPath: /etc/nginx-server-certs readOnly: true - name: nginx-ca-certs mountPath: /etc/nginx-ca-certs readOnly: true volumes: - name: nginx-config configMap: name: nginx-configmap - name: nginx-server-certs secret: secretName: nginx-server-certs - name: nginx-ca-certs secret: secretName: nginx-ca-certs EOF
現在如果我們在網格內部去直接訪問這個 my-nginx
服務,是無法訪問的,第一是沒有內建 CA 證書,另外是 my-nginx
服務開啟了 mTLS,需要客戶端證書才能訪問,現在我們的網格中是沒有對應的客戶端證書的,會出現 400 錯誤。
開啟了雙向認證
為 egress 流量配置雙向 TLS
建立 Kubernetes Secret 儲存客戶端證書:
kubectl create secret -n istio-system generic client-credential --from-file=tls.key=client.example.com.key \ --from-file=tls.crt=client.example.com.crt --from-file=ca.crt=example.com.crt
Secret 所在的名稱空間必須與出口閘道器部署的位置一致,我們這裡是 istio-system
名稱空間。
然後為 my-nginx.mesh-external.svc.cluster.local
建立一個埠為 443 的 Egress Gateway,以及目標規則和虛擬服務來引導流量流經 egress 閘道器並從 egress 閘道器流向外部服務。
$ kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1alpha3 kind: Gateway metadata: name: istio-egressgateway spec: selector: istio: egressgateway servers: - port: number: 443 name: https protocol: HTTPS hosts: - my-nginx.mesh-external.svc.cluster.local tls: mode: ISTIO_MUTUAL --- apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: egressgateway-for-nginx spec: host: istio-egressgateway.istio-system.svc.cluster.local subsets: - name: nginx trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: ISTIO_MUTUAL sni: my-nginx.mesh-external.svc.cluster.local EOF
上面我們定義的 Gateway
物件和前面的一樣,只是將埠改為了 443,然後在 tls
中指定了 mode: ISTIO_MUTUAL
,表示該 Gateway
物件用於 TLS 雙向認證協議的請求。
然後同樣在後面的 DestinationRule
物件中配置了透過 istio-egressgateway
的流量的規則,這裡我們定義了 443
埠的 tls
模式為 ISTIO_MUTUAL
,表示當訪問 my-nginx.mesh-external.svc.clustr.
local
的 TLS 請求時執行 TLS 雙向認證。
最後我們定義一個 VirtualService
物件來引導流量流經 egress 閘道器:
$ kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1alpha3 kind: VirtualService metadata: name: direct-nginx-through-egress-gateway spec: hosts: - my-nginx.mesh-external.svc.cluster.local gateways: - istio-egressgateway - mesh # 網格內部的流量 http: - match: - gateways: - mesh port: 80 route: - destination: host: istio-egressgateway.istio-system.svc.cluster.local subset: nginx port: number: 443 weight: 100 - match: - gateways: - istio-egressgateway port: 443 route: - destination: host: my-nginx.mesh-external.svc.cluster.local port: number: 443 weight: 100 EOF
上面的 VirtualService
物件定義網格內部對 my-nginx.mesh-external.svc.cluster.local
服務的訪問引導至 istio-egressgateway
,然後再由 istio-egressgateway
引導流量流向外部服務。
新增 DestinationRule 執行雙向 TLS:
$ kubectl apply -n istio-system -f - <<EOF apiVersion: networking.istio.io/v1alpha3 kind: DestinationRule metadata: name: originate-mtls-for-nginx spec: host: my-nginx.mesh-external.svc.cluster.local trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 443 tls: mode: MUTUAL credentialName: client-credential # 這必須與之前建立的用於儲存客戶端證書的 Secret 相匹配 sni: my-nginx.mesh-external.svc.cluster.local EOF
傳送一個 HTTP 請求至 http://my-nginx.mesh-external.svc.cluster.local
:
$ kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -sS http://my-nginx.mesh-external.svc.cluster.local <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
檢查 istio-egressgateway Pod 日誌,有一行與請求相關的日誌記錄。如果 Istio 部署在名稱空間 istio-system 中,列印日誌的命令為:
kubectl logs -l istio=egressgateway -n istio-system | grep 'my-nginx.mesh-external.svc.cluster.local' | grep HTTP
將顯示類似如下的一行:
[2023-11-17T08:23:51.203Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 615 17 16 "10.244.1.100" "curl/7.81.0-DEV" "434b5755-54da-9924-9e2a-a204b5a2124c" "my-nginx.mesh-external.svc.cluster.local" "10.244.1.106:443" outbound|443||my-nginx.mesh-external.svc.cluster.local 10.244.2.239:35198 10.244.2.239:8443 10.244.1.100:56448 my-nginx.mesh-external.svc.cluster.local -
雙向認證
即使我們直接在網格中訪問的是 HTTP 的服務,但是透過配置 Istio,我們也可以實現對外部服務的雙向 TLS 認證。
參考文件:https://istio.io/latest/docs/tasks/traffic-management/egress/