出口 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@)
請注意,任何您可以從中執行
exec
和curl
的 Pod 都適用於以下程序。建立一個 Shell 變數來儲存來源 Pod 的名稱,以便將請求傳送到外部服務。如果您使用了 curl 範例,請執行
$ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})
設定存取外部服務
首先,使用「存取外部服務」任務中顯示的相同技術,設定存取外部服務 edition.cnn.com
的權限。不過,這次請使用單一 ServiceEntry
來啟用對該服務的 HTTP 和 HTTPS 存取。
建立
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
向外部 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 始發
從上一節重新定義您的
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。向
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.com
的 politics 區段這一事實。請注意,您使用了與上一節相同的命令。對於以程式設計方式存取外部服務的應用程式,無需變更程式碼。您可以透過設定 Istio 來獲得 TLS 始發的優點,而無需變更任何程式碼。
請注意,使用 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 的服務。此範例相當複雜,因為它需要以下設定
- 產生用戶端和伺服器憑證
- 部署支援相互 TLS 通訊協定的外部服務
- 設定用戶端(curl Pod)以使用步驟 1 中建立的認證
完成此設定後,您就可以設定外部流量通過將執行 TLS 始發的 Sidecar。
產生用戶端和伺服器憑證及金鑰
對於此任務,您可以使用您最愛的工具來產生憑證和金鑰。以下命令使用 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
為
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
產生用戶端憑證和私密金鑰
$ 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 注入的命名空間中)執行。
建立命名空間以代表 Istio 網格外部的服務,即
mesh-external
。請注意,Sidecar Proxy 將不會自動注入到此命名空間中的 Pod 中,因為未在其上啟用自動 Sidecar 注入。$ kubectl create namespace mesh-external
建立 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
建立 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 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)
建立 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
)中。新增必要的
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 始發
新增
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。驗證認證是否已提供給 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
向
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> ...
檢查
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 始發組態
移除已建立的 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
刪除憑證和私密金鑰
$ 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
刪除本範例中使用的產生的組態檔
$ rm ./nginx.conf
清除通用組態
刪除 curl
服務和部署
$ kubectl delete service curl
$ kubectl delete deployment curl