TT Lab
开始
学习 学习路径 课程

Istio 进阶 — 为什么会那样流

签发 SPIFFE 证书并搭建 STRICT 与 PERMISSIVE 链

在 TT Lab 中继续学习

目标

编写 PeerAuthentication,用 openssl 生成网格 CA 和 SPIFFE 证书,亲手建立各个模式对应的 Envoy 过滤器链,确认明文和 mTLS 如何分流,再从证书的 SAN 中推导出 AuthorizationPolicy 的 principal。

为什么重要

改为 STRICT 的那天就会中断的调用、想只把一个端口保留为明文却被悄悄忽略的策略、因为 principal 写错而让所有人都被拦截的授权策略——这三种问题,istioctl validate 都能通过。只要知道模式会变成 Envoy 的哪条链、身份来自证书的哪个位置,就能凭一行统计数据和一份证书找出原因。

步骤

  1. 在 /root/ist2-mtls/pa.yaml 中编写 PeerAuthentication——apiVersion: security.istio.io/v1,名称 reviews-strict,命名空间 default,selector.matchLabels.app: reviews,mtls.mode: STRICT。把 istioctl validate -f pa.yaml 的输出和退出码保存到 /root/ist2-mtls/01-validate.txt(最后一行为 rc=)。
  2. 在 /root/ist2-mtls 中用 openssl 创建一份自签名 CA(ca.crt、ca.key),并创建三份由该 CA 签名的证书——reviews.crt/reviews.key(服务器)、orders.crt/orders.key(要放行的客户端)、intruder.crt/intruder.key(同一个 CA 签名、但不放行的客户端)。每份证书的 SAN 只有一个 URI,即 spiffe://cluster.local/ns/default/sa/<이름>(占位符为服务账号名称)。然后把实际从证书中读取到的 SAN,按 reviews.crt=…、orders.crt=…、intruder.crt=… 三行写入 /root/ist2-mtls/02-ids.txt。
  3. 在 /root/ist2-mtls/strict.yaml 中编写 Envoy 配置——管理端口 9986,监听器 virtualInbound 监听 127.0.0.1:10086,并设置监听器过滤器 envoy.filters.listener.tls_inspector。过滤器链只有一条,即 filter_chain_match.transport_protocol: tls,并在 DownstreamTlsContext 中设置 require_client_certificate: true、服务器证书 /root/ist2-mtls/reviews.crt 和 /root/ist2-mtls/reviews.key,以及受信任 CA /root/ist2-mtls/ca.crt(暂时不加入 SAN 匹配器)。这条链把所有路径发往集群 inbound|8109||(127.0.0.1:8109)。以 ok 模式在 8109 上启动上游并启动 Envoy 之后,在 /root/ist2-mtls/03-strict.txt 中写入四行——plain=(明文 http://localhost:10086/strict 的状态码)、mtls=(用 orders 证书请求 https://localhost:10086/strict 的状态码)、mtls_body=(该响应的正文)、no_filter_chain_match=(统计 listener.127.0.0.1_10086.no_filter_chain_match 的值)。
  4. 把 /root/ist2-mtls/strict.yaml 复制为 /root/ist2-mtls/strict-san.yaml,然后只在这份副本中,往 validation_context 里再加入一个 match_typed_subject_alt_names——san_type: URI,matcher.exact 取第 2 步中读到的 orders.crt 的 SAN,原样填写。用这份配置重新启动 Envoy,并在 /root/ist2-mtls/04-san.txt 中写入三行——orders=(用 orders 证书请求 https://localhost:10086/san 的状态码)、intruder=(用 intruder 证书发出同样请求的状态码)、fail_verify_san=(统计 listener.127.0.0.1_10086.ssl.fail_verify_san 的值)。
  5. 以 /root/ist2-mtls/strict.yaml 为基础创建 /root/ist2-mtls/permissive.yaml——保留第 3 步的 TLS 链,再增加一条既没有 filter_chain_match 也没有 transport_socket 的明文链(同样发往集群 inbound|8109||)。用这份配置重新启动 Envoy,并在 /root/ist2-mtls/05-permissive.txt 中写入三行——plain=(明文 http://localhost:10086/permissive 的状态码)、plain_body=(其正文)、mtls=(用 orders 证书请求 https://localhost:10086/permissive 的状态码)。
  6. 在 /root/ist2-mtls/pa-port.yaml 中写两个文档——(1)Service reviews(命名空间 default,selector app: reviews,port: 80 → targetPort: 10096),(2)PeerAuthentication reviews-port(selector 相同,mtls.mode: STRICT,通过 portLevelMtls 对一个端口设置 DISABLE)。该端口号应该写成什么,请看提示。然后在 /root/ist2-mtls/portlevel.yaml 中编写 Envoy 配置——让与第 3 步相同的监听器(10086)通过 additional_addresses 再监听 127.0.0.1:10096,并设置一条 filter_chain_match.destination_port: 10096 的明文链,以及一条第 3 步的 TLS 链。重新启动 Envoy,并在 /root/ist2-mtls/06-portlevel.txt 中写入四行——validate_rc=(istioctl validate -f pa-port.yaml 的退出码)、plain_10096=、plain_10086=(分别向各端口发送明文 /port 的状态码)、mtls_10086=(用 orders 证书请求 https://localhost:10086/port 的状态码)。
  7. 用 openssl 读取 /root/ist2-mtls/orders.crt 的 SAN,并在 /root/ist2-mtls/07-principal.txt 中写入两行——san=(读到的 URI 原样)、principal=(要放进 AuthorizationPolicy 的 principals 的字符串)。然后在 /root/ist2-mtls/authz.yaml 中编写 AuthorizationPolicy——apiVersion: security.istio.io/v1,名称 reviews-from-orders,命名空间 default,selector.matchLabels.app: reviews,action: ALLOW,一条规则的 from[0].source.principals 中只放这一个 principal。istioctl validate -f authz.yaml 必须通过。
  8. 在 /root/ist2-mtls/08-report.md 中写入 default_mode=(没有任何 PeerAuthentication 时的模式)、strict_plain=(第 3 步中明文请求收到的状态码)、permissive_chains=(第 5 步监听器的过滤器链数量)、orders_principal=(第 7 步的 principal)四行,并在下面写至少四行以 - 开头的说明。

参考

编写 STRICT PeerAuthentication,并离线筛查

在 /root/ist2-mtls/pa.yaml 中编写 PeerAuthentication——apiVersion: security.istio.io/v1,名称 reviews-strict,命名空间 default,selector.matchLabels.app: reviews,mtls.mode: STRICT。把 istioctl validate -f pa.yaml 的输出和退出码保存到 /root/ist2-mtls/01-validate.txt(最后一行为 rc=)。

PeerAuthentication 是接收方工作负载的配置。它不是对调用 reviews 的一方设置,而是对 reviews 自身设置“不要接收明文”,所以 selector 必须是接收方的标签。省略 selector,则作用于整个命名空间;放在根命名空间(istio-system)中,则作用于整个网格。istioctl validate 不需要集群,只检查 schema——如果模式值拼写错误,会在这里被发现。

创建网格 CA 和三份 SPIFFE 证书

在 /root/ist2-mtls 中用 openssl 创建一份自签名 CA(ca.crt、ca.key),并创建三份由该 CA 签名的证书——reviews.crt/reviews.key(服务器)、orders.crt/orders.key(要放行的客户端)、intruder.crt/intruder.key(同一个 CA 签名、但不放行的客户端)。每份证书的 SAN 只有一个 URI,即 spiffe://cluster.local/ns/default/sa/<이름>(占位符为服务账号名称)。然后把实际从证书中读取到的 SAN,按 reviews.crt=…、orders.crt=…、intruder.crt=… 三行写入 /root/ist2-mtls/02-ids.txt。

Istio 的身份不是名称(CN),而是 SAN 中的 URI——spiffe://<신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정>(占位符依次为信任域、命名空间与服务账号)。istiod 确认 Pod 的服务账号令牌之后,签发的正是这种形状的证书。即使在 CSR 中用 -addext "subjectAltName=URI:…" 写入了 SAN,如果签名时没有给 openssl x509 -req 加上 -copy_extensions copyall,扩展也会被丢弃。签名之后,务必用 openssl x509 -noout -ext subjectAltName 确认,并用 openssl verify -CAfile ca.crt 查看证书链。

STRICT 会变成只有一条 TLS 链的监听器

在 /root/ist2-mtls/strict.yaml 中编写 Envoy 配置——管理端口 9986,监听器 virtualInbound 监听 127.0.0.1:10086,并设置监听器过滤器 envoy.filters.listener.tls_inspector。过滤器链只有一条,即 filter_chain_match.transport_protocol: tls,并在 DownstreamTlsContext 中设置 require_client_certificate: true、服务器证书 /root/ist2-mtls/reviews.crt 和 /root/ist2-mtls/reviews.key,以及受信任 CA /root/ist2-mtls/ca.crt(暂时不加入 SAN 匹配器)。这条链把所有路径发往集群 inbound|8109||(127.0.0.1:8109)。以 ok 模式在 8109 上启动上游并启动 Envoy 之后,在 /root/ist2-mtls/03-strict.txt 中写入四行——plain=(明文 http://localhost:10086/strict 的状态码)、mtls=(用 orders 证书请求 https://localhost:10086/strict 的状态码)、mtls_body=(该响应的正文)、no_filter_chain_match=(统计 listener.127.0.0.1_10086.no_filter_chain_match 的值)。

istiod 收到 STRICT 之后,会去掉入站监听器中的明文链。剩下的是只接收被 tls_inspector 判定为“这是 TLS”的连接的那条链,该链要求客户端证书,并用网格 CA 验证。明文连接没有可匹配的链,Envoy 直接将其关闭——连 HTTP 响应都没有,所以 curl 的状态码是 000,留下的痕迹就是 no_filter_chain_match 统计。服务器证书的 SAN 只有 URI,curl 的主机名检查无法通过,所以用 -k 只关闭对服务器的验证,同时用 --cert 和 --key 提交客户端证书。(Istio 的客户端不检查主机名,而是检查服务器 SAN 中的 SPIFFE ID。)

即使由同一个 CA 签名,SAN 不同也会被拒绝

把 /root/ist2-mtls/strict.yaml 复制为 /root/ist2-mtls/strict-san.yaml,然后只在这份副本中,往 validation_context 里再加入一个 match_typed_subject_alt_names——san_type: URI,matcher.exact 取第 2 步中读到的 orders.crt 的 SAN,原样填写。用这份配置重新启动 Envoy,并在 /root/ist2-mtls/04-san.txt 中写入三行——orders=(用 orders 证书请求 https://localhost:10086/san 的状态码)、intruder=(用 intruder 证书发出同样请求的状态码)、fail_verify_san=(统计 listener.127.0.0.1_10086.ssl.fail_verify_san 的值)。

第 3 步的链只检查“是不是网格 CA 签名的”。所以同样来自这个 CA 的 intruder 也通过了。加上 SAN 匹配器之后,除签名之外,还会检查“这是谁的证书”。不过,Istio 使用这个匹配器的地方主要是客户端一侧——由发起调用的一方确认“对方真的是 reviews 的身份吗”(secure naming);而在接收方,“谁可以调用”则由第 7 步的 AuthorizationPolicy 负责。这里是把同样的原理放在接收方,来观察 Envoy 如何比对 SAN。拒绝发生在 TLS 握手阶段,所以状态码是 000。

PERMISSIVE 是两条链

以 /root/ist2-mtls/strict.yaml 为基础创建 /root/ist2-mtls/permissive.yaml——保留第 3 步的 TLS 链,再增加一条既没有 filter_chain_match 也没有 transport_socket 的明文链(同样发往集群 inbound|8109||)。用这份配置重新启动 Envoy,并在 /root/ist2-mtls/05-permissive.txt 中写入三行——plain=(明文 http://localhost:10086/permissive 的状态码)、plain_body=(其正文)、mtls=(用 orders 证书请求 https://localhost:10086/permissive 的状态码)。

PERMISSIVE 的意思是“两种都接收”。在 Envoy 中,它表现为多加一条链。tls_inspector 查看第一个字节,如果是 TLS,就送往 transport_protocol: tls 链,否则送往没有匹配条件的那条链。Istio 实际生成的链还会附带 ALPN(istio-peer-exchange、istio)条件,但原理相同。如果给明文链加上 transport_socket,这条链也会等待 TLS,明文就又被拦住了。

portLevelMtls 是针对工作负载端口设置的一条链

在 /root/ist2-mtls/pa-port.yaml 中写两个文档——(1)Service reviews(命名空间 default,selector app: reviews,port: 80 → targetPort: 10096),(2)PeerAuthentication reviews-port(selector 相同,mtls.mode: STRICT,通过 portLevelMtls 对一个端口设置 DISABLE)。该端口号应该写成什么,请看提示。然后在 /root/ist2-mtls/portlevel.yaml 中编写 Envoy 配置——让与第 3 步相同的监听器(10086)通过 additional_addresses 再监听 127.0.0.1:10096,并设置一条 filter_chain_match.destination_port: 10096 的明文链,以及一条第 3 步的 TLS 链。重新启动 Envoy,并在 /root/ist2-mtls/06-portlevel.txt 中写入四行——validate_rc=(istioctl validate -f pa-port.yaml 的退出码)、plain_10096=、plain_10086=(分别向各端口发送明文 /port 的状态码)、mtls_10086=(用 orders 证书请求 https://localhost:10086/port 的状态码)。

iptables 在把到达 Pod 的连接转向 15006 时,会保留原始目标端口。这个端口不是 Service 的 port,而是容器实际监听的 targetPort,而 istiod 会把 portLevelMtls 的编号原样转换成以 destination_port 匹配的链。所以文档中也写作“工作负载端口”。另外,portLevelMtls 只有在带 selector 的策略中才会被接受——去掉之后,istioctl validate 会拒绝它。在这个 Pod 中没有 iptables,所以改为让一个监听器监听两个端口,来构造同样的形态。

从 SAN 中去掉 spiffe:// 就是 principal

用 openssl 读取 /root/ist2-mtls/orders.crt 的 SAN,并在 /root/ist2-mtls/07-principal.txt 中写入两行——san=(读到的 URI 原样)、principal=(要放进 AuthorizationPolicy 的 principals 的字符串)。然后在 /root/ist2-mtls/authz.yaml 中编写 AuthorizationPolicy——apiVersion: security.istio.io/v1,名称 reviews-from-orders,命名空间 default,selector.matchLabels.app: reviews,action: ALLOW,一条规则的 from[0].source.principals 中只放这一个 principal。istioctl validate -f authz.yaml 必须通过。

mTLS 结束之后,接收方 Envoy 会把对方证书的 URI SAN 记作这个连接的身份。AuthorizationPolicy 的 source.principals 就是与这个身份比对的,写法上要去掉 scheme(spiffe://),采用 <신뢰 도메인>/ns/<네임스페이스>/sa/<서비스 계정>(占位符依次为信任域、命名空间与服务账号)的形式。在 shell 中可以用 ${san#spiffe://} 去掉。注意——即使带着 scheme 写入,istioctl validate 也会通过。但这条策略不会匹配任何对象,如果是 ALLOW 策略,就会拦住所有人。而且 principal 只会出现在通过 mTLS 进入的连接上,所以在允许明文的 PERMISSIVE 端口上,没有任何请求会命中这条规则。

把各个模式在 Envoy 中产生的内容整理成一页

在 /root/ist2-mtls/08-report.md 中写入 default_mode=(没有任何 PeerAuthentication 时的模式)、strict_plain=(第 3 步中明文请求收到的状态码)、permissive_chains=(第 5 步监听器的过滤器链数量)、orders_principal=(第 7 步的 principal)四行,并在下面写至少四行以 - 开头的说明。

数值请从前面步骤的文件中抄录。说明中最好把“模式 → Envoy 中产生的内容 → 生产环境中看到的症状”成对写下来。至于默认模式为什么是那个值,可以回想一下往网格中依次加入 sidecar 的过程。