出口 TLS 發起

存取外部服務」任務示範了如何從網格內部的應用程式存取外部(即服務網格外部)的 HTTP 和 HTTPS 服務。如該任務所述,會使用 ServiceEntry 來設定 Istio 以受控方式存取外部服務。此範例說明如何設定 Istio 來為外部服務的流量執行 TLS 始發。Istio 會開啟與外部服務的 HTTPS 連線,而原始流量為 HTTP。

使用案例

假設有一個舊版應用程式會對外部網站執行 HTTP 呼叫。假設操作該應用程式的組織收到一個新的需求,指出所有外部流量都必須加密。使用 Istio,只需組態即可達成此需求,無需變更應用程式中的任何程式碼。應用程式可以傳送未加密的 HTTP 請求,而 Istio 接著會為應用程式加密這些請求。

從來源傳送未加密的 HTTP 請求,並讓 Istio 執行 TLS 升級的另一個好處是,Istio 可以產生更好的遙測資料,並為未加密的請求提供更多的路由控制。

開始之前

  • 請依照「安裝指南」中的指示設定 Istio。

  • 啟動 curl 範例,該範例將用作外部呼叫的測試來源。

    如果您已啟用自動 Sidecar 注入,請部署 curl 應用程式

    壓縮
    $ kubectl apply -f @samples/curl/curl.yaml@
    

    否則,您必須在部署 curl 應用程式之前手動注入 Sidecar

    壓縮
    $ kubectl apply -f <(istioctl kube-inject -f @samples/curl/curl.yaml@)
    

    請注意,任何您可以從中執行 execcurl 的 Pod 都適用於以下程序。

  • 建立一個 Shell 變數來儲存來源 Pod 的名稱,以便將請求傳送到外部服務。如果您使用了 curl 範例,請執行

    $ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})
    

設定存取外部服務

首先,使用「存取外部服務」任務中顯示的相同技術,設定存取外部服務 edition.cnn.com 的權限。不過,這次請使用單一 ServiceEntry 來啟用對該服務的 HTTP 和 HTTPS 存取。

  1. 建立 ServiceEntry 以啟用對 edition.cnn.com 的存取

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1
    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
    EOF
    
  2. 向外部 HTTP 服務發出請求

    $ kubectl exec "${SOURCE_POD}" -c curl -- 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
    ...
    

    輸出應與上述內容類似(某些詳細資料已替換為省略符號)。

請注意 curl-L 旗標,該旗標指示 curl 追蹤重新導向。在此案例中,伺服器傳回了針對 http://edition.cnn.com/politics 的 HTTP 請求的重新導向回應(301 Moved Permanently)。重新導向回應會指示用戶端傳送額外的請求,這次使用 HTTPS,至 https://edition.cnn.com/politics。對於第二個請求,伺服器傳回了所請求的內容和 200 OK 狀態碼。

雖然 curl 命令以透明方式處理了重新導向,但這裡有兩個問題。第一個問題是多餘的請求,這會使擷取 http://edition.cnn.com/politics 內容的延遲時間增加一倍。第二個問題是 URL 的路徑(在此案例中為 politics)以純文字傳送。如果有一個攻擊者嗅探您的應用程式和 edition.cnn.com 之間的通訊,攻擊者將會知道應用程式擷取了 edition.cnn.com 的哪些特定主題。基於隱私考量,您可能想要防止此類洩漏。

這兩個問題都可以透過設定 Istio 來執行 TLS 始發來解決。

用於輸出流量的 TLS 始發

  1. 從上一節重新定義您的 ServiceEntry,以將 HTTP 請求重新導向至連接埠 443,並新增 DestinationRule 來執行 TLS 始發

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1
    kind: ServiceEntry
    metadata:
      name: edition-cnn-com
    spec:
      hosts:
      - edition.cnn.com
      ports:
      - number: 80
        name: http-port
        protocol: HTTP
        targetPort: 443
      - number: 443
        name: https-port
        protocol: HTTPS
      resolution: DNS
    ---
    apiVersion: networking.istio.io/v1
    kind: DestinationRule
    metadata:
      name: edition-cnn-com
    spec:
      host: edition.cnn.com
      trafficPolicy:
        portLevelSettings:
        - port:
            number: 80
          tls:
            mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com
    EOF
    

    上面的 DestinationRule 將對連接埠 80 上的 HTTP 請求執行 TLS 始發,而 ServiceEntry 接著會將連接埠 80 上的請求重新導向至目標連接埠 443。

  2. http://edition.cnn.com/politics 發送 HTTP 請求,如同上一節一樣

    $ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
    HTTP/1.1 200 OK
    ...
    

    這次您會收到 200 OK 作為第一個也是唯一的回應。Istio 為 curl 執行了 TLS 始發,因此原始 HTTP 請求會以 HTTPS 轉送到 edition.cnn.com。伺服器會直接傳回內容,而無需重新導向。您消除了用戶端和伺服器之間的雙向往返,並且請求在離開網格時已加密,而沒有洩漏您的應用程式擷取了 edition.cnn.compolitics 區段這一事實。

    請注意,您使用了與上一節相同的命令。對於以程式設計方式存取外部服務的應用程式,無需變更程式碼。您可以透過設定 Istio 來獲得 TLS 始發的優點,而無需變更任何程式碼。

  3. 請注意,使用 HTTPS 存取外部服務的應用程式會繼續像以前一樣運作

    $ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics
    HTTP/2 200
    ...
    

其他安全性考量

由於應用程式 Pod 和本機主機上的 Sidecar Proxy 之間的流量仍然未加密,因此能夠滲透應用程式節點的攻擊者仍然能夠在節點的本機網路上看到未加密的通訊。在某些環境中,嚴格的安全性要求可能會指出所有流量都必須加密,即使在節點的本機網路上也是如此。在如此嚴格的要求下,應用程式應僅使用 HTTPS (TLS)。本範例中描述的 TLS 始發將不足以滿足需求。

另請注意,即使使用應用程式發起的 HTTPS,攻擊者也可以透過檢查 伺服器名稱指示 (SNI) 來知道是否正在傳送至 edition.cnn.com 的請求。SNI 欄位在 TLS 交握期間以未加密的方式傳送。使用 HTTPS 可防止攻擊者知道特定主題和文章,但無法阻止攻擊者得知正在存取 edition.cnn.com

清除 TLS 始發組態

移除您建立的 Istio 組態項目

$ kubectl delete serviceentry edition-cnn-com
$ kubectl delete destinationrule edition-cnn-com

用於輸出流量的相互 TLS 始發

本節說明如何設定 Sidecar 以針對外部服務執行 TLS 始發,這次是使用需要相互 TLS 的服務。此範例相當複雜,因為它需要以下設定

  1. 產生用戶端和伺服器憑證
  2. 部署支援相互 TLS 通訊協定的外部服務
  3. 設定用戶端(curl Pod)以使用步驟 1 中建立的認證

完成此設定後,您就可以設定外部流量通過將執行 TLS 始發的 Sidecar。

產生用戶端和伺服器憑證及金鑰

對於此任務,您可以使用您最愛的工具來產生憑證和金鑰。以下命令使用 openssl

  1. 建立根憑證和私密金鑰以簽署您的服務憑證

    $ 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
    
  2. 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"
    $ 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
    

    或者,如果您想要為目的地啟用 SAN 驗證,則可以將 SubjectAltNames 新增至憑證。例如

    $ cat > san.conf <<EOF
    [req]
    distinguished_name = req_distinguished_name
    req_extensions = v3_req
    x509_extensions = v3_req
    prompt = no
    [req_distinguished_name]
    countryName = US
    [v3_req]
    keyUsage = critical, digitalSignature, keyEncipherment
    extendedKeyUsage = serverAuth, clientAuth
    basicConstraints = critical, CA:FALSE
    subjectAltName = critical, @alt_names
    [alt_names]
    DNS = my-nginx.mesh-external.svc.cluster.local
    EOF
    $
    $ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:4096 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization" -config san.conf
    $ 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 -extfile san.conf -extensions v3_req
    
  3. 產生用戶端憑證和私密金鑰

    $ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization"
    $ 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 注入的命名空間中)執行。

  1. 建立命名空間以代表 Istio 網格外部的服務,即 mesh-external。請注意,Sidecar Proxy 將不會自動注入到此命名空間中的 Pod 中,因為未在其上啟用自動 Sidecar 注入。

    $ kubectl create namespace mesh-external
    
  2. 建立 Kubernetes 密碼以保留伺服器和 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
    
  3. 建立 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
    
  4. 建立 Kubernetes ConfigMap 以保留 NGINX 伺服器的組態

    $ kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
    
  5. 部署 NGINX 伺服器

    $ kubectl apply -f - <<EOF
    apiVersion: v1
    kind: Service
    metadata:
      name: my-nginx
      namespace: mesh-external
      labels:
        run: my-nginx
      annotations:
        "networking.istio.io/exportTo": "." # simulate an external service by not exporting outside this namespace
    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
      replicas: 1
      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
    

設定用戶端(curl Pod)

  1. 建立 Kubernetes 密碼以保留用戶端的憑證

    $ kubectl create secret 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
    

    密碼必須建立在與用戶端 Pod 部署在同一命名空間(在此案例中為 default)中。

  2. 新增必要的 RBAC 以確保用戶端 Pod(在此案例中為 curl)可存取上一步中建立的密碼。

    $ kubectl create role client-credential-role --resource=secret --verb=list
    $ kubectl create rolebinding client-credential-role-binding --role=client-credential-role --serviceaccount=default:curl
    

在 Sidecar 設定用於輸出流量的相互 TLS 始發

  1. 新增 ServiceEntry 以將 HTTP 請求重新導向至連接埠 443,並新增 DestinationRule 來執行相互 TLS 始發

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1
    kind: ServiceEntry
    metadata:
      name: originate-mtls-for-nginx
    spec:
      hosts:
      - my-nginx.mesh-external.svc.cluster.local
      ports:
      - number: 80
        name: http-port
        protocol: HTTP
        targetPort: 443
      - number: 443
        name: https-port
        protocol: HTTPS
      resolution: DNS
    ---
    apiVersion: networking.istio.io/v1
    kind: DestinationRule
    metadata:
      name: originate-mtls-for-nginx
    spec:
      workloadSelector:
        matchLabels:
          app: curl
      host: my-nginx.mesh-external.svc.cluster.local
      trafficPolicy:
        loadBalancer:
          simple: ROUND_ROBIN
        portLevelSettings:
        - port:
            number: 80
          tls:
            mode: MUTUAL
            credentialName: client-credential # this must match the secret created earlier to hold client certs, and works only when DR has a workloadSelector
            sni: my-nginx.mesh-external.svc.cluster.local
            # subjectAltNames: # can be enabled if the certificate was generated with SAN as specified in previous section
            # - my-nginx.mesh-external.svc.cluster.local
    EOF
    

    上面的 DestinationRule 將對連接埠 80 上的 HTTP 請求執行 mTLS 始發,而 ServiceEntry 接著會將連接埠 80 上的請求重新導向至目標連接埠 443。

  2. 驗證認證是否已提供給 Sidecar 並處於啟用狀態。

    $ istioctl proxy-config secret deploy/curl | grep client-credential
    kubernetes://client-credential            Cert Chain     ACTIVE     true           1                                          2024-06-04T12:15:20Z     2023-06-05T12:15:20Z
    kubernetes://client-credential-cacert     Cert Chain     ACTIVE     true           10792363984292733914                       2024-06-04T12:15:19Z     2023-06-05T12:15:19Z
    
  3. http://my-nginx.mesh-external.svc.cluster.local 發送 HTTP 請求

    $ kubectl exec "$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})" -c curl -- curl -sS http://my-nginx.mesh-external.svc.cluster.local
    <!DOCTYPE html>
    <html>
    <head>
    <title>Welcome to nginx!</title>
    ...
    
  4. 檢查 curl Pod 的記錄中是否有與我們的請求對應的行。

    $ kubectl logs -l app=curl -c istio-proxy | grep 'my-nginx.mesh-external.svc.cluster.local'
    

    您應該會看到類似以下的行

    [2022-05-19T10:01:06.795Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 615 1 0 "-" "curl/7.83.1-DEV" "96e8d8a7-92ce-9939-aa47-9f5f530a69fb" "my-nginx.mesh-external.svc.cluster.local:443" "10.107.176.65:443"
    

清除相互 TLS 始發組態

  1. 移除已建立的 Kubernetes 資源

    $ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
    $ kubectl delete secret client-credential
    $ kubectl delete rolebinding client-credential-role-binding
    $ kubectl delete role client-credential-role
    $ kubectl delete configmap nginx-configmap -n mesh-external
    $ kubectl delete service my-nginx -n mesh-external
    $ kubectl delete deployment my-nginx -n mesh-external
    $ kubectl delete namespace mesh-external
    $ kubectl delete serviceentry originate-mtls-for-nginx
    $ kubectl delete destinationrule originate-mtls-for-nginx
    
  2. 刪除憑證和私密金鑰

    $ rm example.com.crt example.com.key my-nginx.mesh-external.svc.cluster.local.crt my-nginx.mesh-external.svc.cluster.local.key my-nginx.mesh-external.svc.cluster.local.csr client.example.com.crt client.example.com.csr client.example.com.key
    
  3. 刪除本範例中使用的產生的組態檔

    $ rm ./nginx.conf
    

清除通用組態

刪除 curl 服務和部署

$ kubectl delete service curl
$ kubectl delete deployment curl
這個資訊有用嗎?
您有任何改進建議嗎?

感謝您的回饋!