Istio的流量管理(實操三)

Istio的流量管理(實操三)

涵蓋官方文檔Traffic Management章節中的egress部分。其中有一小部分問題(已在下文標註)待官方解決。html

目錄

訪問外部服務

因爲啓用了istio的pod的出站流量默認都會被重定向到代理上,所以對集羣外部URL的訪問取決於代理的配置。默認狀況下,Envoy代理會透傳對未知服務的訪問,雖然這種方式爲新手提供了便利,但最好配置更嚴格的訪問控制。node

本節展現使用以下三種方式訪問外部服務:nginx

  1. 容許Envoy代理透傳到網格外部的服務
  2. 配置service entries來訪問外部訪問
  3. 透傳某一個IP端的請求

部署

  • 部署sleep app,用於發送請求。git

    $ kubectl apply -f samples/sleep/sleep.yaml
  • 設置SOURCE_POD爲請求源pod名github

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

Envoy透傳流量到外部服務

istio有一個安裝選項meshConfig.outboundTrafficPolicy.mode,用於配置sidecar處理外部服務(即沒有定義到istio內部服務註冊中心的服務)。若是該選項設置爲ALLOW_ANY,則istio代理會放行到未知服務的請求;若是選項設置爲REGISTRY_ONLY,則istio代理會阻塞沒有在網格中定義HTTP服務或服務表項的主機。默認值爲ALLOW_ANY,容許快速對istio進行評估。shell

  1. 首先將meshConfig.outboundTrafficPolicy.mode選項設置爲ALLOW_ANY。默認應該就是ALLOW_ANY,使用以下方式獲取當前的模式:json

    $kubectl get configmap istio -n istio-system -o yaml |grep -o "mode: ALLOW_ANY" |uniq
    mode: ALLOW_ANY

    若是沒有配置模式,能夠手動添加:後端

    outboundTrafficPolicy: 
      mode: ALLOW_ANY
  2. 從網格內向外部服務發送兩個請求,能夠看到請求成功,返回200api

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -I https://www.baidu.com | grep  "HTTP/"; kubectl exec -it $SOURCE_POD -c sleep -- curl -I https://edition.cnn.com | grep "HTTP/"
    HTTP/1.1 200 OK
    HTTP/2 200

    使用這種方式能夠訪問外部服務,但沒法對該流量進行監控和控制,下面介紹如何監控和控制網格到外部服務的流量。安全

控制訪問外部服務

使用ServiceEntry配置能夠從istio集羣內部訪問公共服務。本節展現如何配置訪問外部HTTP服務,httpbin.org以及www.baidu.com,同時會監控和控制istio流量。

修改默認的阻塞策略

爲了展現如何控制訪問外部服務的方式,須要將meshConfig.outboundTrafficPolicy.mode設置爲REGISTRY_ONLY

  1. 執行以下命令將meshConfig.outboundTrafficPolicy.mode選項設置爲REGISTRY_ONLY

    $ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: ALLOW_ANY/mode: REGISTRY_ONLY/g' | kubectl replace -n istio-system -f -
  2. 從SOURCE_POD訪問外部HTTPS服務,此時請求會被阻塞(可能須要等一段時間來使配置生效)

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -I https://www.baidu.com | grep  "HTTP/"; kubectl exec -it $SOURCE_POD -c sleep -- curl -I https://edition.cnn.com | grep "HTTP/"
    command terminated with exit code 35
    command terminated with exit code 35

訪問外部HTTP服務

  1. 建立一個ServiceEntry註冊外部服務,這樣就能夠直接訪問外部HTTP服務,能夠看到此處並無用到virtual service和destination rule

    下面serviceEntry使用DNS 做爲resolution是一種比較安全的方式,將resolution設置爲NONE將可能致使攻擊。例如,惡意客戶可能會再HOST首部中設置httpbin.org,但實際上訪問的不一樣的IP地址。istio sidecar代理會信任HOST首部,並錯誤地容許這次訪問(即便會將流量傳遞到不一樣於主機的IP地址),該主機多是一個惡意網站,或是一個被網格安全策略屏蔽的合法網站。

    使用DNS resolution時,sidecar代理會忽略原始目的地址,並將流量傳遞給hosts字段的主機。在轉發流量前會使用DNS請求hosts字段的IP地址。

    serviceEntry包括以下三種resolution

    Name Description
    NONE Assume that incoming connections have already been resolved (to a specific destination IP address). Such connections are typically routed via the proxy using mechanisms such as IP table REDIRECT/ eBPF. After performing any routing related transformations, the proxy will forward the connection to the IP address to which the connection was bound.
    STATIC Use the static IP addresses specified in endpoints (see below) as the backing instances associated with the service.
    DNS Attempt to resolve the IP address by querying the ambient DNS, during request processing. If no endpoints are specified, the proxy will resolve the DNS address specified in the hosts field, if wildcards are not used. If endpoints are specified, the DNS addresses specified in the endpoints will be resolved to determine the destination IP address. DNS resolution cannot be used with Unix domain socket endpoints.
    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: httpbin-ext
    spec:
      hosts:
      - httpbin.org #外部服務URI
      ports:
      - number: 80 #外部服務HTTP端口信息
        name: http
        protocol: HTTP
      resolution: DNS
      location: MESH_EXTERNAL # 表示一個外部服務,即httpbin.org是網格外部的服務
    EOF
  2. 從SOURCE_POD請求外部HTTP服務

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl http://httpbin.org/headers
    {
      "headers": {
        "Accept": "*/*",
        "Content-Length": "0",
        "Host": "httpbin.org",
        "User-Agent": "curl/7.64.0",
        ...
        "X-Envoy-Decorator-Operation": "httpbin.org:80/*",
      }
    }

    注意HTTP添加了istio sidecar代理首部X-Envoy-Decorator-Operation

  3. 校驗SOURCE_POD sidecar代理的日誌(實際並無如同官方文檔中的打印,issue跟蹤)

    $ kubectl logs $SOURCE_POD -c istio-proxy | tail

訪問外部HTTPS服務

  1. 建立ServiceEntry容許訪問外部HTTPS服務

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: baidu
    spec:
      hosts:
      - www.baidu.com
      ports:
      - number: 443 # 外部服務HTTPS端口
        name: https
        protocol: HTTPS #指定外部服務爲HTTPS協議
      resolution: DNS
      location: MESH_EXTERNAL
    EOF
  2. SOURCE_POD訪問外部服務

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -I https://www.baidu.com | grep  "HTTP/"
    HTTP/1.1 200 OK

管理到外部的流量

與管理集羣內部的流量相似,istio 的路由規則也能夠管理使用ServiceEntry配置的外部服務。本例將會爲httpbin.org服務設置一個超時規則.

  1. 從測試的pod向外部服務httpbin.org的/delay地址發送一個請求,大概5s後返回200

    $ kubectl exec -it $SOURCE_POD -c sleep -- time curl -o /dev/null -s -w "%{http_code}\n" http://httpbin.org/delay/5
    200
    real    0m 5.43s
    user    0m 0.00s
    sys     0m 0.00s
  2. 對外部服務httpbin.org設置一個3s的超時時間

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: httpbin-ext
    spec:
      hosts:
        - httpbin.org #此處的hosts與serviceEntry的hosts字段內容對應
      http:
      - timeout: 3s
        route:
          - destination:
              host: httpbin.org 
            weight: 100
    EOF
  3. 幾秒後,從新訪問該服務,能夠看到訪問超時

    $ kubectl exec -it $SOURCE_POD -c sleep -- time curl -o /dev/null -s -w "%{http_code}\n" http://httpbin.org/delay/5
    504
    real    0m 3.02s
    user    0m 0.00s
    sys     0m 0.00s

卸載

$ kubectl delete serviceentry httpbin-ext google
$ kubectl delete virtualservice httpbin-ext --ignore-not-found=true

直接訪問外部服務

能夠配置Envoy sidecar,使其不攔截特定IP段的請求。爲了實現該功能,能夠修改global.proxy.includeIPRangesglobal.proxy.excludeIPRanges配置選項(相似白名單和黑名單),並使用kubectl apply命令更新istio-sidecar-injector配置。也能夠修改annotations traffic.sidecar.istio.io/includeOutboundIPRanges來達到相同的效果。在更新istio-sidecar-injector配置後,相應的變更會影響到全部的應用pod。

與使用ALLOW_ANY流量策略配置sidecar放行全部到未知服務的流量不一樣,上述方式會繞過sidecar的處理,即在特定IP段上不啓用istio功能。使用這種方式不能增量地爲特定目的地添加service entry,但使用ALLOW_ANY方式是能夠的,所以這種方式僅僅建議用於性能測試或其餘特殊場景中。

一種不把到外部IP的流量重定向到sidecar代理的方式是將global.proxy.includeIPRanges設置爲集羣內部服務使用的一個IP段或多個IP段。

找到平臺使用的內部IP段後,就可使用以下方式配置includeIPRanges,這樣目的地非10.0.0.1/24的流量會繞過sidecar的處理。

$ istioctl manifest apply <the flags you used to install Istio> --set values.global.proxy.includeIPRanges="10.0.0.1/24"

總結

本節介紹了三種訪問外部服務的方式:

  1. 配置Envoy 容許訪問外部服務
  2. 在網格內部使用service entry註冊可訪問的外部服務,推薦使用這種方式
  3. 配置istio sidecar排除處理某些IP段的流量

第一種方式的流量會通過istio sidecar代理,當使用這種方式時,沒法監控訪問外部服務的流量,沒法使用istio的流量控制功能。第二種方法能夠在調用集羣內部或集羣外部的服務時充分使用istio服務網格特性,本章的例子中,在訪問外部服務時設置了超時時間。第三種方式會繞過istio sidecar代理,直接訪問外部服務。然而這種方式須要指定集羣的配置,與第一種方式相似,這種方式也沒法監控到外部服務的流量,且沒法使用istio的功能。

卸載

$ kubectl delete -f samples/sleep/sleep.yaml

環境恢復

檢查當前的模式

$ kubectl get configmap istio -n istio-system -o yaml | grep -o "mode: ALLOW_ANY" | uniq
$ kubectl get configmap istio -n istio-system -o yaml | grep -o "mode: REGISTRY_ONLY" | uniq

將模式從ALLOW_ANY切換到REGISTRY_ONLY

$ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: ALLOW_ANY/mode: REGISTRY_ONLY/g' | kubectl replace -n istio-system -f -

將模式從REGISTRY_ONLY切換到ALLOW_ANY

$ kubectl get configmap istio -n istio-system -o yaml | sed 's/mode: REGISTRY_ONLY/mode: ALLOW_ANY/g' | kubectl replace -n istio-system -f -

Egress TLS Origination(Egress TLS源)

本節展現如何經過配置istio來(對到外部服務的流量)初始化TLS。當原始流量爲HTTP時,Istio會與外部服務創建HTTPS鏈接,即istio會加密到外部服務的請求。

建立sleep應用

$ kubectl apply -f samples/sleep/sleep.yaml

獲取sleep的pod名

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

建立ServiceEntryVirtualService訪問 edition.cnn.com

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: edition-cnn-com
spec:
  hosts:
  - edition.cnn.com #外部服務URI
  ports:
  - number: 80     # HTTP訪問
    name: http-port
    protocol: HTTP
  - number: 443    # HTTPS訪問
    name: https-port
    protocol: HTTPS #指定外部服務爲HTTPS協議
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: edition-cnn-com
spec:
  hosts:
  - edition.cnn.com #外部服務URI
  tls: #非終結的TLS&HTTPS流量
  - match: #將edition.cnn.com:443的流量分發到edition.cnn.com:443
    - port: 443
      sniHosts:
      - edition.cnn.com
    route:
    - destination:
        host: edition.cnn.com
        port:
          number: 443
      weight: 100
EOF

訪問外部服務,下面使用了-L選項使請求端依照返回的重定向信息從新發起請求。第一個請求會發往http://edition.cnn.com/politics,服務端會返回重定向信息,第二個請求會按照重定向信息發往https://edition.cnn.com/politics。能夠看到第一次是HTTP訪問,第二次是HTTPS訪問。

若是沒有上述VirtualService,也能夠經過下面命令進行訪問。此處應該是爲了與下面例子結合。

$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics

HTTP/1.1 301 Moved Permanently
...
location: https://edition.cnn.com/politics
...

HTTP/2 200
...

上述過程會有兩個弊端:上面的第一個HTTP訪問顯然是冗餘的;若是在應用和edition.cnn.com 之間存在攻擊者,這樣該攻擊者就能夠經過嗅探鏈路獲取請求端執行的操做,存在安全風險。

使用istio的TLS源能夠解決如上問題。

爲Egress流量配置TLS源

從新定義 ServiceEntryVirtualService ,並增長DestinationRule來發起TLS。此時VirtualService會將HTTP請求流量從80端口重定向到DestinationRule的443端口,而後由DestinationRule來發起TLS。

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry # serviceEntry跟前面配置同樣
metadata:
  name: edition-cnn-com
spec:
  hosts:
  - edition.cnn.com #註冊到註冊中心的host。用於選擇virtualService和DestinationRule
  ports:
  - number: 80
    name: http-port
    protocol: HTTP
  - number: 443
    name: https-port-for-tls-origination
    protocol: HTTPS
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: edition-cnn-com #請求的hosts字段
spec:
  hosts:
  - edition.cnn.com #請求中的hosts字段內容
  http:
  - match:
    - port: 80 #後續將http流量經過destinationrule轉換爲https流量
    route:
    - destination:
        host: edition.cnn.com #此時定義了DestinationRule,會通過DestinationRule處理
        subset: tls-origination
        port:
          number: 443
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: edition-cnn-com
spec:
  host: edition.cnn.com #istio註冊表中的服務
  subsets:
  - name: tls-origination
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN
      portLevelSettings: #配置與上游服務edition.cnn.com的鏈接。即在443端口上使用tls SIMPLE進行鏈接
      - port:
          number: 443
        tls:
          mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com
EOF

http://edition.cnn.com/politics發送請求,能夠看到此時會返回200,且不會通過重定向,至關於作了一個代理。

$ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
HTTP/1.1 200 OK
...

固然直接使用https進行訪問也是能夠的,與上面使用http進行訪問的結果相同kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics

須要考慮的安全性問題

因爲應用和sidecar代理之間是沒有加密。所以滲透到應用所在的node節點的攻擊者仍然可以看到該節點上未加密的本地通訊內容。對於安全性較高的場景,建議應用直接使用HTTPS。

卸載

$ kubectl delete serviceentry edition-cnn-com
$ kubectl delete virtualservice edition-cnn-com
$ kubectl delete destinationrule edition-cnn-com
$ kubectl delete -f samples/sleep/sleep.yaml

Egress 網關

本節描述如何經過一個指定的egress網關訪問外部服務。istio使用ingress和egress網關在服務網格邊界配置負載均衡。一個ingress網關容許定義網格的入站點,egress網關的用法相似,定義了網格內流量的出站點。

使用場景

假設在一個安全要求比較高的組織中,全部離開服務網格的流量都要通過一個指定的節點(前面的egress訪問都是在離開pod以後按照k8s方式訪問,並無指定必須通過某個節點),這些節點會運行在指定的機器上,與運行應用的集羣的節點分開。這些特定的節點會在出站流量上應用策略,且對這些節點的監控將更加嚴格。

另一個場景是集羣中的應用所在的節點沒有公網IP,所以網格內部的服務沒法訪問因特網。定義一個egress網關併爲該網關所在的節點分配公網IP,這樣流量就能夠經過該節點訪問公網服務。

環境配置

建立sleep應用並獲取Pod名

$ kubectl apply -f samples/sleep/sleep.yaml
$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

部署Istio egress網關

校驗是否已經部署istio egress網關

$ kubectl get pod -l istio=egressgateway -n istio-system

若是沒有部署,執行以下步驟部署egress網關

$ istioctl manifest apply -f cni-annotations.yaml --set values.global.istioNamespace=istio-system --set values.gateways.istio-ingressgateway.enabled=true --set values.gateways.istio-egressgateway.enabled=true

注意:apply的時候使用本身定製化的文件,不然系統會使用默認的profile,致使配置丟失!

下面操做關於在default命名空間中爲egress網關建立destination rule,所以要求sleep應用也部署在default命名空間中。若是應用不在default命名空間中,將沒法在destination rule查找路徑找到destination rule,客戶端請求將會失敗。

HTTP流量的egress網關

  1. 上面例子中,當網格內的客戶端能夠直接訪問外部服務,此處將會建立一個egress網關,內部流量訪問外部服務時會通過該網關。建立一個ServiceEntry容許流量訪問外部服務edition.cnn.com:

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: cnn
    spec:
      hosts:
      - edition.cnn.com
      ports: #能夠經過HTTP和HTTPS服務外部服務
      - number: 80
        name: http-port
        protocol: HTTP
      - number: 443
        name: https
        protocol: HTTPS
      resolution: DNS
    EOF
  2. 校驗請求可以發往http://edition.cnn.com/politics,此處的操做與上一節相同。

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
    HTTP/1.1 301 Moved Permanently
    ...
    
    HTTP/2 200
    ...
  3. edition.cnn.com建立一個Gateway,端口80,監聽來自edition.cnn.com:80的流量。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: Gateway
    metadata:
      name: istio-egressgateway
    spec:
      selector:
        istio: egressgateway
      servers:
      - port: #監聽來自edition.cnn.com:80的流量,
          number: 80
          name: http
          protocol: HTTP
        hosts:
        - edition.cnn.com
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule #該DestinationRule沒有定義任何規則,實際能夠刪除該DestinationRule,並刪除下面VirtualService的"subset: cnn"一行
    metadata:
      name: egressgateway-for-cnn
    spec:
      host: istio-egressgateway.istio-system.svc.cluster.local
      subsets:
      - name: cnn #下面VirtualService中會用到
    EOF
  4. 定義VirtualService,將流量從sidecar定向到egress網關,而後將流量從egress網關定向到外部服務。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: direct-cnn-through-egress-gateway
    spec:
      hosts:
      - edition.cnn.com
      gateways: #列出應用路由規則的網關
      - istio-egressgateway
      - mesh #istio保留字段,表示網格中的全部sidecar,當忽略gateways字段時,默認會使用mesh,此處表示全部sidecar到edition.cnn.com的請求
      http: #採用http路由規則
      - match: #各個match是OR關係
        - gateways: #處理mesh網關,未來自mesh的edition.cnn.com:80請求發往istio-egressgateway.istio-system.svc.cluster.local:80
          - mesh
          port: 80
        route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: cnn #對應DestinationRule中的subset名,因爲使用了subset,所以必須使用DestinationRule。刪除該行後就能夠不使用上面的DestinationRule
            port:
              number: 80
          weight: 100
      - match:
        - gateways:
          - istio-egressgateway #處理istio-egressgateway網關,未來自gateway edition.cnn.com:80的請求發往edition.cnn.com:80
          port: 80
        route:
        - destination:
            host: edition.cnn.com #該host就對應serviceEntry註冊的服務地址
            port:
              number: 80
          weight: 100
    EOF
  5. 發送HTTP請求http://edition.cnn.com/politics

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
    HTTP/1.1 301 Moved Permanently
    ...
    
    HTTP/2 200
    ...
  6. 校驗egress日誌(須要啓用Envoy日誌)

    $ kubectl logs -l istio=egressgateway -c istio-proxy -n istio-system | tail
    [2020-08-25T14:55:49.810Z] "GET /politics HTTP/2" 301 - "-" "-" 0 0 1445 1444 "10.80.3.231" "curl/7.64.0" "2151bde2-4382-4e2f-b088-e464943c2a9b" "edition.cnn.com" "151.101.1.67:80" outbound|80||edition.cnn.com 10.80.3.232:51516 10.80.3.232:8080 10.80.3.231:38072 - -

本例中仍然實在sleep pod中執行HTTP請求,經過301重定向從新發送HTTPS請求,而上面規則中並無將HTTPs流程轉發給網關,所以從上面網關上看不到到443端口的流量,但能夠在sleep的istio-proxy sidecar的日誌中能夠看到完整的流量信息,以下:

[2020-08-25T14:55:33.114Z] "GET /politics HTTP/1.1" 301 - "-" "-" 0 0 310 310 "-" "curl/7.64.0" "d57ddf5f-985b-431a-8766-7481b75dc486" "edition.cnn.com" "151.101.1.67:80" outbound|80||edition.cnn.com 10.80.3.231:48390 151.101.65.67:80 10.80.3.231:44674 - default
[2020-08-25T14:55:33.439Z] "- - -" 0 - "-" "-" 906 1326852 5490 - "-" "-" "-" "-" "151.101.129.67:443" outbound|443||edition.cnn.com 10.80.3.231:47044 151.101.65.67:443 10.80.3.231:42990 edition.cnn.com -

卸載

$ kubectl delete gateway istio-egressgateway
$ kubectl delete serviceentry cnn
$ kubectl delete virtualservice direct-cnn-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-cnn

HTTPS流量的egress gateway

本節展現經過egress網關定向HTTPS流量,。會使用到ServiceEntry,一個egress Gateway和一個VirtualService

  1. 建立到edition.cnn.comServiceEntry,定義外部服務https://edition.cnn.com

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: cnn
    spec:
      hosts:
      - edition.cnn.com
      ports:
      - number: 443
        name: tls
        protocol: TLS #protocol爲TLS,用於非終結的流量
      resolution: DNS
    EOF

    protocol字段能夠爲HTTP|HTTPS|GRPC|HTTP2|MONGO|TCP|TLS其中之一,其中TLS 表示不會終止TLS鏈接,且鏈接會基於SNI首部進行路由。

  2. 校驗能夠經過ServiceEntry訪問https://edition.cnn.com/politics

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics
    HTTP/2 200
    ...
  3. edition.cnn.com建立egress Gateway,一個destination rule和一個virtual service。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: Gateway
    metadata:
      name: istio-egressgateway
    spec:
      selector:
        istio: egressgateway
      servers:
      - port:
          number: 443
          name: tls
          protocol: TLS #該字段與serviceEntry的字段相同
        hosts:
        - edition.cnn.com
        tls:
          mode: PASSTHROUGH #透傳模式,不在網關上終止TLS,由sidecar發起TLS
    ---
    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: #此處由http變爲了tls
      - match:
        - gateways:
          - mesh
          port: 443
          sniHosts:
          - edition.cnn.com #基於SNI的路由
        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 #指定tls的SNI
        route:
        - destination:
            host: edition.cnn.com
            port:
              number: 443
          weight: 100
    EOF

    因爲TLS自己是加密的,沒法像HTTP同樣根據host首部字段進行路由管理,所以採用了SNI擴展。SNI位於TLS協商的client-hello階段,做爲client-hello的擴展字段存在,基於TLS SNI的路由與基於HTTP host首部字段的路由管理,在邏輯上是相同的。SNI也支持通配符模式。

  4. 訪問https://edition.cnn.com/politics

    $ kubectl exec -it $SOURCE_POD -c sleep -- curl -sL -o /dev/null -D - https://edition.cnn.com/politics
    HTTP/2 200
    ...
  5. 校驗log

    $ kubectl logs -l istio=egressgateway -n istio-system
    ...
    [2020-06-02T09:06:43.152Z] "- - -" 0 - "-" "-" 906 1309129 1282 - "-" "-" "-" "-" "151.101.193.67:443" outbound|443||edition.cnn.com 10.83.1.219:39574 10.83.1.219:443 10.80.3.25:35492 edition.cnn.com -

卸載

$ 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網關出去的流量的安全性,僅能保證經過sidecar代理的流量的安全性。若是攻擊者繞過了sidecar代理,就能夠不通過egress網關直接訪問外部服務。此時,攻擊者的行爲不受istio的控制和監控。集羣管理員或雲供應商必須保證全部的流量都要通過egress網關。例如,集羣管理員能夠配置一個防火牆,拒絕全部非egress網關的流量。Kubernetes network policies也能夠禁止全部非egress網關的流量。此外,集羣管理員或雲供應商能夠配置網絡來保證應用節點只能經過網關訪問因特網,爲了實現這種效果,須要阻止將公共IP分配給網關之外的pod,並配置NAT設備丟棄非egress網關的報文。

使用Kubernetes network policies

本節展現如何建立一個Kubernetes network policy來防止繞過egress網關。爲了測試網絡策略,須要建立一個命名空間test-egress,部署sleep應用,並嘗試向網關安全的外部服務發送請求。

首先完成中的egress-gateway-for-https-traffic步驟,而後執行以下操做

  1. 建立test-egress命名空間

    $ kubectl create namespace test-egress
  2. sleep部署到test-egress命名空間中

    $ kubectl apply -n test-egress -f samples/sleep/sleep.yaml
  3. 校驗部署的pod不存在istio sidecar

    $ kubectl get pod $(kubectl get pod -n test-egress -l app=sleep -o jsonpath={.items..metadata.name}) -n test-egress
    NAME                    READY   STATUS    RESTARTS   AGE
    sleep-f8cbf5b76-g2t2l   1/1     Running   0          27s
  4. test-egress 命名空間中的sleep pod向https://edition.cnn.com/politics 發送HTTPS請求,返回200成功

    $ kubectl exec -it $(kubectl get pod -n test-egress -l app=sleep -o jsonpath={.items..metadata.name}) -n test-egress -c sleep -- curl -s -o /dev/null -w "%{http_code}\n"  https://edition.cnn.com/politics
    200
  5. 在istio組件所在的命名空間建立標籤,若是istio組件部署在istio-system命名空間中,則操做方式以下:

    $ kubectl label namespace istio-system istio=system
  6. kube-system命名空間打標籤

    $ kubectl label ns kube-system kube-system=true
  7. 部署一個NetworkPolicy限制從test-egress命名空間到istio-system命名空間和kube-system DNS服務的egress流量:

    $ cat <<EOF | kubectl apply -n test-egress -f -
    apiVersion: networking.k8s.io/v1
    kind: NetworkPolicy
    metadata:
      name: allow-egress-to-istio-system-and-kube-dns
    spec:
      podSelector: {}
      policyTypes:
      - Egress
      egress:
      - to:
        - namespaceSelector:
            matchLabels:
              kube-system: "true"
        ports:
        - protocol: UDP
          port: 53
      - to:
        - namespaceSelector:
            matchLabels:
              istio: system
    EOF
  8. 從新發送HTTPS請求到https://edition.cnn.com/politics,此時因爲network policy阻止了流量,請求會失敗。因爲sleep Pod沒法繞過istio-egressgateway(須要環境保證,若是環境上即便沒有istio-egressgateway也能訪問外部服務,則此處可能會與預期不同,本人使用的openshift環境沒法測試這種場景)訪問外部服務,惟一的方式是將流量定向到istio-egressgateway上。

    $ kubectl exec -it $(kubectl get pod -n test-egress -l app=sleep -o jsonpath={.items..metadata.name}) -n test-egress -c sleep -- curl -v https://edition.cnn.com/politics
    Hostname was NOT found in DNS cache
      Trying 151.101.65.67...
      Trying 2a04:4e42:200::323...
    Immediate connect fail for 2a04:4e42:200::323: Cannot assign requested address
      Trying 2a04:4e42:400::323...
    Immediate connect fail for 2a04:4e42:400::323: Cannot assign requested address
      Trying 2a04:4e42:600::323...
    Immediate connect fail for 2a04:4e42:600::323: Cannot assign requested address
      Trying 2a04:4e42::323...
    Immediate connect fail for 2a04:4e42::323: Cannot assign requested address
    connect to 151.101.65.67 port 443 failed: Connection timed out
  9. 如今給test-egress命名空間的sleep pod注入istio sidecar代理

    $ kubectl label namespace test-egress istio-injection=enabled
  10. 從新在test-egress命名空間中部署sleep deployment

    openshift環境須要首先執行以下步驟:

    $ cat <<EOF | oc -n test-egress create -f -
    apiVersion: "k8s.cni.cncf.io/v1"
    kind: NetworkAttachmentDefinition
    metadata:
      name: istio-cni
    EOF
    
    $ oc adm policy add-scc-to-group privileged system:serviceaccounts:test-egress
    $ oc adm policy add-scc-to-group anyuid system:serviceaccounts:test-egress

    部署sleep應用

    $ kubectl delete deployment sleep -n test-egress
    $ kubectl apply -f samples/sleep/sleep.yaml -n test-egress
  11. 校驗test-egress命名空間的sleep注入了istio sidecar

    $ kubectl get pod $(kubectl get pod -n test-egress -l app=sleep -o jsonpath={.items..metadata.name}) -n test-egress -o jsonpath='{.spec.containers[*].name}'
    sleep istio-proxy
  12. 建立與default命名空間中相同的destination rule,將流量定向到egress網關:

    $ kubectl apply -n test-egress -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: egressgateway-for-cnn
    spec:
      host: istio-egressgateway.istio-system.svc.cluster.local #內部服務地址,不須要用serviceEntry
      subsets:
      - name: cnn
    EOF
  13. 發送HTTPS請求到https://edition.cnn.com/politics:

    $ kubectl exec -it $(kubectl get pod -n test-egress -l app=sleep -o jsonpath={.items..metadata.name}) -n test-egress -c sleep -- curl -s -o /dev/null -w "%{http_code}\n" https://edition.cnn.com/politics
  14. 校驗egress網關代理的日誌

    $ kubectl logs -l istio=egressgateway -n istio-system
    ...
    [2020-06-02T09:04:11.239Z] "- - -" 0 - "-" "-" 906 1309030 1606 - "-" "-" "-" "-" "151.101.1.67:443" outbound|443||edition.cnn.com 10.83.1.219:56116 10.83.1.219:443 10.80.3.25:59032 edition.cnn.com -

卸載網絡策略

$ kubectl delete -f samples/sleep/sleep.yaml -n test-egress
$ kubectl delete destinationrule egressgateway-for-cnn -n test-egress
$ kubectl delete networkpolicy allow-egress-to-istio-system-and-kube-dns -n test-egress
$ kubectl label namespace kube-system kube-system-
$ kubectl label namespace istio-system istio-
$ kubectl delete namespace test-egress

帶TLS源的Egress網關(文件掛載)

在上一節的HTTPS流量的egress gateway中展現瞭如何配置istio來實現對外部服務的流量發起TLS。HTTP流量的egress網關中展現例子展現瞭如何配置istio來經過一個特定的egress網格服務來轉發egress流量。本節的例子將結合這兩個例子來描述如何配置一個egress網關來爲到外部服務的流量發起TLS。

部署

在default(已啓用sidecar自動注入)命名空間下安裝sleep

$ kubectl apply -f samples/sleep/sleep.yaml

獲取Pod名稱

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

建立egress網關

$ istioctl install -f cni-annotations.yaml --set values.global.istioNamespace=istio-system --set values.gateways.istio-egressgateway.enabled=true  --set meshConfig.accessLogFile="/dev/stdout"

使用一個egress網關發起TLS

本節描述如何使用於HTTPS流量的egress gateway相同的方式發起TLS,但此處使用了一個egress網關。注意這種狀況下,經過egress網關來發起TLS,而前面的例子中使用了sidecar發起TLS(curl時指定的是https://edition.cnn.com/politics)。

  1. edition.cnn.com定義一個ServiceEntry

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: cnn
    spec:
      hosts:
      - edition.cnn.com
      ports:
      - number: 80
        name: http
        protocol: HTTP
      - number: 443
        name: https
        protocol: HTTPS
      resolution: DNS
    EOF
  2. 校驗能夠經過建立的ServiceEntryhttp://edition.cnn.com/politics發送請求

    # kubectl exec "${SOURCE_POD}" -c sleep -- curl -sL -o /dev/null -D - http://edition.cnn.com/politics
    HTTP/1.1 301 Moved Permanently
    ...
    location: https://edition.cnn.com/politics
    ...
  3. edition.cnn.com建立一個Gateway,監聽edition.cnn.com:80,以及一個destination rule來處理sidecar到egress網關的請求

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: Gateway
    metadata:
      name: istio-egressgateway
    spec:
      selector:
        istio: egressgateway
      servers: #配置網關暴露的主機信息
      - port:
          number: 80
          name: https-port-for-tls-origination
          protocol: HTTPS
        hosts:
        - edition.cnn.com
        tls: 
          mode: ISTIO_MUTUAL #與網關的鏈接使用ISTIO_MUTUAL模式
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: egressgateway-for-cnn
    spec:
      host: istio-egressgateway.istio-system.svc.cluster.local
      subsets:
      - name: cnn
        trafficPolicy:
          loadBalancer:
            simple: ROUND_ROBIN
          portLevelSettings: #基於單個端口的流量策略
          - port:
              number: 80
            tls: #與上游服務的鏈接設置,即到網關的tls配置,使用ISTIO_MUTUAL模式
              mode: ISTIO_MUTUAL
              sni: edition.cnn.com #表示TLS鏈接的服務端
    EOF
  4. 定義一個VirtualService將流量轉移到egress網關,以及一個DestinationRule來爲到edition.cnn.com的請求發起TLS。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: direct-cnn-through-egress-gateway
    spec:
      hosts:
      - edition.cnn.com
      gateways:
      - istio-egressgateway
      - mesh
      http:
      - match:
        - gateways: #處理來自網格內部全部到edition.cnn.com的流量,發送到egress網關,並使用subset: cnn進行處理
          - mesh
          port: 80
        route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: cnn
            port:
              number: 80
          weight: 100
      - match:
        - gateways:
          - istio-egressgateway #處理來自網關istio-egressgateway的流量,直接發往edition.cnn.com
          port: 80
        route:
        - destination:
            host: edition.cnn.com
            port:
              number: 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 # 網關到edition.cnn.com使用SIMPLE模式,因爲edition.cnn.com是網格外部服務,所以不能使用ISTIO_MUTUAL
    EOF

    整個過程爲:網格內部HTTP流量->istio-egressgateway(配置TLS)->發起TLS鏈接

  5. 發送HTTP請求到http://edition.cnn.com/politics

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

    此時的輸出中不包含301 Moved Permanently 消息

  6. 校驗istio-egressgateway pod的日誌

    $ kubectl logs -l istio=egressgateway -c istio-proxy -n istio-system | tail

    能夠看到以下輸出:

    [2020-08-25T15:16:17.840Z] "GET /politics HTTP/2" 200 - "-" "-" 0 1297688 7518 460 "10.80.3.231" "curl/7.64.0" "2c71707e-3304-418c-840e-c37256c1ad41" "edition.cnn.com" "151.101.193.67:443" outbound|443||edition.cnn.com 10.80.3.232:38522 10.80.3.232:8080 10.80.3.231:46064 edition.cnn.com -

各類資源的tls設置:

資源 描述
virtualService tls字段:用於非終結TLS&HTTPS流量的路由規則。一般使用ClientHello消息中的SNI值進行路由。TLS路由將會應用於端口名爲https - tls-的平臺服務,使用HTTPS/TLS協議的非終結網關端口(使用passthroughTLS模式),以及使用HTTPS/TLS協議的serviceEntry端口。注:不關聯virtual service的https-tls-端口的流量將被視爲不透明的TCP流量。
DestinationRule DestinationRule主要對鏈接上游服務的tls進行配置,包含網格內的網關以及網格外的服務
ClientTLSSettings字段:鏈接上游的SSL/TLS相關設置
portLevelSettings字段:按照端口對上游服務進行設置,該字段包含了ClientTLSSettings字段
Gateway Gateway主要暴露的服務的tls進行配置,含ingress和egress,前者一般可使用SIMPLE/MUTUAL模式,後者可使用SIMPLE/MUTUAL/ISTIO_MUTUAL模式
ServerTLSSettings
字段:控制服務端行爲的TLS相關選項集。使用這些選項來控制是否應將全部http請求重定向到https,並使用TLS模式

卸載

$ kubectl delete gateway istio-egressgateway
$ kubectl delete serviceentry cnn
$ kubectl delete virtualservice direct-cnn-through-egress-gateway
$ kubectl delete destinationrule originate-tls-for-edition-cnn-com
$ kubectl delete destinationrule egressgateway-for-cnn

使用egress網關發起mutual TLS

與前面章節相似,本節描述如何配置egress網關來向外部服務發起TLS,不一樣的是此次要使用mutual TLS(上面用的是SIMPLE模式)。

在本例中首先須要:

  1. 生成client和server證書
  2. 部署支持mutual TLS協議的外部服務
  3. 從新部署egress網關,使用mutual TLS證書

而後就是經過egress 網關發起TLS。

生成client和server的證書和key

  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 -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
  3. 生成client證書和私鑰

    $ 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 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in client.example.com.csr -out client.example.com.crt

部署mutual TLS server

爲了模擬一個支持mutual TLS協議的外部服務,須要在kubernetes集羣中部署一個NGINX服務,但該服務位於istio服務網格外,即位於一個沒有啓用istio sidecar代理注入的命名空間。

  1. 建立一個惟一istio網格外的命名空間,名爲mesh-external,該命名空間不啓用sidecar自動注入。

    $ kubectl create namespace mesh-external
  2. 建立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
  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 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
      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

使用client證書從新部署egress網關

  1. 建立kubernetes secret,包含客戶端證書和CA證書

    $ kubectl create -n istio-system secret tls nginx-client-certs --key client.example.com.key --cert client.example.com.crt
    $ kubectl create -n istio-system secret generic nginx-ca-certs --from-file=example.com.crt
  2. 更新istio-egressgateway deployment來掛載建立的secret。建立以下gateway-patch.json文件來給istio-egressgateway deployment打補丁。

    cat > gateway-patch.json <<EOF
    [{
      "op": "add",
      "path": "/spec/template/spec/containers/0/volumeMounts/0",
      "value": {
        "mountPath": "/etc/istio/nginx-client-certs",
        "name": "nginx-client-certs",
        "readOnly": true
      }
    },
    {
      "op": "add",
      "path": "/spec/template/spec/volumes/0",
      "value": {
      "name": "nginx-client-certs",
        "secret": {
          "secretName": "nginx-client-certs",
          "optional": true
        }
      }
    },
    {
      "op": "add",
      "path": "/spec/template/spec/containers/0/volumeMounts/1",
      "value": {
        "mountPath": "/etc/istio/nginx-ca-certs",
        "name": "nginx-ca-certs",
        "readOnly": true
      }
    },
    {
      "op": "add",
      "path": "/spec/template/spec/volumes/1",
      "value": {
      "name": "nginx-ca-certs",
        "secret": {
          "secretName": "nginx-ca-certs",
          "optional": true
        }
      }
    }]
    EOF
  3. 使用以下命令使補丁生效

    $ kubectl -n istio-system patch --type=json deploy istio-egressgateway -p "$(cat gateway-patch.json)"
  4. 校驗加載到istio-egressgateway pod中的密鑰和證書

    $ kubectl exec -n istio-system "$(kubectl -n istio-system get pods -l istio=egressgateway -o jsonpath='{.items[0].metadata.name}')" -- ls -al /etc/istio/nginx-client-certs /etc/istio/nginx-ca-certs

    tls.crttls.key 應該位於 /etc/istio/nginx-client-certs目錄中,而 ca-chain.cert.pem 位於/etc/istio/nginx-ca-certs目錄中。

配置egress流量的mutual TLS源

  1. my-nginx.mesh-external.svc.cluster.local:443建立一個egress Gateway,以及destination rules和virtual service來將流量轉發到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 #暴露給網格內部服務地址,使用ISTIO_MUTUAL進行交互
        tls:
          mode: ISTIO_MUTUAL
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule #處理網格內部pod到網關的流量
    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
  2. 定義一個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: #內部流量採用http協議,由網關進行mutual tls協商
      - 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
  3. 添加一個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 #使用MUTUAL模式鏈接外部服務,證書位於網關pod中
            clientCertificate: /etc/istio/nginx-client-certs/tls.crt
            privateKey: /etc/istio/nginx-client-certs/tls.key
            caCertificates: /etc/istio/nginx-ca-certs/example.com.crt
            sni: my-nginx.mesh-external.svc.cluster.local
    EOF
  4. 發送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 -s http://my-nginx.mesh-external.svc.cluster.local
    <!DOCTYPE html>
    <html>
    <head>
    <title>Welcome to nginx!</title>
    ...
  5. 校驗istio-egressgateway pod的日誌

    # kubectl logs -l istio=egressgateway -n istio-system | grep 'my-nginx.mesh-external.svc.cluster.local' | grep HTTP
    [2020-08-26T08:26:15.054Z] "GET / HTTP/1.1" 200 - "-" "-" 0 612 4 4 "10.80.3.231" "curl/7.64.0" "e8bf12bd-9c39-409e-a837-39afc151fc7e" "my-nginx.mesh-external.svc.cluster.local" "10.80.2.14:443" outbound|443||my-nginx.mesh-external.svc.cluster.local 10.80.2.15:56608 10.80.2.15:8443 10.80.3.231:50962 my-nginx.mesh-external.svc.cluster.local -

卸載

$ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
$ kubectl delete secret istio-egressgateway-certs istio-egressgateway-ca-certs -n istio-system
$ 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 gateway istio-egressgateway
$ kubectl delete virtualservice direct-nginx-through-egress-gateway
$ kubectl delete destinationrule -n istio-system originate-mtls-for-nginx
$ kubectl delete destinationrule egressgateway-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
$ rm ./gateway-patch.json
$ kubectl delete service sleep
$ kubectl delete deployment sleep

帶TLS源的Egress網關(SDS)

本節展現如何經過配置一個egress網關來爲到外部服務的流量發起TLS。使用Secret Discovery Service (SDS)來配置私鑰,服務證書以及根證書(上一節中使用文件掛載方式來管理證書)。

部署

部署sleep應用,並獲取其Pod名

$ kubectl apply -f samples/sleep/sleep.yaml
$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

egress網關使用SDS發起TLS

生成CA和server證書和密鑰

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

部署一個simple 模式的TLS服務

下面的操做與使用文件掛載相同,部署一個NGINX服務

  1. 建立istio網格外的命名空間mesh-external

    $ kubectl create namespace mesh-external
  2. 建立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
  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 off; # simple TLS下server不須要校驗client的證書
      }
    }
    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
    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

爲egress流量發起simple TLS

  1. 建立一個kubernetes Secret來保存egress網格發起TLS使用的CA證書,因爲使用的是SIMPLE模式,所以無需客戶端證書,僅對ca證書實現SDS,後續在網關的destinationRule中使用。

    $ kubectl create secret generic client-credential-cacert --from-file=ca.crt=example.com.crt -n istio-system

    注意,Istio-only-CA證書的secret名稱必須以-cacert結尾,而且必須在與部署的Istio相同的命名空間(默認爲Istio-system)中建立該secret。

    secret名稱不該該以istioprometheus開頭,且secret不能包含token字段

    下面的配置除最後一個destinationRule外,其他配置都與上一節相同

  2. my-nginx.mesh-external.svc.cluster.local:443建立一個egress Gateway,以及destination rules和virtual service來將流量轉發到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
  3. 定義一個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
  4. 添加一個DestinationRule來發起TLS

    $ kubectl apply -n istio-system -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: originate-tls-for-nginx
    spec:
      host: my-nginx.mesh-external.svc.cluster.local
      trafficPolicy:
        loadBalancer:
          simple: ROUND_ROBIN
        portLevelSettings:
        - port:
            number: 443
          tls:
            mode: SIMPLE
            credentialName: client-credential # 對應前面建立的包含ca證書的secret client-credential-cacert,但此時不帶"-cacert"後綴
            sni: my-nginx.mesh-external.svc.cluster.local #網格外部服務
    EOF
  5. 發送一個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 -s http://my-nginx.mesh-external.svc.cluster.local
    <!DOCTYPE html>
    <html>
    <head>
    <title>Welcome to nginx!</title>
    ...
  6. 檢查istio-egressgateway中的訪問日誌

    # kubectl logs -l istio=egressgateway -n istio-system | grep 'my-nginx.mesh-external.svc.cluster.local' | grep HTTP
    [2020-08-26T12:26:09.316Z] "GET / HTTP/1.1" 200 - "-" "-" 0 612 3 3 "10.80.3.231" "curl/7.64.0" "67803676-5617-4e12-a14a-5cef95ea2e87" "my-nginx.mesh-external.svc.cluster.local" "10.80.2.19:443" outbound|443||my-nginx.mesh-external.svc.cluster.local 10.80.2.15:40754 10.80.2.15:8443 10.80.3.231:57626 my-nginx.mesh-external.svc.cluster.local -

卸載

$ kubectl delete destinationrule originate-tls-for-nginx -n istio-system
$ kubectl delete virtualservice direct-nginx-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-nginx
$ kubectl delete gateway istio-egressgateway
$ kubectl delete secret client-credential-cacert -n istio-system
$ kubectl delete service my-nginx -n mesh-external
$ kubectl delete deployment my-nginx -n mesh-external
$ kubectl delete configmap nginx-configmap -n mesh-external
$ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
$ kubectl delete namespace mesh-external
$ 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
$ rm ./nginx.conf

egress網關使用SDS發起mutual TLS

建立客戶端和服務端證書和密鑰

下面操做跟前面同樣,建立CA和客戶端,服務端證書

$ 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

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

$ 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 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in client.example.com.csr -out client.example.com.crt

部署一個mutual TLS服務端

下面的配置也跟以前同樣

$ kubectl create namespace mesh-external
$ 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
$ 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; # mutual TLS下的server會校驗client的證書
  }
}
EOF
$ kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
$ 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
  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

配置egress流量使用SDS發起mutual TLS

  1. 建立一個kubernetes secret來保存客戶端證書和ca證書

    $ 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

    使用SDS的secret名稱跟上一節的要求同樣,部署到istio所在的命名空間,且名稱不能以istioprometheus開頭,不能包含token字段。

  2. my-nginx.mesh-external.svc.cluster.local:443建立Gateway

    $ 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
  3. 建立VirtualService

    $ 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
  4. 與前面不一樣點就在該DestinationRule中的credentialName字段,包含了前面建立的證書client-credential

    $ 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 # this must match the secret created earlier to hold client certs
            sni: my-nginx.mesh-external.svc.cluster.local
    EOF
  5. 發送請求並校驗egressgateway pod的日誌

    $ kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -s http://my-nginx.mesh-external.svc.cluster.local
    $ kubectl logs -l istio=egressgateway -n istio-system | grep 'my-nginx.mesh-external.svc.cluster.local' | grep HTTP

卸載

$ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
$ kubectl delete secret client-credential -n istio-system
$ 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 gateway istio-egressgateway
$ kubectl delete virtualservice direct-nginx-through-egress-gateway
$ kubectl delete destinationrule -n istio-system originate-mtls-for-nginx
$ kubectl delete destinationrule egressgateway-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
$ rm ./gateway-patch.json
$ kubectl delete service sleep
$ kubectl delete deployment sleep

使用通配符主機的egress

上兩節中爲網關配置了特定的主機名,如 edition.cnn.com。本節將展現如何爲egress流量配置位於同域的一組主機,如*.wikipedia.org

背景說明

假設要在istio上爲全部語言的wikipedia.org站點啓用egress流量,每一個特定語言的wikipedia.org站點都有其各自的主機名,如en.wikipedia.orgde.wikipedia.org分別表示英文和德文。此時可能會但願爲全部的Wikipedia egress流量配置相同的參數,而不須要爲每種語言的站點單獨指定。

原文中用於展現的站點爲*.wikipedia.org,但鑑於這類站點在國內沒法訪問,故修改成*.baidu.com

部署

部署sleep應用並獲取POD名稱

$ kubectl apply -f samples/sleep/sleep.yaml
$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

配置到通配符主機的直接流量

首先,爲了簡化場景,建立一個帶通配符主機的ServiceEntry,並直接訪問服務。當直接調用服務時(不通過egress網關),通配符主機的配置與其餘主機並無什麼不一樣(只是對同一域中的主機的服務更加方便)。

*.baidu.com定義一個ServiceEntry和相應的VirtualSevice

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: baidu
spec:
  hosts:
  - "*.baidu.com" #通配符主機
  ports:
  - number: 443
    name: tls
    protocol: TLS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: baidu
spec:
  hosts:
  - "*.baidu.com"
  tls:
  - match:
    - port: 443
      sniHosts:
      - "*.baidu.com"
    route:
    - destination:
        host: "*.baidu.com"
        port:
          number: 443
EOF

發送請求給https://map.baidu.com/和https://fanyi.baidu.com/:

# kubectl exec -it $SOURCE_POD -c sleep -- sh -c 'curl -s https://map.baidu.com/ | grep -o "<title>.*</title>"; curl -s https://fanyi.baidu.com/ | grep -o "<title>.*</title>"'
<title>百度地圖</title>
<title>百度翻譯-200種語言互譯、溝通全世界!</title>

能夠不使用VirtualService

卸載

$ kubectl delete serviceentry baidu
$ kubectl delete virtualservice baidu

配置到通配符主機的egress網關流量

經過egress網關訪問通配符主機的配置取決於通配符域集是否由一個公共主機來提供服務。例如*.wikipedia.org,全部指定語言的站點都由*wikipedia.org的某一個服務端提供服務,這樣就能夠將流量路由到任何*.wikipedia.org站點對應的IP(包括www.wikipedia.org)。

因爲map.baidu.com和fanyi.baidu.com的服務並非由www.baidu.com對應的某個IP服務的(可使用nslookup或dig命令查看),所以沒法用於測試本場景,下面爲官網內容

通常狀況下,若是一個通配符的全部域名不是由一個託管服務器提供服務的,則須要更復雜的配置。

單個主機服務器的通配符配置

當一個服務端服務全部的通配符主機時,對使用egress網關訪問通配符主機的配置與訪問非通配符主機的配置相似。

  1. *.wikipedia.org,建立一個egress Gateway,destination rule和一個virtual service,將流量導入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: tls
          protocol: TLS
        hosts:
        - "*.wikipedia.org"
        tls:
          mode: PASSTHROUGH #由網格內部發起https請求,非終結TLS
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: egressgateway-for-wikipedia
    spec:
      host: istio-egressgateway.istio-system.svc.cluster.local
      subsets:
        - name: wikipedia
    ---
    apiVersion: networking.istio.io/v1alpha3
    kind: VirtualService
    metadata:
      name: direct-wikipedia-through-egress-gateway
    spec:
      hosts:
      - "*.wikipedia.org"
      gateways:
      - mesh
      - istio-egressgateway
      tls: #網格內部的TLS流量處理
      - match:
        - gateways:
          - mesh
          port: 443
          sniHosts:
          - "*.wikipedia.org"
        route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: wikipedia
            port:
              number: 443
          weight: 100
      - match:
        - gateways:
          - istio-egressgateway
          port: 443
          sniHosts:
          - "*.wikipedia.org"
        route:
        - destination:
            host: www.wikipedia.org #將流量從網格傳給外部服務
            port:
              number: 443
          weight: 100
    EOF
  2. 爲目的服務www.wikipedia.com建立ServiceEntry

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: ServiceEntry
    metadata:
      name: www-wikipedia
    spec:
      hosts:
      - www.wikipedia.org
      ports:
      - number: 443
        name: tls
        protocol: TLS
      resolution: DNS
    EOF
  3. 發送請求給https://map.baidu.com/和https://fanyi.baidu.com/:

    $ kubectl exec -it $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>
卸載
$ kubectl delete serviceentry www-wikipedia
$ kubectl delete gateway istio-egressgateway
$ kubectl delete virtualservice direct-wikipedia-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-wikipedia

任意域的通配符配置

上一節中的配置之因此可以生效,是由於任何一個wikipedia.org服務端均可以服務全部的*.wikipedia.org站點。然有些狀況下不是這樣的,例若有可能但願訪問更加通用的域,如.com.org

在istio網關上配置到任意通配符的域會帶來挑戰,上一節中直接將流量傳遞給了 www.wikipedia.org(直接配置到了網關上)。受限於Envoy(默認的istio egress網關代理),網關並不知道接收到的請求中的任意主機的IP地址。Envoy會將流量路由到預約義的主機預約義的IP地址請求中的原始目的IP地址。在網關場景下,因爲請求會首先被路由到egress網關上,所以會丟失請求中的原始目的IP地址,並將目的IP地址替換爲網關的IP地址,最終會致使基於Envoy的istio網關沒法路由到沒有進行預配置的任意主機,進而致使沒法爲任意通配符域執行流量控制。

爲了給HTTPS和TLS啓用流量控制,須要額外部署一個SNI轉發代理。Envoy會將到通配符域的請求路由到SNI轉發代理,而後將請求轉發到SNI中指定的目的地。

使用SNI代理和相關組件的egress網關架構以下,因爲Envoy沒法處理任意通配符的主機,所以須要轉發到SNI代理上進行SNI的路由處理。

下面將展現如何從新部署egress網關來使用SNI代理,並配置istio經過網關路由HTTPS流量到任意通配符域。

配置egress網關的SNI代理

本節中將在標準的istio Envoy代理以外部署爲egress網關部署一個SNI代理。本例中使用Nginx做爲SNI代理,該SNI代理將會監聽8443端口,而後將流量轉發到443端口。

  1. 爲Nginx SNI代理建立配置文件。注意server下的listen指令爲8443,proxy_pass指令使用ssl_preread_server_name,端口443以及將ssl_preread設置爲on來啓用SNI reading。

    $ cat <<EOF > ./sni-proxy.conf
    user www-data;
    
    events {
    }
    
    stream {
      log_format log_stream '\$remote_addr [\$time_local] \$protocol [\$ssl_preread_server_name]'
      '\$status \$bytes_sent \$bytes_received \$session_time';
    
      access_log /var/log/nginx/access.log log_stream;
      error_log  /var/log/nginx/error.log;
    
      # tcp forward proxy by SNI
      server {
        resolver 8.8.8.8 ipv6=off;
        listen       127.0.0.1:8443;
        proxy_pass   \$ssl_preread_server_name:443;
        ssl_preread  on;
      }
    }
    EOF
  2. 建立一個kubernets ConfigMap來保存Nginx SNI代理的配置

    $ kubectl create configmap egress-sni-proxy-configmap -n istio-system --from-file=nginx.conf=./sni-proxy.conf
  3. 下面命令將生成 istio-egressgateway-with-sni-proxy.yaml

本例由於官方isitoOperator格式有變而沒法運行,官方可能須要修改代碼,參見此issue

配置使用SNI代理的egress網關的路由轉發

....

卸載
$ kubectl delete serviceentry wikipedia
$ kubectl delete gateway istio-egressgateway-with-sni-proxy
$ kubectl delete virtualservice direct-wikipedia-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-wikipedia
$ kubectl delete --ignore-not-found=true envoyfilter forward-downstream-sni egress-gateway-sni-verifier
$ kubectl delete serviceentry sni-proxy
$ kubectl delete destinationrule disable-mtls-for-sni-proxy
$ kubectl delete -f ./istio-egressgateway-with-sni-proxy.yaml
$ kubectl delete configmap egress-sni-proxy-configmap -n istio-system
$ rm ./istio-egressgateway-with-sni-proxy.yaml
$ rm ./sni-proxy.conf

卸載

$ kubectl delete -f samples/sleep/sleep.yaml

Egress流量的kubernetes服務

kubernetes ExternalName services 和帶Endpoints的kubernetes services 容許爲外部服務建立本地DNS別名,該DNS別名的格式與本地服務的DNS表項的格式相同,即 <service name>.<namespace name>.svc.cluster.local。DNS別名爲工做負載提供了位置透明性:負載能夠經過這種方式調用本地和外部服務。若是某個時間須要在集羣中部署外部服務,就能夠經過更新該kubernetes service來引用本地版本。工做負載將繼續運行,不須要作任何改變。

本任務將展現istio如何使用這些kubernetes機制來訪問外部服務。不過此時必須使用TLS模式來進行訪問,而不是istio的mutual TLS。由於外部服務不是istio服務網格的一部分,所以不能使用istio mutual TLS,必須根據外部服務的須要以及負載訪問外部服務的方式來設置TLS模式。若是負載發起明文HTTP請求,但外部服務須要TLS,此時可能須要istio發起TLS。若是負載已經使用了TLS,那麼流量已經通過加密,此時就能夠禁用istio的mutual TLS。

本節描述如何將istio集成到現有kubernetes配置中

雖然本例使用了HTTP協議,但使用egress流量的kubernetes Services也可使用其餘協議。

部署

  • 部署sleep應用並獲取POD名稱

    $ kubectl apply -f samples/sleep/sleep.yaml
    $ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
  • 建立一個不使用istio的命名空間

    $ kubectl create namespace without-istio
  • without-istio命名空間下啓用sleep

    $ kubectl apply -f samples/sleep/sleep.yaml -n without-istio
  • 建立一個環境變量SOURCE_POD_WITHOUT_ISTIO來保存without-istio命名空間下的pod名稱

    $ export SOURCE_POD_WITHOUT_ISTIO="$(kubectl get pod -n without-istio -l app=sleep -o jsonpath={.items..metadata.name})"
  • 校驗該pod沒有istio sidecar

    # kubectl get pod "$SOURCE_POD_WITHOUT_ISTIO" -n without-istio
    NAME                    READY   STATUS    RESTARTS   AGE
    sleep-f8cbf5b76-tbptz   1/1     Running   0          53s

經過kubernetes ExternalName service訪問外部服務

  1. default命名空間中爲httpbin.org建立一個kubernetes ExternalName service,將外服服務httpbin.org映射爲kubernetes服務my-httpbin,便可以經過訪問my-httpbin.default.svc.cluster.local來訪問my-httpbin

    $ kubectl apply -f - <<EOF
    kind: Service
    apiVersion: v1
    metadata:
      name: my-httpbin
    spec:
      type: ExternalName
      externalName: httpbin.org
      ports:
      - name: http
        protocol: TCP
        port: 80
    EOF
  2. 觀察service,能夠看到並無cluster IP

    # kubectl get svc my-httpbin
    NAME         TYPE           CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
    my-httpbin   ExternalName   <none>       httpbin.org   80/TCP    3s
  3. 在源pod(不帶istio sidecar)中經過kubernetes的service主機名訪問 httpbin.org。因爲在網格中,所以不須要訪問時禁用istio 的mutual TLS。

    # kubectl exec "$SOURCE_POD_WITHOUT_ISTIO" -n without-istio -c sleep -- curl my-httpbin.default.svc.cluster.local/headers
    {
      "headers": {
        "Accept": "*/*",
        "Host": "my-httpbin.default.svc.cluster.local",
        "User-Agent": "curl/7.64.0",
        "X-Amzn-Trace-Id": "Root=1-5f485a71-54548d2e5f8b0dc1002e2ce0"
      }
    }
  4. 本例中,使用向 httpbin.org 發送了未加密的HTTP請求。爲了簡單例子,下面禁用了TLS模式,容許向外部服務發送未加密的流量。在實際使用時,建議配置Egress TLS源,下面至關於將流量重定向到內部服務my-httpbin.default.svc.cluster.local上,而my-httpbin.default.svc.cluster.local映射了外部服務 httpbin.org,這樣就能夠經過這種方式經過kubernetes service來訪問外部服務。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: my-httpbin
    spec:
      host: my-httpbin.default.svc.cluster.local
      trafficPolicy:
        tls:
          mode: DISABLE
    EOF
  5. 在帶istio sidecar的pod中經過kubernetes service主機名訪問httpbin.org,注意isito sidecar添加的首部,如 X-Envoy-Decorator-Operation。同時注意Host首部字段等於本身的service主機名。

    # kubectl exec "$SOURCE_POD" -c sleep -- curl my-httpbin.default.svc.cluster.local/headers
    {
      "headers": {
        "Accept": "*/*",
        "Content-Length": "0",
        "Host": "my-httpbin.default.svc.cluster.local",
        "User-Agent": "curl/7.64.0",
        "X-Amzn-Trace-Id": "Root=1-5f485d05-ac81b19dcee92359b5cae307",
        "X-B3-Sampled": "0",
        "X-B3-Spanid": "bee9babd29c28cec",
        "X-B3-Traceid": "b8260c4ba5390ed0bee9babd29c28cec",
        "X-Envoy-Attempt-Count": "1",
        "X-Envoy-Decorator-Operation": "my-httpbin.default.svc.cluster.local:80/*",
        "X-Envoy-Peer-Metadata": "ChoKCkNMVVNURVJfSUQSDBoKS3ViZXJuZXRlcwo2CgxJTlNUQU5DRV9JUFMSJhokMTAuODAuMi4yNixmZTgwOjozMGI4OmI3ZmY6ZmUxNDpiMjE0Ct4BCgZMQUJFTFMS0wEq0AEKDgoDYXBwEgcaBXNsZWVwChkKDGlzdGlvLmlvL3JldhIJGgdkZWZhdWx0CiAKEXBvZC10ZW1wbGF0ZS1oYXNoEgsaCWY4Y2JmNWI3NgokChlzZWN1cml0eS5pc3Rpby5pby90bHNNb2RlEgcaBWlzdGlvCioKH3NlcnZpY2UuaXN0aW8uaW8vY2Fub25pY2FsLW5hbWUSBxoFc2xlZXAKLwojc2VydmljZS5pc3Rpby5pby9jYW5vbmljYWwtcmV2aXNpb24SCBoGbGF0ZXN0ChoKB01FU0hfSUQSDxoNY2x1c3Rlci5sb2NhbAofCgROQU1FEhcaFXNsZWVwLWY4Y2JmNWI3Ni13bjlyNwoWCglOQU1FU1BBQ0USCRoHZGVmYXVsdApJCgVPV05FUhJAGj5rdWJlcm5ldGVzOi8vYXBpcy9hcHBzL3YxL25hbWVzcGFjZXMvZGVmYXVsdC9kZXBsb3ltZW50cy9zbGVlcAoaCg9TRVJWSUNFX0FDQ09VTlQSBxoFc2xlZXAKGAoNV09SS0xPQURfTkFNRRIHGgVzbGVlcA==",
        "X-Envoy-Peer-Metadata-Id": "sidecar~10.80.2.26~sleep-f8cbf5b76-wn9r7.default~default.svc.cluster.local"
      }
    }

卸載

$ kubectl delete destinationrule my-httpbin
$ kubectl delete service my-httpbin

使用帶endpoints的kubernetes service訪問一個外部服務

  1. map.baidu.com建立一個kubernetes service,不帶selector

    $ kubectl apply -f - <<EOF
    kind: Service
    apiVersion: v1
    metadata:
      name: my-baidu-map
    spec:
      ports:
      - protocol: TCP
        port: 443
        name: tls
    EOF
  2. 爲外部服務手動建立endpoints,IP來自map.baidu.com後端地址。此時能夠經過kubernetes service直接訪問外部服務

    # nslookup map.baidu.com
    Server:         100.100.2.136
    Address:        100.100.2.136#53
    
    Non-authoritative answer:
    map.baidu.com   canonical name = map.n.shifen.com.
    Name:   map.n.shifen.com
    Address: 180.101.49.69
    $ kubectl apply -f - <<EOF
    kind: Endpoints
    apiVersion: v1
    metadata:
      name: my-baidu-map
    subsets:
      - addresses:
          - ip: 180.101.49.69
        ports:
          - port: 443
            name: tls
    EOF
  3. 觀測上述service,能夠經過其cluster IP訪問map.baidu.com

    # oc get svc my-baidu-map
    NAME           TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
    my-baidu-map   ClusterIP   10.84.20.176   <none>        443/TCP   116s
  4. 從不帶istio sidecar的pod中向map.baidu.com發送HTTPS請求。注意下面curl在訪問map.baidu.com時使用了--resolve選項

    # kubectl exec "$SOURCE_POD_WITHOUT_ISTIO" -n without-istio -c sleep -- curl -s --resolve map.baidu.com:443:"$(kubectl get service my-baidu-map -o jsonpath='{.spec.clusterIP}')" https://map.baidu.com | grep -o "<title>.*</title>"
    <title>百度地圖</title>
  5. 這種狀況下,負載會直接向map.baidu.com發送HTTPS請求,此時能夠安全禁用istio的mutual TLS(固然也能夠不由用,此時不須要部署destinationRule)

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: my-baidu-map
    spec:
      host: my-baidu-map.default.svc.cluster.local
      trafficPolicy:
        tls:
          mode: DISABLE
    EOF
  6. 從帶istio sidecar的pod中訪問map.baidu.com

    # kubectl exec "$SOURCE_POD" -c sleep -- curl -s --resolve map.baidu.com:443:"$(kubectl get service my-baidu-map -o jsonpath='{.spec.clusterIP}')" https://map.baidu.com  | grep -o "<title>.*</title>"
    <title>百度地圖</title>
  7. 校驗請求確實是經過cluster IP(10.84.20.176)進行訪問的。

    # kubectl exec "$SOURCE_POD" -c sleep -- curl -v --resolve map.baidu.com:443:"$(kubectl get service my-baidu-map -o jsonpath='{.spec.clusterIP}')" https://map.baidu.com -o /dev/null
    * Expire in 0 ms for 6 (transfer 0x562c95903680)
    * Added map.baidu.com:443:10.84.20.176 to DNS cache
    * Hostname map.baidu.com was found in DNS cache
    *   Trying 10.84.20.176...
    * TCP_NODELAY set

卸載

$ kubectl delete destinationrule my-baidu-map
$ kubectl delete endpoints my-baidu-map
$ kubectl delete service my-baidu-map

卸載

$ kubectl delete -f samples/sleep/sleep.yaml
$ kubectl delete -f samples/sleep/sleep.yaml -n without-istio
$ kubectl delete namespace without-istio
$ unset SOURCE_POD SOURCE_POD_WITHOUT_ISTIO

使用外部HTTPS代理

在前面配置Egress網關的例子中展現瞭如何經過istio邊界組件Egress網關將流量轉發到外部服務中。然而,可是,有些狀況下須要外部的、遺留的(非Istio)HTTPS代理來訪問外部服務。例如,公司可能已經部署了一個代理,全部組織中的應用都必須經過該代理來轉發流量。

本例展現如何經過外部代理轉發流量。因爲全部的因爲都會使用HTTP CONNECT方法來與HTTPS代理創建鏈接,配置流量到一個外部代理不一樣於配置流量到外部HTTP和HTTPS服務。

部署

建立sleep應用並獲取POD名稱

$ kubectl apply -f samples/sleep/sleep.yaml
$ export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

部署一個HTTPS代理

爲了模擬一個遺留的代理,須要在集羣中部署HTTPS代理。爲了模擬在集羣外部運行的更真實的代理,須要經過代理的IP地址而不是Kubernetes服務的域名來定位代理的pod。本例使用Squid,但也可使用其餘HTTPS代理來支持HTTP CONNECT。

  1. 爲HTTPS代理建立一個命名空間,不啓用istio sidecar自動注入。使用這種方式來模擬集羣外的代理。

    $ kubectl create namespace external
  2. 建立Squid代理的配置文件

    $ cat <<EOF > ./proxy.conf
    http_port 3128
    
    acl SSL_ports port 443
    acl CONNECT method CONNECT
    
    http_access deny CONNECT !SSL_ports
    http_access allow localhost manager
    http_access deny manager
    http_access allow all
    
    coredump_dir /var/spool/squid
    EOF
  3. 建立一個kubernetes ConfigMap來保存代理的配置

    $ kubectl create configmap proxy-configmap -n external --from-file=squid.conf=./proxy.conf
  4. 部署Squid容器。注:openshift可能會由於scc致使權限錯誤,爲方便測試,將容器設置爲privileged權限

    # oc adm policy add-scc-to-user privileged -z default
    $ kubectl apply -f - <<EOF
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: squid
      namespace: external
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: squid
      template:
        metadata:
          labels:
            app: squid
        spec:
          serviceAccount: default
          volumes:
          - name: proxy-config
            configMap:
              name: proxy-configmap
          containers:
          - name: squid
            image: sameersbn/squid:3.5.27
            imagePullPolicy: IfNotPresent
            securityContext:
              privileged: true
            volumeMounts:
            - name: proxy-config
              mountPath: /etc/squid
              readOnly: true
    EOF
  5. 在external命名空間中建立sleep應用來測試到代理的流量(不受istio控制)

    $ kubectl apply -n external -f samples/sleep/sleep.yaml
  6. 獲取代理pod的地址並定義在PROXY_IP環境變量中

    $ export PROXY_IP="$(kubectl get pod -n external -l app=squid -o jsonpath={.items..podIP})"
  7. 定義PROXY_PORT環境變量來保存代理的端口,即Squid使用的端口3128

    $ export PROXY_PORT=3128
  8. 從external命名空間中的sleep Pod中經過代理向外部服務發送請求:

    # kubectl exec "$(kubectl get pod -n external -l app=sleep -o jsonpath={.items..metadata.name})" -n external -- sh -c "HTTPS_PROXY=$PROXY_IP:$PROXY_PORT curl https://map.baidu.com" | grep -o "<title>.*</title>"
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0<title>百度地圖</title>
    100  154k    0  154k    0     0   452k      0 --:--:-- --:--:-- --:--:--  452k
  9. 檢查代理的訪問日誌:

    # kubectl exec "$(kubectl get pod -n external -l app=squid -o jsonpath={.items..metadata.name})" -n external -- tail /var/log/squid/access.log
    1598596320.477    342 10.80.2.81 TCP_TUNNEL/200 165939 CONNECT map.baidu.com:443 - HIER_DIRECT/180.101.49.69 -

如今,完成了以下兩個於istio無關的任務:

  • 部署了HTTPS 代理
  • 經過代理訪問map.baidu.com

下面將配置啓用istio的pod使用HTTPS代理。

配置流量到外部HTTPS代理

  1. 爲HTTPS代理定義一個TCP(非HTTP) Service Entry。雖然應用會使用HTTP CONNECT方法來與HTTPS代理創建鏈接,但必須爲代理配置TCP流量,而非HTTP。一旦創建鏈接,代理只是充當一個TCP隧道。

    $ kubectl apply -f - <<EOF
    apiVersion: networking.istio.io/v1beta1
    kind: ServiceEntry
    metadata:
      name: proxy
    spec:
      hosts:
      - my-company-proxy.com # ignored
      addresses:
      - $PROXY_IP/32 #hosts字段的後端IP
      ports:
      - number: $PROXY_PORT
        name: tcp
        protocol: TCP
      location: MESH_EXTERNAL
    EOF
  2. 從default命名空間中的sleep Pod發送請求,因爲該pod帶有sidecar,istio會對流量進行控制

    # kubectl exec "$SOURCE_POD" -c sleep -- sh -c "HTTPS_PROXY=$PROXY_IP:$PROXY_PORT curl https://map.baidu.com" | grep -o "<title>.*</title>"
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0<title>百度地圖</title>
    100  154k    0  154k    0     0   439k      0 --:--:-- --:--:-- --:--:--  439k
  3. 檢查istio sidecar 代理的日誌,能夠看到對外訪問了my-company-proxy.com

    # kubectl exec "$SOURCE_POD" -c sleep -- sh -c "HTTPS_PROXY=$PROXY_IP:$PROXY_PORT curl https://map.baidu.com" | grep -o "<title>.*</title>"
    [2020-08-28T12:38:10.064Z] "- - -" 0 - "-" "-" 898 166076 354 - "-" "-" "-" "-" "10.80.2.87:3128" outbound|3128||my-company-proxy.com 10.80.2.77:36576 10.80.2.87:3128 10.80.2.77:36574 - -
  4. 檢查代理的訪問日誌,能夠看到轉發了HTTP CONNECT請求

    # kubectl exec "$(kubectl get pod -n external -l app=squid -o jsonpath={.items..metadata.name})" -n external -- tail /var/log/squid/access.log
    1598618290.412    346 10.80.2.77 TCP_TUNNEL/200 166076 CONNECT map.baidu.com:443 - HIER_DIRECT/180.101.49.69 -

過程理解

在本例中完成了以下步驟:

  1. 部署一個HTTPS代理來模擬外部代理
  2. 建立TCP service entry來使istio控制的流量轉發到外部代理

注意不能爲須要通過外部代理的外部服務(如map.baidu.com)建立service entry。這是由於從istio的角度看,這些請求僅會發送到外部代理,Istio並不知道外部代理會進一步轉發請求的事實。

卸載

$ kubectl delete -f samples/sleep/sleep.yaml
$ kubectl delete -f samples/sleep/sleep.yaml -n external
$ kubectl delete -n external deployment squid
$ kubectl delete -n external configmap proxy-configmap
$ rm ./proxy.conf
$ kubectl delete namespace external
$ kubectl delete serviceentry proxy
相關文章
相關標籤/搜索