將出口流量路由至萬用字元目的地
一種通用方法,用於設定可以將流量動態路由到一組受限目標遠端主機(包括萬用字元網域)的出口閘道。
如果您使用 Istio 來處理應用程式發起的流量到網格外部的目的地,您可能熟悉出口閘道的概念。出口閘道可以用來監控和轉發來自網格內部應用程式的流量到網格外部的位置。如果您的系統在受限環境中運作,並且您想要控制從您的網格可以訪問公共網際網路的內容,這會是一個有用的功能。
配置出口閘道來處理任意萬用字元網域的使用案例,在 1.13 版之前的官方 Istio 文件中已有說明,但隨後被移除,因為文件中描述的解決方案並非官方支援或建議,並且在未來版本的 Istio 中可能會損壞。儘管如此,舊的解決方案在 1.20 之前的 Istio 版本中仍然可用。然而,Istio 1.20 捨棄了一些此方法運作所需的 Envoy 功能。
這篇文章嘗試描述我們如何解決此問題,並使用與 Istio 版本無關的元件和 Envoy 功能,以類似的方法填補了這個空白,而且無需單獨的 Nginx SNI 代理。我們的方法允許舊解決方案的使用者在系統面臨 Istio 1.20 中的重大變更之前,無縫遷移配置。
要解決的問題
目前文件中描述的出口閘道使用案例依賴於流量的目標(主機名稱)在 VirtualService
中靜態配置的事實,告訴出口閘道 Pod 中的 Envoy 將匹配的輸出連線 TCP 代理到哪裡。您可以使用多個,甚至是萬用字元的 DNS 名稱來匹配路由條件,但您無法將流量路由到應用程式請求中指定的確切位置。例如,您可以匹配目標為 *.wikipedia.org
的流量,但接著需要將流量轉發到單一最終目標,例如,en.wikipedia.org
。如果有另一個服務,例如,anyservice.wikipedia.org
,它與 en.wikipedia.org
不在相同的伺服器上託管,則到該主機的流量將會失敗。這是因為,即使 HTTP 酬載的 TLS 交握中的目標主機名稱包含 anyservice.wikipedia.org
,en.wikipedia.org
伺服器也將無法提供請求。
此問題的高階解決方案是在每個新的閘道連線中檢查應用程式 TLS 交握中的原始伺服器名稱(SNI 擴充),並使用它作為目標來動態 TCP 代理離開閘道的流量(此名稱以純文字傳送,因此不需要 TLS 終止或其他中間人操作)。
當透過出口閘道限制出口流量時,我們需要鎖定出口閘道,使其只能被網格內的用戶端使用。這是透過在應用程式 Sidecar 和閘道之間強制執行 ISTIO_MUTUAL
(mTLS 對等驗證)來實現的。這表示在應用程式 L7 酬載上將有兩層 TLS。一層是由最終遠端目標終止的應用程式發起的端對端 TLS 會話,另一層是 Istio mTLS 會話。
另一個需要記住的事情是,為了減輕任何潛在的應用程式 Pod 損壞,應用程式 Sidecar 和閘道都應執行主機名稱清單檢查。這樣,任何受損的應用程式 Pod 仍然只能存取允許的目標,而不能存取其他任何內容。
低階 Envoy 程式設計來救援
最近的 Envoy 版本包含一種動態 TCP 轉發代理解決方案,該解決方案使用每個連線的 SNI 標頭來決定應用程式請求的目標。雖然 Istio VirtualService
無法配置這樣的目標,但我們可以使用 EnvoyFilter
來更改 Istio 產生的路由指示,以便使用 SNI 標頭來決定目標。
為了讓一切正常運作,我們首先配置一個自訂的出口閘道來監聽輸出流量。使用 DestinationRule
和 VirtualService
,我們指示應用程式 Sidecar 將流量(針對選定的主機名稱清單)路由到該閘道,並使用 Istio mTLS。在閘道 Pod 端,我們使用上面提到的 EnvoyFilter
建構 SNI 轉發器,引入內部 Envoy 監聽器和叢集以使一切正常運作。最後,我們將閘道實作的 TCP 代理的內部目的地修補到內部 SNI 轉發器。
端對端請求流程如下圖所示
此圖顯示使用 SNI 作為路由金鑰,對 en.wikipedia.org
的出口 HTTPS 請求。
應用程式容器
應用程式發起對最終目的地的 HTTP/TLS 連線。將目的地的的主機名稱放入 SNI 標頭。此 TLS 會話不會在網格內解密。僅檢查 SNI 標頭(因為它是純文字)。
Sidecar 代理
Sidecar 攔截來自應用程式發起的 TLS 會話中,與 SNI 標頭中的主機名稱相符的流量。根據 VirtualService,流量會路由到出口閘道,同時也將原始流量封裝到 Istio mTLS 中。外部 TLS 會話在 SNI 標頭中具有閘道服務位址。
網格監聽器
在閘道中建立專用的監聽器,該監聽器會相互驗證 Istio mTLS 流量。在外部 Istio mTLS 終止之後,它會將內部 TLS 流量無條件地以 TCP 代理傳送到同一閘道中的另一個(內部)監聽器。
SNI 轉發器
另一個具有 SNI 轉發器的監聽器對原始 TLS 會話執行新的 TLS 標頭檢查。如果內部 SNI 主機名稱與允許的網域名稱(包括萬用字元)相符,則它會將流量 TCP 代理到目的地,從每個連線的標頭讀取。此監聽器是 Envoy 內部的(允許它重新啟動流量處理以查看內部 SNI 值),因此沒有任何 Pod(網格內或網格外)可以直接連線到它。此監聽器透過 EnvoyFilter 100% 手動配置。
部署範例
為了部署範例配置,首先建立 istio-egress
命名空間,然後使用以下 YAML 來部署出口閘道,以及一些 RBAC 及其 Service
。在此範例中,我們使用閘道注入方法來建立閘道。根據您的安裝方法,您可能想要以不同的方式部署它(例如,使用 IstioOperator
CR 或使用 Helm)。
# New k8s cluster service to put egressgateway into the Service Registry,
# so application sidecars can route traffic towards it within the mesh.
apiVersion: v1
kind: Service
metadata:
name: egressgateway
namespace: istio-egress
spec:
type: ClusterIP
selector:
istio: egressgateway
ports:
- port: 443
name: tls-egress
targetPort: 8443
---
# Gateway deployment with injection method
apiVersion: apps/v1
kind: Deployment
metadata:
name: istio-egressgateway
namespace: istio-egress
spec:
selector:
matchLabels:
istio: egressgateway
template:
metadata:
annotations:
inject.istio.io/templates: gateway
labels:
istio: egressgateway
sidecar.istio.io/inject: "true"
spec:
containers:
- name: istio-proxy
image: auto # The image will automatically update each time the pod starts.
securityContext:
capabilities:
drop:
- ALL
runAsUser: 1337
runAsGroup: 1337
---
# Set up roles to allow reading credentials for TLS
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: istio-egressgateway-sds
namespace: istio-egress
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "watch", "list"]
- apiGroups:
- security.openshift.io
resourceNames:
- anyuid
resources:
- securitycontextconstraints
verbs:
- use
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: istio-egressgateway-sds
namespace: istio-egress
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: istio-egressgateway-sds
subjects:
- kind: ServiceAccount
name: default
確認閘道 Pod 在 istio-egress
命名空間中已啟動並執行,然後套用以下 YAML 來配置閘道路由
# Define a new listener that enforces Istio mTLS on inbound connections.
# This is where sidecar will route the application traffic, wrapped into
# Istio mTLS.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: egressgateway
namespace: istio-system
spec:
selector:
istio: egressgateway
servers:
- port:
number: 8443
name: tls-egress
protocol: TLS
hosts:
- "*"
tls:
mode: ISTIO_MUTUAL
---
# VirtualService that will instruct sidecars in the mesh to route the outgoing
# traffic to the egress gateway Service if the SNI target hostname matches
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: direct-wildcard-through-egress-gateway
namespace: istio-system
spec:
hosts:
- "*.wikipedia.org"
gateways:
- mesh
- egressgateway
tls:
- match:
- gateways:
- mesh
port: 443
sniHosts:
- "*.wikipedia.org"
route:
- destination:
host: egressgateway.istio-egress.svc.cluster.local
subset: wildcard
# Dummy routing instruction. If omitted, no reference will point to the Gateway
# definition, and istiod will optimise the whole new listener out.
tcp:
- match:
- gateways:
- egressgateway
port: 8443
route:
- destination:
host: "dummy.local"
weight: 100
---
# Instruct sidecars to use Istio mTLS when sending traffic to the egress gateway
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: egressgateway
namespace: istio-system
spec:
host: egressgateway.istio-egress.svc.cluster.local
subsets:
- name: wildcard
trafficPolicy:
tls:
mode: ISTIO_MUTUAL
---
# Put the remote targets into the Service Registry
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: wildcard
namespace: istio-system
spec:
hosts:
- "*.wikipedia.org"
ports:
- number: 443
name: tls
protocol: TLS
---
# Access logging for the gateway
apiVersion: telemetry.istio.io/v1alpha1
kind: Telemetry
metadata:
name: mesh-default
namespace: istio-system
spec:
accessLogging:
- providers:
- name: envoy
---
# And finally, the configuration of the SNI forwarder,
# it's internal listener, and the patch to the original Gateway
# listener to route everything into the SNI forwarder.
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: sni-magic
namespace: istio-system
spec:
configPatches:
- applyTo: CLUSTER
match:
context: GATEWAY
patch:
operation: ADD
value:
name: sni_cluster
load_assignment:
cluster_name: sni_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
envoy_internal_address:
server_listener_name: sni_listener
- applyTo: CLUSTER
match:
context: GATEWAY
patch:
operation: ADD
value:
name: dynamic_forward_proxy_cluster
lb_policy: CLUSTER_PROVIDED
cluster_type:
name: envoy.clusters.dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig
dns_cache_config:
name: dynamic_forward_proxy_cache_config
dns_lookup_family: V4_ONLY
- applyTo: LISTENER
match:
context: GATEWAY
patch:
operation: ADD
value:
name: sni_listener
internal_listener: {}
listener_filters:
- name: envoy.filters.listener.tls_inspector
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
filter_chains:
- filter_chain_match:
server_names:
- "*.wikipedia.org"
filters:
- name: envoy.filters.network.sni_dynamic_forward_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.sni_dynamic_forward_proxy.v3.FilterConfig
port_value: 443
dns_cache_config:
name: dynamic_forward_proxy_cache_config
dns_lookup_family: V4_ONLY
- name: envoy.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: tcp
cluster: dynamic_forward_proxy_cluster
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
log_format:
text_format_source:
inline_string: '[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%
%PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS% %CONNECTION_TERMINATION_DETAILS%
"%UPSTREAM_TRANSPORT_FAILURE_REASON%" %BYTES_RECEIVED% %BYTES_SENT% %DURATION%
%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%"
"%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" %UPSTREAM_CLUSTER%
%UPSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_LOCAL_ADDRESS% %DOWNSTREAM_REMOTE_ADDRESS%
%REQUESTED_SERVER_NAME% %ROUTE_NAME%
'
- applyTo: NETWORK_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.filters.network.tcp_proxy"
patch:
operation: MERGE
value:
name: envoy.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: tcp
cluster: sni_cluster
檢查 istiod
和閘道日誌是否有任何錯誤或警告。如果一切順利,您的網格 Sidecar 現在會將 *.wikipedia.org
請求路由到您的閘道 Pod,而閘道 Pod 接著會將它們轉發到應用程式請求中指定的確切遠端主機。
試試看
遵循其他 Istio 出口範例,我們將使用 sleep Pod 作為傳送請求的測試來源。假設在您的預設命名空間中啟用自動 Sidecar 注入,請使用以下命令部署測試應用程式
$ kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.24/samples/sleep/sleep.yaml
取得您的 sleep 和閘道 Pod
$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
$ export GATEWAY_POD=$(kubectl get pod -n istio-egress -l istio=egressgateway -o jsonpath={.items..metadata.name})
執行以下命令以確認您可以連線到 wikipedia.org
網站
$ kubectl exec "$SOURCE_POD" -c sleep -- sh -c 'curl -s https://en.wikipedia.org/wiki/Main_Page | grep -o "<title>.*</title>"; curl -s https://de.wikipedia.org/wiki/Wikipedia:Hauptseite | grep -o "<title>.*</title>"'
<title>Wikipedia, the free encyclopedia</title>
<title>Wikipedia – Die freie Enzyklopädie</title>
我們可以連線到英文和德文的 wikipedia.org
子網域,太棒了!
通常,在生產環境中,我們會封鎖未配置為透過出口閘道重新導向的外部請求,但由於我們在測試環境中沒有這樣做,讓我們存取另一個外部網站以進行比較
$ kubectl exec "$SOURCE_POD" -c sleep -- sh -c 'curl -s https://cloud.ibm.com/login | grep -o "<title>.*</title>"'
<title>IBM Cloud</title>
由於我們已在全域開啟存取日誌記錄(使用資訊清單中的 Telemetry
CR),我們現在可以檢查日誌,以了解代理程式如何處理上述請求。
首先,檢查閘道日誌
$ kubectl logs -n istio-egress $GATEWAY_POD
[...]
[2023-11-24T13:21:52.798Z] "- - -" 0 - - - "-" 813 111152 55 - "-" "-" "-" "-" "185.15.59.224:443" dynamic_forward_proxy_cluster 172.17.5.170:48262 envoy://sni_listener/ envoy://internal_client_address/ en.wikipedia.org -
[2023-11-24T13:21:52.798Z] "- - -" 0 - - - "-" 1531 111950 55 - "-" "-" "-" "-" "envoy://sni_listener/" sni_cluster envoy://internal_client_address/ 172.17.5.170:8443 172.17.34.35:55102 outbound_.443_.wildcard_.egressgateway.istio-egress.svc.cluster.local -
[2023-11-24T13:21:53.000Z] "- - -" 0 - - - "-" 821 92848 49 - "-" "-" "-" "-" "185.15.59.224:443" dynamic_forward_proxy_cluster 172.17.5.170:48278 envoy://sni_listener/ envoy://internal_client_address/ de.wikipedia.org -
[2023-11-24T13:21:53.000Z] "- - -" 0 - - - "-" 1539 93646 50 - "-" "-" "-" "-" "envoy://sni_listener/" sni_cluster envoy://internal_client_address/ 172.17.5.170:8443 172.17.34.35:55108 outbound_.443_.wildcard_.egressgateway.istio-egress.svc.cluster.local -
有四個日誌項目,代表我們的三個 curl 請求中的兩個。每一對都會顯示單個請求如何流經 Envoy 流量處理管道。它們以相反的順序印出,但我們可以看到第 2 行和第 4 行顯示請求到達閘道服務,並通過內部 sni_cluster
目標。第 1 行和第 3 行顯示最終目標是從內部 SNI 標頭決定的,也就是應用程式設定的目標主機。請求被轉發到 dynamic_forward_proxy_cluster
,該叢集最終將請求從 Envoy 發送到遠端目標。
太棒了,但是對 IBM Cloud 的第三個請求在哪裡?讓我們檢查 Sidecar 日誌
$ kubectl logs $SOURCE_POD -c istio-proxy
[...]
[2023-11-24T13:21:52.793Z] "- - -" 0 - - - "-" 813 111152 61 - "-" "-" "-" "-" "172.17.5.170:8443" outbound|443|wildcard|egressgateway.istio-egress.svc.cluster.local 172.17.34.35:55102 208.80.153.224:443 172.17.34.35:37020 en.wikipedia.org -
[2023-11-24T13:21:52.994Z] "- - -" 0 - - - "-" 821 92848 55 - "-" "-" "-" "-" "172.17.5.170:8443" outbound|443|wildcard|egressgateway.istio-egress.svc.cluster.local 172.17.34.35:55108 208.80.153.224:443 172.17.34.35:37030 de.wikipedia.org -
[2023-11-24T13:21:55.197Z] "- - -" 0 - - - "-" 805 15199 158 - "-" "-" "-" "-" "104.102.54.251:443" PassthroughCluster 172.17.34.35:45584 104.102.54.251:443 172.17.34.35:45582 cloud.ibm.com -
如您所見,維基百科請求透過閘道傳送,而對 IBM Cloud 的請求則直接從應用程式 Pod 傳送到網際網路,如 PassthroughCluster
日誌所示。
結論
我們使用出口閘道實作了對出口 HTTPS/TLS 流量的受控路由,支援任意和萬用字元網域名稱。在生產環境中,這篇文章中顯示的範例將會擴充以支援 HA 需求(例如,新增區域感知閘道 Deployment
等),並限制應用程式的直接外部網路存取,以便應用程式只能透過閘道存取公共網路,而該閘道僅限於一組預定義的遠端主機名稱。
此解決方案可輕鬆擴展。您可以在設定中包含多個網域名稱,它們會在您部署後立即被加入允許清單!無需為每個網域設定 VirtualService
或其他路由細節。但請注意,網域名稱會列在設定中的多個位置。如果您使用 CI/CD 工具(例如 Kustomize),最好將網域名稱列表提取到一個單一位置,您可以從該位置渲染到所需的設定資源。
就是這樣!希望這對您有所幫助。如果您是先前基於 Nginx 解決方案的現有使用者,您現在可以遷移到此方法,然後再升級到 Istio 1.20,否則會中斷您目前的設定。
SNI 路由愉快!