• Русский
  • Ошибки 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
      # Получаем ресурсы gw
      #echo "begin to list gw in $ns"
      gateways=$(kubectl get gw -n $ns -o jsonpath='{.items[*].metadata.name}')
      # Получаем YAML файл ресурса Gateway
      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[]')
            # Выводим имя Gateway и информацию о хостах
            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 1: два Gateway используют один TLS-сертификат
    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 2: один Gateway использует один TLS-сертификат в разных секциях Hosts
    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
    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