启动三种 TLS 模式,确认由谁出示证书
目标
把 Gateway 的 TLS 模式会变成网关 Envoy 监听器的什么,转换成规则;把 SIMPLE、MUTUAL、PASSTHROUGH 和 httpsRedirect 作为等价的监听器启动,并通过客户端收到的证书,证明 TLS 在哪里终止。
为什么重要
网关上的证书故障,大多源于搞错了“TLS 在哪里终止”。修改了网关证书,客户端看到的却是后端证书;或者因为没有 SNI 而选不出链的连接,被误认为是证书问题。了解各个模式会在 Envoy 中生成什么之后,凭一份监听器配置和一行统计数据就能区分。
步骤
- 在
/root/ist2-gw/gw.yaml中编写 Gateway——名称shop-gw,命名空间default,selector为istio: ingressgateway,两个 server:① 端口443、名称https、协议HTTPS,hosts 为shop.example.com,tls.mode: SIMPLE,tls.credentialName: shop-cert;② 端口80、名称http、协议HTTP,hosts 为shop.example.com,tls.httpsRedirect: true。把istioctl validate -f gw.yaml的输出和退出码保存到/root/ist2-gw/01-validate.txt(最后一行为rc=)。然后把去掉credentialName这一行的副本创建为/root/ist2-gw/gw-nocred.yaml,用同样的命令检查,并把结果保存到/root/ist2-gw/01-nocred.txt。 - 在
/root/ist2-gw/02-modes.txt中写五行——对于SIMPLE、MUTUAL、OPTIONAL_MUTUAL、PASSTHROUGH、ISTIO_MUTUAL,每种写一行,格式为<모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no>(第一个占位符为模式;用空格分隔)。terminates是解密 TLS 的位置,client_cert是网关如何处理客户端证书(mesh表示要求 istiod 签发的网格证书),filter是链中最后一个网络过滤器,needs_vs是流量要流通是否必须有 VirtualService。服务器的协议按 HTTPS(PASSTHROUGH 按 TLS)处理。 - 在
/root/ist2-gw/certs中用 openssl 创建证书——CA(ca.crt、ca.key,主体/O=lab/CN=lab-ca)以及由该 CA 签名的网关证书(gateway.crt、gateway.key,主体/O=gateway/CN=shop.example.com,SAN 为DNS:shop.example.com)。然后在/root/ist2-gw/gw-simple.yaml中编写 Envoy 配置——管理端口9988,监听器127.0.0.1:10088的链上放置DownstreamTlsContext(网关证书)加 HCM,路由配置名称使用 Istio 给这个 HTTPS server 命名的https.443.https.shop-gw.default,domains 为shop.example.com,集群outbound|8117||shop.default.svc.cluster.local→127.0.0.1:8117(明文上游upstream.py 8117 ok)。启动之后,以shop.example.com发出请求,并在/root/ist2-gw/03-simple.txt中写入四行——code=(HTTP 状态码)、body=(响应正文)、seen_o=(客户端收到的证书主体中,仅 O 的值)、seen_sha256=(该证书的 SHA-256 指纹,即openssl x509 -fingerprint -sha256输出中=之后的部分)。 - 用同一个 CA 创建客户端证书
/root/ist2-gw/certs/client.crt、client.key(主体/O=client/CN=client,SAN 为DNS:client)。/root/ist2-gw/gw-mutual.yaml与gw-simple.yaml相同,只是在DownstreamTlsContext中加入了require_client_certificate: true和validation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)。启动之后,在/root/ist2-gw/04-mutual.txt中写入四行——without_cert_code=和without_cert_rc=(不带客户端证书发出请求的 HTTP 状态码和 curl 退出码)、with_cert_code=(用--cert和--key发出请求的 HTTP 状态码)、fail_verify_no_cert=(统计listener.127.0.0.1_10088.ssl.fail_verify_no_cert的值)。 - 用同一个 CA 创建后端证书
/root/ist2-gw/certs/backend.crt、backend.key(主体/O=backend/CN=shop.example.com,SAN 为DNS:shop.example.com),并用openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quiet启动 TLS 上游。在/root/ist2-gw/gw-pass.yaml中编写 PASSTHROUGH 网关——管理端口9988,监听器127.0.0.1:10088上放置tls_inspector监听器过滤器,链为filter_chain_match.server_names: ["shop.example.com"]加一个tcp_proxy(没有 transport_socket),集群outbound|8111||shop.default.svc.cluster.local→127.0.0.1:8111。启动之后,以shop.example.com发出请求,并在/root/ist2-gw/05-passthrough.txt中写入三行——code=、seen_o=、seen_sha256=(与第 3 步的方式相同)。 - 对用
gw-pass.yaml运行的网关连接两次——① 把 SNI 设为other.example.com(--resolve other.example.com:10088:127.0.0.1),② 不带 SNI,直接用 IP(https://127.0.0.1:10088/)。两次都要指定--cacert /root/ist2-gw/certs/ca.crt。在/root/ist2-gw/06-sni.txt中写五行——sni=other.example.com、sni_rc=(①的 curl 退出码)、no_sni_rc=(②的退出码)、stat=(统计没能选出链的连接的监听器统计的完整名称)、count=(连接两次之后该统计的值)。 - 在
/root/ist2-gw/gw-redirect.yaml中编写与第 1 步中 80 端口 server 对应的明文监听器——管理端口9988,监听器127.0.0.1:10098(没有 transport_socket),HCM 的路由配置名称使用 Istio 给明文 80 server 命名的http.80,virtual host 的 domains 为shop.example.com,在这个 virtual host 中设置require_tls: ALL,路由为outbound|8117||shop.default.svc.cluster.local→127.0.0.1:8117。启动之后,把curl -H 'Host: shop.example.com' localhost:10098/cart的结果分两行写入/root/ist2-gw/07-redirect.txt——code=(HTTP 状态码)、location=(Location 头的值原样)。 - 在
/root/ist2-gw/08-report.md中写五行——simple_seen_o=(第 3 步中看到的 O)、passthrough_seen_o=(第 5 步)、mutual_without_cert_code=(第 4 步)、sni_mismatch_rc=(第 6 步①的退出码)、redirect_code=(第 7 步)。并在下面写至少四行以-开头的说明。
参考
- 这个 Pod 中既没有真正的 istiod,也没有真正的 sidecar。所以无法用
istioctl proxy-config查看实际生成的内容,需要了解转换规则,并亲手编写等价的 Envoy 配置来确认行为。同样的规则会原样出现在生产集群的proxy-config输出中。 - 所有证书都由
/root/ist2-gw/certs中的同一个 CA 签名。CA 只在第 3 步创建一次——如果重新创建,之前签名的证书就无法再用新 CA 验证。要带上 SAN,签名时需要-copy_extensions copyall。 - 第 3、4、5 步的监听器使用同一个端口 10088。一次只启动一个,每一步的配置文件各自独立。
- TLS 上游(
openssl s_server)也要用setsid --fork nohup … </dev/null与 shell 脱离后启动,重新启动时用pkill -x openssl清理。 - 启动 Envoy 时,请用
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(占位符依次为配置文件与日志文件)使它与 shell 完全脱离。重新启动之前,用pkill -x envoy清理(pkill -f 'envoy -c'会把包含该字符串的 shell 自己也杀掉)。 - 镜像中有模拟上游的服务器:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow(占位符为端口)。响应正文为<모드>:<포트> <경로>(占位符依次为模式、端口与路径)。 - 修改配置之后,启动之前先用
envoy --mode validate -c <파일>(占位符为配置文件)筛查一遍。集群名称中含有|,所以在 YAML 中必须用引号括起来。
编写含两个 server 的 Gateway,并观察 istioctl 对 SIMPLE 的要求
在 /root/ist2-gw/gw.yaml 中编写 Gateway——名称 shop-gw,命名空间 default,selector 为 istio: ingressgateway,两个 server:① 端口 443、名称 https、协议 HTTPS,hosts 为 shop.example.com,tls.mode: SIMPLE,tls.credentialName: shop-cert;② 端口 80、名称 http、协议 HTTP,hosts 为 shop.example.com,tls.httpsRedirect: true。把 istioctl validate -f gw.yaml 的输出和退出码保存到 /root/ist2-gw/01-validate.txt(最后一行为 rc=)。然后把去掉 credentialName 这一行的副本创建为 /root/ist2-gw/gw-nocred.yaml,用同样的命令检查,并把结果保存到 /root/ist2-gw/01-nocred.txt。
Gateway 是只在网关 Pod 的 Envoy 上打开端口并配置证书的资源。只有配上 VirtualService 才会产生路由。SIMPLE 的意思是“由网关终止 TLS”,所以必须有服务器证书,而 Istio 是从 credentialName 所指向的 Secret 中获取它。因此去掉那一行之后,即使没有集群,istioctl 也会拒绝——请原样保存拒绝的原因。退出码是命令之后紧接着的 $?。可以用 sed '/credentialName/d' 只去掉一行。
把五种 TLS 模式转写成 Envoy 一侧的形态
在 /root/ist2-gw/02-modes.txt 中写五行——对于 SIMPLE、MUTUAL、OPTIONAL_MUTUAL、PASSTHROUGH、ISTIO_MUTUAL,每种写一行,格式为 <모드> terminates=<gateway|upstream> client_cert=<none|required|optional|mesh> filter=<hcm|tcp_proxy> needs_vs=<yes|no>(第一个占位符为模式;用空格分隔)。terminates 是解密 TLS 的位置,client_cert 是网关如何处理客户端证书(mesh 表示要求 istiod 签发的网格证书),filter 是链中最后一个网络过滤器,needs_vs 是流量要流通是否必须有 VirtualService。服务器的协议按 HTTPS(PASSTHROUGH 按 TLS)处理。
分岔点只有一个——网关 Envoy 的链上是否会出现 DownstreamTlsContext。如果出现,网关就会解密,既然解密了,就能读取 HTTP 并通过 HCM 进行路由。如果没有出现,网关只能看到密文,所以只能通过 SNI 选择链,把字节转交出去。客户端证书请理解为 Envoy 的 require_client_certificate 和 validation_context 这两个值的组合。示例行中的 AUTO_PASSTHROUGH 是一个例外:SNI 本身就写着目的地,所以不需要 VirtualService。
SIMPLE——网关终止 TLS,并出示自己的证书
在 /root/ist2-gw/certs 中用 openssl 创建证书——CA(ca.crt、ca.key,主体 /O=lab/CN=lab-ca)以及由该 CA 签名的网关证书(gateway.crt、gateway.key,主体 /O=gateway/CN=shop.example.com,SAN 为 DNS:shop.example.com)。然后在 /root/ist2-gw/gw-simple.yaml 中编写 Envoy 配置——管理端口 9988,监听器 127.0.0.1:10088 的链上放置 DownstreamTlsContext(网关证书)加 HCM,路由配置名称使用 Istio 给这个 HTTPS server 命名的 https.443.https.shop-gw.default,domains 为 shop.example.com,集群 outbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117(明文上游 upstream.py 8117 ok)。启动之后,以 shop.example.com 发出请求,并在 /root/ist2-gw/03-simple.txt 中写入四行——code=(HTTP 状态码)、body=(响应正文)、seen_o=(客户端收到的证书主体中,仅 O 的值)、seen_sha256=(该证书的 SHA-256 指纹,即 openssl x509 -fingerprint -sha256 输出中 = 之后的部分)。
一个 SIMPLE server 在 Envoy 中就是一条链加一个 transport_socket。Istio 不是用文件而是用 SDS(kubernetes://shop-cert)放入证书,但即使用文件放入,Envoy 看到的也是一样的。要按名称连接,请使用 curl --resolve 호스트:포트:127.0.0.1 --cacert(占位符依次为主机与端口);而客户端实际收到的证书,请把 openssl s_client -connect … -servername … 的输出交给 openssl x509 来查看。要把 SAN 带进证书,签名时需要 -copy_extensions copyall。路由名称的规则是 https.<포트>.<포트이름>.<Gateway이름>.<네임스페이스>(占位符依次为端口、端口名称、Gateway 名称与命名空间)。
MUTUAL——在同一条链上加入对客户端证书的要求
用同一个 CA 创建客户端证书 /root/ist2-gw/certs/client.crt、client.key(主体 /O=client/CN=client,SAN 为 DNS:client)。/root/ist2-gw/gw-mutual.yaml 与 gw-simple.yaml 相同,只是在 DownstreamTlsContext 中加入了 require_client_certificate: true 和 validation_context.trusted_ca(/root/ist2-gw/certs/ca.crt)。启动之后,在 /root/ist2-gw/04-mutual.txt 中写入四行——without_cert_code= 和 without_cert_rc=(不带客户端证书发出请求的 HTTP 状态码和 curl 退出码)、with_cert_code=(用 --cert 和 --key 发出请求的 HTTP 状态码)、fail_verify_no_cert=(统计 listener.127.0.0.1_10088.ssl.fail_verify_no_cert 的值)。
MUTUAL 就是在 SIMPLE 之上再叠加两个值——“必须出示证书”(require_client_certificate)和“用什么来验证”(validation_context)。在 Istio 中,用于验证的 CA 来自 credentialName 所指 Secret 的 ca.crt(或 <이름>-cacert(占位符为名称))。如果不带证书连接,连 HTTP 都到达不了,所以状态码会是表示没有响应的值,而退出码会随 TLS 版本而不同——请原样写下。拒绝会留在 Envoy 一侧的统计中(对 /stats 执行 grep)。
PASSTHROUGH——网关只读取 SNI,看到的是后端证书
用同一个 CA 创建后端证书 /root/ist2-gw/certs/backend.crt、backend.key(主体 /O=backend/CN=shop.example.com,SAN 为 DNS:shop.example.com),并用 openssl s_server -accept 8111 -cert /root/ist2-gw/certs/backend.crt -key /root/ist2-gw/certs/backend.key -www -quiet 启动 TLS 上游。在 /root/ist2-gw/gw-pass.yaml 中编写 PASSTHROUGH 网关——管理端口 9988,监听器 127.0.0.1:10088 上放置 tls_inspector 监听器过滤器,链为 filter_chain_match.server_names: ["shop.example.com"] 加一个 tcp_proxy(没有 transport_socket),集群 outbound|8111||shop.default.svc.cluster.local → 127.0.0.1:8111。启动之后,以 shop.example.com 发出请求,并在 /root/ist2-gw/05-passthrough.txt 中写入三行——code=、seen_o=、seen_sha256=(与第 3 步的方式相同)。
在 PASSTHROUGH 中,网关既没有证书,也没有密钥。但它仍然需要选择目的地,所以只通过 tls_inspector 窥视 TLS 第一条消息(ClientHello)中以明文携带的 SNI,再用 server_names 匹配这个名称来选择链。由于没有解密,无法使用 HCM,而是由 tcp_proxy 把字节原样转交出去。所以握手的对象是后端,客户端收到的证书也是后端的——请与第 3 步的结果比较指纹。s_server -www 会以 200 和状态页面来响应 HTTP 请求。
SNI 不匹配或缺失时,就没有可选择的链
对用 gw-pass.yaml 运行的网关连接两次——① 把 SNI 设为 other.example.com(--resolve other.example.com:10088:127.0.0.1),② 不带 SNI,直接用 IP(https://127.0.0.1:10088/)。两次都要指定 --cacert /root/ist2-gw/certs/ca.crt。在 /root/ist2-gw/06-sni.txt 中写五行——sni=other.example.com、sni_rc=(①的 curl 退出码)、no_sni_rc=(②的退出码)、stat=(统计没能选出链的连接的监听器统计的完整名称)、count=(连接两次之后该统计的值)。
PASSTHROUGH 监听器只有一条带 server_names 的链,没有默认链。如果名称不同,或者用 IP 连接导致 curl 根本不发送 SNI,Envoy 没有可选的链,会立即关闭连接——在 curl 看来就是握手失败。监听器统计的名称以 listener.<주소>_<포트>.(占位符依次为地址与端口)开头。请在 /stats 中用 filter_chain 查找。
httpsRedirect 就是 virtual host 的 require_tls 一行
在 /root/ist2-gw/gw-redirect.yaml 中编写与第 1 步中 80 端口 server 对应的明文监听器——管理端口 9988,监听器 127.0.0.1:10098(没有 transport_socket),HCM 的路由配置名称使用 Istio 给明文 80 server 命名的 http.80,virtual host 的 domains 为 shop.example.com,在这个 virtual host 中设置 require_tls: ALL,路由为 outbound|8117||shop.default.svc.cluster.local → 127.0.0.1:8117。启动之后,把 curl -H 'Host: shop.example.com' localhost:10098/cart 的结果分两行写入 /root/ist2-gw/07-redirect.txt——code=(HTTP 状态码)、location=(Location 头的值原样)。
Istio 会在 httpsRedirect: true 的 server 的 virtual host 中放入 require_tls: ALL。这样 Envoy 在查看路由之前,就会把明文请求重定向到 https——甚至不会到达上游。明文 HTTP server 的路由名称不带网关名称,是 http.<포트>(占位符为端口),所以使用同一端口的多个 Gateway 的主机会合并到同一份路由配置中。请用 curl -s -D - -o /dev/null 查看响应头。Location 中的主机来自请求的 Host 头。
整理各个模式下由谁终止 TLS
在 /root/ist2-gw/08-report.md 中写五行——simple_seen_o=(第 3 步中看到的 O)、passthrough_seen_o=(第 5 步)、mutual_without_cert_code=(第 4 步)、sni_mismatch_rc=(第 6 步①的退出码)、redirect_code=(第 7 步)。并在下面写至少四行以 - 开头的说明。
数值请从前面步骤的文件中抄录。说明行中把“在这个模式下,网关 Envoy 的链上会生成什么,因此客户端会看到什么”成对写下来,今后在生产环境中遇到证书问题时,就清楚该从哪里入手了。