Возникают ошибки 404 при конфигурации нескольких шлюзов с одним TLS-сертификатом

Содержание

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

Симптом

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

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

Анализ

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

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

Метод устранения

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

NOTE
  • Скрипт зависит от утилиты 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
  ...

Также можно использовать запись с подстановочным знаком:

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.
  • Убедиться, что в VirtualService в поле destination используется формат FQDN Kubernetes.

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

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

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

Возврат кода ответа 421 в случае проблемы позволяет клиенту повторно установить соединение, которое будет направлено на правильный целевой хост.

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

Примените следующую конфигурацию 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