当多个网关配置相同的 TLS 证书时发生 404 错误

问题描述

症状

在使用 HTTP/2 协议通过 Istio Ingress Gateway 访问时,发生 404 错误。

这是 Istio 社区已知的问题。更多信息请参考 当多个网关配置相同的 TLS 证书时发生 404 错误

分析

使用相同的 TLS 证书配置多个网关会导致利用 HTTP/2 连接重用的浏览器(即大多数浏览器)在访问第二个主机时产生 404 错误,而该连接之前已连接到另一个主机。

示例: 如果域名 a.example.comb.example.com 使用相同的 TLS 证书并通过相同的 Istio Ingress Gateway 访问,但在两个不同的 Gateway 资源中配置,则 HTTP/2 浏览器客户端在访问 a.example.com 后访问 b.example.com 时将遇到 404 错误。这是由于浏览器的 HTTP/2 连接重用造成的。

故障排除方法

您可以使用以下脚本快速检查您的环境中是否具有与问题描述匹配的 Gateway 配置。该脚本需要在 Istio Ingress Gateway 所在的业务集群的主节点上执行。

注意:

  • 该脚本依赖于 jq 工具。如果您的集群节点没有 jq 工具,请在执行脚本之前先在集群中安装 jq。工具下载链接:jq download
  • jq 工具版本必须为 1.7 或更高。
#!/bin/bash

nslist=$(kubectl get ns  -o jsonpath='{.items[*].metadata.name}')
declare -A cred_map

echo "开始检查 gw"
for ns in $nslist; do
  # 获取 gw 资源
  gateways=$(kubectl get gw -n $ns -o jsonpath='{.items[*].metadata.name}')
  # 获取 Gateway 资源的 YAML 文件
  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 该凭据已存在于其他 gw 资源中,请务必在 gw 资源 ${cred_map[$secname]} 中合并主机,并删除此 gw!  \033[0m"
        hosts=$(echo "$gateway_json" | jq -r '.spec.servers[] | .hosts[]')
        # 输出 Gateway 名称和主机信息
        echo -e  "\033[31m 无效的 gw 名称命名空间: $gateway ,  $ns \033[0m"
        echo "主机: $hosts"
      else
        echo "首次获取秘密名称 $secname 该 gw 是 $gateway $ns"
        cred_map["$secname"]="$gateway~$ns"
      fi

      echo ""
    fi
  done
done

脚本执行输出示例:

[root@idp-lihuang-w9x9w-9n9jv-cluster0-dt2n4 gwtls]# sh check.sh
开始检查 gw
首次获取秘密名称 jiaxiurc-com 该 gw 是 drawdb-gateway drawdb
首次获取秘密名称 gyssg-com 该 gw 是 ec jxb-ec
首次获取秘密名称 nexus 该 gw 是 nexus-gateway nexus
 该凭据已存在于其他 gw 资源中,请务必在 gw 资源 drawdb-gateway~drawdb 中合并主机,并删除此 gw!
 无效的 gw 名称命名空间: authory-gateway, nm-edu-authory
主机: rzzx-test.jiaxiurc.com
rzzx-test.jiaxiurc.com

如果您在输出中看到类似信息:“该凭据已存在于其他 gw 资源中,请务必在 gw 资源 drawdb-gateway~drawdb 中合并主机,并删除此 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 资源

解决方案描述

将使用相同 TLS 证书的多个 Gateway 资源合并为一个。

实施步骤

  1. 将多个 Gateway 资源合并为一个 Gateway 配置,使用相同的 spec.servers.hosts 列表或通配符域名配置。
  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 在不同 Hosts 部分使用相同的 TLS 证书
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

步骤总结

  • 将 Gateway 资源的 spec.servers.hosts 合并,将使用相同证书的所有 Gateway 资源合并为一个服务器配置。
  • 修改 VirtualService 资源以指向合并后的 Gateway。
  • 确保 VirtualService 中的目标使用 Kubernetes FQDN 格式。

重要注意事项: 执行上述步骤后,请重新运行检查脚本以确认问题已得到解决。

解决方案 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