• Русский
  • Ошибки 404 возникают при конфигурировании нескольких gateway с одним и тем же TLS-сертификатом

    Описание проблемы

    Симптом

    При доступе через Istio Ingress Gateway по протоколу HTTP/2 возникает ошибка 404.

    Эта проблема является известной в сообществе Istio. Дополнительные сведения см. в 404 errors occur when multiple gateways configured with same TLS certificate.

    Анализ

    Если настроить более одного gateway с использованием одного и того же TLS-сертификата, браузеры, использующие повторное использование HTTP/2-соединений (то есть большинство браузеров), будут выдавать ошибки 404 при обращении ко второму host после того, как соединение с другим host уже было установлено.

    Пример: если домены a.example.com и b.example.com используют один и тот же TLS-сертификат и доступны через один и тот же Istio Ingress Gateway, но настроены в двух разных ресурсах Gateway, клиент браузера с поддержкой HTTP/2 столкнется с ошибкой 404 при обращении к b.example.com после обращения к a.example.com. Это связано с повторным использованием HTTP/2-соединения браузером.

    Метод диагностики

    Вы можете использовать следующий скрипт, чтобы быстро проверить, есть ли в вашей среде конфигурации Gateway, соответствующие описанной проблеме. Скрипт необходимо запускать на master-узле бизнес-кластера, где расположен Istio Ingress Gateway.

    NOTE
    • Скрипт зависит от инструмента jq. Если на узле вашего кластера нет инструмента jq, установите jq в кластере перед запуском скрипта. Ссылка для загрузки инструмента: jq download.
    • Версия инструмента jq должна быть 1.7 или выше.
    #!/bin/bash
    
    nslist=$(kubectl get ns  -o jsonpath='{.items[*].metadata.name}')
    declare -A cred_map
    
    echo "begin to check gw"
    for ns in $nslist; do
      # Get gw resources
      #echo "begin to list gw in $ns"
      gateways=$(kubectl get gw -n $ns -o jsonpath='{.items[*].metadata.name}')
      # Get the YAML file of the Gateway resource
      for gateway in $gateways; do
        gateway_yaml=$(kubectl get gw -n $ns $gateway  -o yaml)
        gateway_json=$(kubectl get gw -n $ns $gateway  -o json)
    
        tls_lines=$(echo "$gateway_yaml" | grep  'credentialName:')
        secname=$(echo "$gateway_yaml" | grep  'credentialName:'|awk '{print $2}')
    
        if [[ -n "$tls_lines" ]]; then
          found=false
          for key in "${!cred_map[@]}"; do
            if [[ "$key" == "$secname" ]]; then
              found=true
              break
            fi
          done
    
          if [[ $found == true ]]; then
            echo -e "\033[31m cred already exist in other gw resource ,please must merge hosts in the  gw resource  ${cred_map[$secname]} ,and delete this gw!  \033[0m"
            hosts=$(echo "$gateway_json" | jq -r '.spec.servers[] | .hosts[]')
            # Output Gateway name and hosts information
            echo -e  "\033[31m invalid gw name namespace: $gateway ,  $ns \033[0m"
            echo "Hosts: $hosts"
          else
            echo "first get secret name $secname the gw is $gateway $ns"
            cred_map["$secname"]="$gateway~$ns"
          fi
    
    
          #for key in "${!cred_map[@]}"; do
            #echo "Key: $key, Value: ${cred_map[$key]}"
          #done
    
          echo ""
        fi
      done
    done

    Пример вывода при выполнении скрипта:

    [root@idp-lihuang-w9x9w-9n9jv-cluster0-dt2n4 gwtls]# sh check.sh
    begin to check gw
    first get secret name jiaxiurc-com the gw is drawdb-gateway drawdb
    first get secret name gyssg-com the gw is ec jxb-ec
    first get secret name nexus the gw is nexus-gateway nexus
     cred already exist in other gw resource, please must merge hosts in the gw resource drawdb-gateway~drawdb, and delete this gw!
     invalid gw name namespace: authory-gateway, nm-edu-authory
    Hosts: rzzx-test.jiaxiurc.com
    rzzx-test.jiaxiurc.com

    Если в выводе вы видите сообщение, подобное следующему: “cred already exist in other gw resource, please must merge hosts in the gw resource drawdb-gateway~drawdb, and delete this gw!”, это означает, что вы столкнулись с проблемой, описанной в этом документе.

    Обзор решений

    Для устранения этой проблемы мы предлагаем два решения. Ниже приведено их сравнение, на основании которого вы можете выбрать подходящий вариант для своей среды.

    Сравнение решений

    РешениеПреимуществаНедостатки
    (Рекомендуется) Решение 1: Объединение ресурсов Gateway- Исправление, рекомендованное сообществом, с обратной совместимостью.
    - Сохраняет производительность HTTP/2 для клиента.
    - Если в кластере большое количество связанных ресурсов Gateway, операция объединения может быть трудоемкой.
    Решение 2: Код ответа 421- Нет необходимости изменять существующие ресурсы Gateway и VirtualService.
    - Сохраняет производительность HTTP/2 для клиента.
    - Сильно зависит от поддержки кода ответа 421 клиентом. Большинство популярных браузеров поддерживают код ответа 421, например Chrome, Firefox и Safari (версия Safari должна быть 15.1 или выше, то есть macOS Monterey).
    - Перед обновлением Istio необходимо проверить совместимость EnvoyFilter.

    Решение 1: Объединение ресурсов Gateway

    Описание решения

    Объедините несколько ресурсов Gateway, использующих один и тот же TLS-сертификат, в один.

    Шаги по реализации

    1. Объедините несколько ресурсов Gateway в одну конфигурацию Gateway, используя один и тот же список spec.servers.hosts или конфигурацию wildcard-домена.
    2. Измените связанные ресурсы VirtualService, чтобы они указывали на объединенный Gateway.

    Например, в исходной конфигурации два Gateway используют один и тот же TLS-сертификат testhl:

    # Gateway Error Example 1: Two Gateways use the same TLS certificate
    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: default2
      namespace: istio-system
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - "asm2.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https
          number: 443
          protocol: HTTPS
    ---
    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: default
      namespace: istio-system
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - "asm1.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https
          number: 443
          protocol: HTTPS
    ---
    # Gateway Error Example 2: The same Gateway uses the same TLS certificate in different Hosts sections
    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: error-3
      namespace: istio-system
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - "asm1.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https-2
          number: 443
          protocol: HTTPS
      - hosts:
        - "asm2.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https
          number: 443
          protocol: HTTPS
    ---
    # VirtualService Example
    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: default2
      namespace: bus-system
    spec:
      gateways:
      - istio-system/default2
      hosts:
      - asm2.test.com
      http:
      - route:
        - destination:
            host: asm-0.testhl.svc.cluster.local
            port:
              number: 80
      ...
    ---
    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: default
      namespace: bus-system
    spec:
      gateways:
      - istio-system/default
      hosts:
      - asm1.test.com
      ...

    Корректная конфигурация после объединения:

    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: default2
      namespace: istio-system
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - "asm2.test.com"
        - "asm1.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https
          number: 443
          protocol: HTTPS
    ---
    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: default1
      namespace: istio-system
    spec:
      gateways:
      - istio-system/default2
      hosts:
      - asm2.test.com
      ...
    ---
    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: default2
      namespace: istio-system
    spec:
      gateways:
      - istio-system/default2
      hosts:
      - asm1.test.com
      ...

    Также это можно записать в формате wildcard:

    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: default2
      namespace: istio-system
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - "*.test.com"
        tls:
          mode: SIMPLE
          credentialName: "testhl"
        port:
          name: https
          number: 443
          protocol: HTTPS

    Краткое описание шагов

    • Объедините spec.servers.hosts ресурсов Gateway, сведя все ресурсы Gateway, использующие один и тот же сертификат, в одну конфигурацию server.
    • Измените ресурсы VirtualService так, чтобы они указывали на объединенный Gateway.
    • Убедитесь, что destination в VirtualService использует формат Kubernetes FQDN.

    Важное примечание: после выполнения указанных шагов повторно запустите скрипт проверки, чтобы убедиться, что проблема устранена.

    Решение 2: Код ответа 421

    Описание решения

    Код состояния ответа 421 при возникновении проблемы позволяет клиенту повторно установить соединение, после чего запрос будет направлен к правильному target Host.

    Шаги по реализации

    Примените следующую конфигурацию EnvoyFilter:

    apiVersion: networking.istio.io/v1alpha3
    kind: EnvoyFilter
    metadata:
      name: misdirected-request
      namespace: istio-system
    spec:
      configPatches:
        - applyTo: HTTP_FILTER
          match:
            context: GATEWAY
            listener:
              filterChain:
                filter:
                  name: envoy.filters.network.http_connection_manager
                  subFilter:
                    name: envoy.filters.http.router
          patch:
            operation: INSERT_BEFORE
            value:
              name: envoy.lua
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                inlineCode: |
                  local function get_host_from_authority(authority)
                    local colon_pos = authority:find(":", 1, true)
                    return colon_pos and authority:sub(1, colon_pos - 1) or authority
                  end
    
                  function envoy_on_request(request_handle)
                    local streamInfo = request_handle:streamInfo()
                    local requestedServerName = streamInfo:requestedServerName()
    
                    if requestedServerName ~= "" then
                      local host = get_host_from_authority(request_handle:headers():get(":authority"))
                      local isWildcard = string.sub(requestedServerName, 1, 2) == "*."
    
                      if isWildcard and not string.find(host, string.sub(requestedServerName, 3)) then
                        request_handle:respond({[":status"] = "421"}, "Misdirected Request")
                      elseif not isWildcard and requestedServerName ~= host then
                        request_handle:respond({[":status"] = "421"}, "Misdirected Request")
                      end
                    end
                  end