当多个网关配置相同的 TLS 证书时发生 404 错误
问题描述
症状
在使用 HTTP/2 协议通过 Istio Ingress Gateway 访问时,发生 404 错误。
这是 Istio 社区已知的问题。更多信息请参考 当多个网关配置相同的 TLS 证书时发生 404 错误。
分析
使用相同的 TLS 证书配置多个网关会导致利用 HTTP/2 连接重用的浏览器(即大多数浏览器)在访问第二个主机时产生 404 错误,而该连接之前已连接到另一个主机。
示例: 如果域名 a.example.com
和 b.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 资源合并为一个。
实施步骤
- 将多个 Gateway 资源合并为一个 Gateway 配置,使用相同的
spec.servers.hosts
列表或通配符域名配置。
- 修改相关的 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