Istio 網格的出口定義者:深入瞭解 Egress Gateway

2023-12-01 18:01:29

本文分享自華為雲社群《Istio Egress 出口閘道器使用》,作者:k8s技術圈。

前面我們瞭解了位於服務網格內部的應用應如何存取網格外部的 HTTP 和 HTTPS 服務,知道如何通過 ServiceEntry 物件設定 Istio 以受控的方式存取外部服務,這種方式實際上是通過 Sidecar 直接呼叫的外部服務,但是有時候我們可能需要通過專用的 Egress Gateway 服務來呼叫外部服務,這種方式可以更好的控制對外部服務的存取。

Istio 使用 Ingress 和 Egress Gateway 設定執行在服務網格邊緣的負載均衡,Ingress Gateway 允許定義網格所有入站流量的入口。Egress Gateway 是一個與 Ingress Gateway 對稱的概念,它定義了網格的出口。Egress Gateway 允許我們將 Istio 的功能(例如,監視和路由規則)應用於網格的出站流量。

使用場景

比如有一個對安全要求非常嚴格的團隊,要求服務網格所有的出站流量必須經過一組專用節點。專用節點執行在專門的機器上,與叢集中執行應用程式的其他節點隔離,這些專用節點用於實施 Egress 流量的策略,並且受到比其餘節點更嚴密地監控。

另一個使用場景是叢集中的應用節點沒有公有 IP,所以在該節點上執行的網格服務都無法存取網際網路,那麼我們就可以通過定義 Egress gateway,將公有 IP 分配給 Egress Gateway 節點,用它引導所有的出站流量,可以使應用節點以受控的方式存取外部服務。

接下來我們就來學習下在 Istio 中如何設定使用 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 請求。

原理都是一樣的,只是我們需要在相應的 ServiceEntryEgress 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 的概念前面我們也已經介紹過了。

1.png

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 機構的證書,如 VerisignDigicert 等,這些證書在釋出時被打包在一起,當我們下載瀏覽器時,就經把正確的證書放進了瀏覽器,如果 CA 不被信任,則找不到對應 CA 的證書,證書也會被判定非法。

2.png

認證過程

當然 HTTPS 的工作流程和這個過程基本就一致了:

  • 1.使用者端發起一個 HTTPS 請求。
  • 2.伺服器端把設定好的證書返回給使用者端。
  • 3.使用者端驗證證書:比如是否在有效期內,證書的用途是不是匹配 Client 請求的站點,是不是在 CRL 吊銷列表裡面,它的上一級證書是否有效等。
  • 4.使用者端使用偽亂數生成對稱金鑰,並通過證書裡伺服器的公鑰進行加密,後續使用該對稱金鑰進行傳輸資訊。
  • 5.伺服器端使用自己的私鑰解密這個訊息,得到對稱金鑰。至此,使用者端和伺服器端都持有了相同的對稱金鑰。
  • 6.伺服器端使用對稱金鑰加密明文內容 A,傳送給使用者端。
  • 7.使用者端使用對稱金鑰解密響應的密文,得到明文內容 A。
  • 8.使用者端再次發起 HTTPS 的請求,使用對稱金鑰加密請求的明文內容 B,然後伺服器使用對稱金鑰解密密文,得到明文內容 B。

3.png

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 錯誤。

4.png

開啟了雙向認證

為 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 -
 

5.png

雙向認證

即使我們直接在網格中存取的是 HTTP 的服務,但是通過設定 Istio,我們也可以實現對外部服務的雙向 TLS 認證。

參考檔案:https://istio.io/latest/docs/tasks/traffic-management/egress/

點選關注,第一時間瞭解華為雲新鮮技術~