本文分享自華為雲社群《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})
首先建立一個 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 發起 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 之外的所有流量。
接下來我們將學習如何通過設定 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 發起,則可以解決這兩個問題。
為 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 發起。
前面我們學習瞭如何通過設定 Istio 實現對外部服務的 TLS 發起,這裡其實還有一個 mTLS 的概念呢,由於 TLS 本身就比較複雜,對於雙向 TLS(mTLS)就更復雜了。
TLS 是一個連線層協定,旨在為 TCP 連線提供安全保障。TLS 在連線層工作,可以與任何使用 TCP 的應用層協定結合使用。例如,HTTPS 是 HTTP 與 TLS 的結合(HTTPS 中的 S 指的是 SSL,即 TLS 的前身),TLS 認證的流程大致如下所示:
Verisign
、Digicert
等,這些證書在釋出時被打包在一起,當我們下載瀏覽器時,就經把正確的證書放進了瀏覽器,如果 CA 不被信任,則找不到對應 CA 的證書,證書也會被判定非法。認證過程
當然 HTTPS 的工作流程和這個過程基本就一致了:
HTTPS 工作流程
當然雙向 TLS 就更為複雜了,Mutual TLS(雙向 TLS),或稱 mTLS,對於常規的 TLS,只需要伺服器端認證,mTLS 相對來說有一個額外的規定:使用者端也要經過認證。在 mTLS 中,使用者端和伺服器都有一個證書,並且雙方都使用它們的公鑰/私鑰對進行身份驗證。
TLS 保證了真實性,但預設情況下,這隻發生在一個方向上:使用者端對伺服器進行認證,但伺服器並不對使用者端進行認證。為什麼 TLS 的預設只在一個方向進行認證?因為使用者端的身份往往是不相關的。例如我們在存取優點知識的時候,你的瀏覽器已經驗證了要存取的網站伺服器端的身份,但伺服器端並沒有驗證你的瀏覽器的身份,它實際上並不關心你的瀏覽器的身份,這對於網際網路上的 Web 專案來說足夠了。但是在某些情況下,伺服器確實需要驗證使用者端的身份,例如,當用戶端需要存取某些敏感資料時,伺服器可能需要驗證使用者端的身份,以確保使用者端有權存取這些資料,這就是 mTLS 的用武之地,mTLS 是保證微服務之間跨服務通訊安全的好方法。
接下來我們就來測試下如何通過 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 錯誤。
開啟了雙向認證
建立 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/