layout: post
title: "一文带你彻底厘清 Isito 中的证书工作机制" subtitle: "" excerpt: "" author: "赵化冰" date: 2020-05-25 description: "Istio 为微服务提供了无侵入,可插拔的安全框架。应用不需要修改代码,就可以利用 Istio 提供的双向 TLS 认证实现服务身份认证,并基于服务身份信息提供细粒度的访问控制。本文将揭秘 Istio 双向 TLS 认证的实现机制。" image: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Obi-Obi_Valley_-_Mapleton_Falls_National_Park.tif/lossy-page1-2560px-Obi-Obi_Valley_-_Mapleton_Falls_National_Park.tif.jpg" published: true tags:
- Istio
在上一篇文章一文带你彻底厘清 Kubernetes 中的证书工作机制中,我们介绍了 Kubernetes 中证书的工作机制。在这篇文章中,我们继续探讨 Istio 是如何使用证书来实现网格中服务的身份认证和安全通信的。
本文是对 Istio 认证工作机制的深度分析,假设读者已经了解 Service Mesh 以及 Istio 的相关基础概念,因此在本文对此类基础概念不再解释。对于 Istio 不熟悉的读者,建议先阅读 Istio 官方网站上的的这篇基础介绍 What is Istio?。
Istio 为微服务提供了无侵入,可插拔的安全框架。应用不需要修改代码,就可以利用 Istio 提供的双向 TLS 认证实现服务身份认证,并基于服务身份信息提供细粒度的访问控制。Istio 安全的高层架构如下图所示:
图1. Istio Security Architecture,图片来源istio.io
图中展示了 Istio 中的服务认证和授权两部分内容。让我们暂时忽略掉授权部分,先关注认证部分。服务认证是通过控制面和数据面一起实现的:
图1是对 Istio 安全架构的一个高度概括的描述,让我们把图1中控制面的交互展开,看一下其中的细节。
图2. Istio 证书分发流程
我们先暂时忽略图中右边蓝色虚线的部分(稍后会在 控制面身份认证 部分讲到),图中左半部分描述了 Istio 控制面向 Envoy 签发证书的流程:
从图2可以看到,Istio 证书签发的过程中涉及到了三个组件: Istiod (Istio CA) ---> Pilot-agent ---> Enovy。为什么其他 xDS 接口都是由 Istiod 直接向 Envoy 提供,但 SDS 却要通过 Pilot-agent 进行一次中转,而不是直接由 Envoy 通过 SDS 接口从 Istiod 获取证书呢?这样做主要有两个原因。
首先,在 Istio 的证书签发流程中,由 Pilot-agent 生成私钥和 CSR,再通过 CSR 向 Istiod 中的 CA 申请证书。在整个过程中,私钥只存在于本地的 Istio-proxy 容器中。如果去掉中间 Pilot-agent 这一步,直接由 Envoy 向 Isitod 申请证书,则需要由 Istiod 生成私钥,并将私钥和证书一起通过网络返回给 Envoy,这将大大增加私钥泄露的风险。
另一方面,通过 Pilot-agent 来提供 SDS 服务,由 Pilot-agent 生成标准的 CSR 证书签名请求,可以很容易地对接不同的 CA 服务器,方便 Istio 和其他证书机构进行集成。
要通过服务证书来实现网格中服务的身份认证,必须首先确保服务从控制面获取自身证书的流程是安全的。Istio 通过 Istiod 和 Pilog-agent 之间的 gRPC 通道传递 CSR 和证书,因此在这两个组件进行通信时,双方需要先验证对方的身份,以避免恶意第三方伪造 CSR 请求或者假冒 Istiod CA 服务器。在目前的版本中(Istio1.6),Pilot-agent 和 Istiod 分布采用了不同的认证方式。
备注:除了 Kubernetes 之外, Istio 也支持虚机部署,在虚机部署的场景下,由于没有 service account,Pilot-agent 和 Pilotd 之间的身份认证方式有所不同。由于 Istio 的主要使用场景还是 Kubernetes,本文只分析 Kubernetes 部署场景。
和其他 xDS 接口一样,SDS 也是 Envoy 支持的一种动态配置服务接口。Envoy 可以通过 SDS(secret discovery service) 接口从 SDS 服务器自动获取证书。和之前的方式相比,SDS 最大的好处就是简化了证书管理。在没有使用 SDS 前,Istio 中的服务证书被创建为 Kubernetes secret,并挂载到代理容器中。如果证书过期了,则需要更新 secret 并重启 Envoy 容器,以启用新的证书。使用SDS后,SDS 服务器(Pilot-agent充当了 SDS 服务器的角色)将向 Envoy 实例主动推送证书。如果证书过期,SDS 服务器只需将新的证书推送到 Envoy 例中,Envoy 会使用新的证书来创建链接,无需重新启动。
图3. Envoy SDS 服务
可以看到,Istio 采用 SDS 后,避免了在证书更新后重启 Envoy,大大减少了证书更新对业务的影响。同时,由于 Pilot-agent 和 Envoy 处于同一容器中,私钥只存在于本地容器,避免了在网络中传递私钥,也降低了私钥泄露的安全风险。
SDS 服务向 Envoy 下发的数据结构为extensions.transport_sockets.tls.v3.Secret
,其结构如下:
{
"name": "...", // Secret 名称
"tls_certificate": "{...}", // 数字证书
"session_ticket_keys": "{...}",
"validation_context": "{...}", // 证书验证信息
"generic_secret": "{...}"
}
其中在 Istio 中用到的主要是 tls_certificate
和 validation_context
。 分别用于传递数字证书和验证对方证书使用到的根证书。下面是这两个字段的结构,结构中标注了我们主要需要关注的内容。
tls_certificate
{
"certificate_chain": "{...}", // 证书内容
"private_key": "{...}", // 证书的私钥
"private_key_provider": "{...}",
"password": "{...}"
}
validation_context
{
"trusted_ca": "{...}", // CA 根证书
"verify_certificate_spki": [],
"verify_certificate_hash": [],
"match_subject_alt_names": [], // 需要验证的 subject alt name
"crl": "{...}",
"allow_expired_certificate": "...",
"trust_chain_verification": "..."
}
在 Istio 的 Enovy 边车配置中,有两处需要通过 SDS 来配置证书:
下面我们来看一下 bookinfo 示例中 Envoy 边车代理上 reviews 微服务相关的证书配置,以对 Istio 中 SDS 的运作机制有一个更清晰的认识。为了简略起见,本文只显示了部分关键的配置。你也可以查看 Github 上的完整配置。
通过 Envoy 的管理端口,可以导出 Envoy 中的当前配置,导出命令如下:
kubectl exec reviews-v1-6d8bc58dd7-ts8kw -c istio-proxy curl http://127.0.0.1:15000/config_dump > config_dump
配置文件中 SDS 服务器的定义如下,Pilot-agent 在 /etc/istio/proxy/SDS
这路径上通过 unix domain socket 提供了一个 SDS 服务器。
{
"name": "sds-grpc",
"type": "STATIC",
"connect_timeout": "10s",
"hidden_envoy_deprecated_hosts": [
{
"pipe": {
"path": "/etc/istio/proxy/SDS"
}
}
],
"http2_protocol_options": {}
}
Details Pod 在 9080 端口对外提供服务,因此需要通过 SDS 配置 9080 端口上的服务端证书和验证客户端证书的 CA 根证书。
{
"name": "virtualInbound", // 15006端口上的虚拟入向监听器
"active_state": {
"version_info": "2020-05-14T03:59:54Z/25",
"listener": {
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "virtualInbound",
"address": {
"socket_address": {
"address": "0.0.0.0",
"port_value": 15006
}
},
"filter_chains": [
{
"filter_chain_match": {
"prefix_ranges": [
{
"address_prefix": "10.44.0.8",
"prefix_len": 32
}
],
"destination_port": 9080 // 用于处理发向reviews服务9080端口的业务请求的filter chain
},
"filters": [...],
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"@type": "type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext",
"common_tls_context": {
"alpn_protocols": [
"h2",
"http/1.1"
],
"tls_certificate_sds_secret_configs": [ // 配置服务器端证书
{
"name": "default", // 服务器证书 Secret 名称
"sds_config": {
"api_config_source": {
"api_type": "GRPC",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "sds-grpc" // 配置用于获取服务器端证书的 SDS 服务器
}
}
]
}
}
}
],
"combined_validation_context": {
"default_validation_context": {},
"validation_context_sds_secret_config": { // 配置验证客户端证书的 CA 根证书
"name": "ROOTCA", // CA 根证书 Secret 名称
"sds_config": {
"api_config_source": {
"api_type": "GRPC",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "sds-grpc" // 配置用于获取 CA 根证书的 SDS 服务器
}
}
]
}
}
}
}
},
"require_client_certificate": true
}
}
}
],
}
}
客户端 Pod 上的 Enovy 通过 reviews outbound cluster 访问上游 reviews 服务,因此需要在该 cluster 上配置客户端证书以及验证服务器端证书的 CA 根证书。在这里我们需要注意的是,Envoy 在验证服务器端证书时会同时验证证书中的 subject alternative name 字段。该字段中设置的是 reviews 服务 Pod 关联的 Service Account 名称。 由于 Service Account 是 Istio 中认可的一种用户账户,因此通过为 Service Account 设置不同的资源访问权限,可以进一步实现细粒度的权限控制,例如按照 URL 进行授权。
{
"version_info": "2020-05-14T03:15:47Z/18",
"cluster": {
"@type": "type.googleapis.com/envoy.api.v2.Cluster",
"name": "outbound|9080||reviews.default.svc.cluster.local",
"type": "EDS",
"eds_cluster_config": {
"eds_config": {
"ads": {}
},
"service_name": "outbound|9080||reviews.default.svc.cluster.local"
},
"service_name": "outbound|9080||reviews.default.svc.cluster.local"
},
"circuit_breakers": {...},
"filters": [...],
"transport_socket_matches": [
{
"name": "tlsMode-istio",
"match": {
"tlsMode": "istio"
},
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"@type": "type.googleapis.com/envoy.api.v2.auth.UpstreamTlsContext",
"common_tls_context": {
"alpn_protocols": [
"istio-peer-exchange",
"istio"
],
"tls_certificate_sds_secret_configs": [
{
"name": "default", // 配置用于访问 reviews 服务的客户端证书
"sds_config": {
"api_config_source": {
"api_type": "GRPC",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "sds-grpc" // 配置用于获取客户端证书的 SDS 服务器
}
}
]
}
}
}
],
"combined_validation_context": {
"default_validation_context": {
"verify_subject_alt_name": [
"spiffe://cluster.local/ns/default/sa/bookinfo-reviews" // 验证服务器证书时需要验证 SAN 中的 service account 名称
]
},
"validation_context_sds_secret_config": {
"name": "ROOTCA", // 配置验证 reviews 服务器证书的 CA 根证书
"sds_config": {
"api_config_source": {
"api_type": "GRPC",
"grpc_services": [
{
"envoy_grpc": {
"cluster_name": "sds-grpc" // 配置用于获取 CA 根证书的 SDS 服务器
}
}
]
}
}
}
}
},
"sni": "outbound_.9080_._.reviews.default.svc.cluster.local"
}
}
},
]
},
"last_updated": "2020-05-14T03:16:33.061Z"
}
上面配置中 SAN 中的 service account 名称来自于 reviews pod 中的 service account 配置。
apiVersion: apps/v1
kind: Deployment
metadata:
name: reviews-v1
labels:
app: reviews
version: v1
spec:
replicas: 1
selector:
matchLabels:
app: reviews
version: v1
template:
metadata:
labels:
app: reviews
version: v1
spec:
serviceAccountName: bookinfo-reviews // 设置 reviews pod 的 service account
containers:
- name: reviews
image: docker.io/istio/examples-bookinfo-reviews-v1:1.15.0
imagePullPolicy: IfNotPresent
env:
- name: LOG_DIR
value: "/tmp/logs"
ports:
- containerPort: 9080
...
在导出的配置中可以看到 Envoy 通过 SDS 服务器获取到的证书(为了简略起见,省略了证书中间的部分内容)。
{
"@type": "type.googleapis.com/envoy.admin.v3.SecretsConfigDump",
"dynamic_active_secrets": [
{
"name": "default",
"version_info": "05-14 03:16:30.900",
"last_updated": "2020-05-14T03:16:31.125Z",
"secret": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
"name": "default",
"tls_certificate": {
"certificate_chain": {
"inline_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FUXXXXXXXXXXXXXXUZJQ0FURS0tLS0tCg==" // details 服务的证书,该证书被同时用作了服务器证书和客户端证书
},
"private_key": {
"inline_bytes": "W3JlZGFjdGVkXQ==" // detatils 服务证书对应的私钥
}
}
}
},
{
"name": "ROOTCA",
"version_info": "2020-05-14 03:16:31.300416193 +0000 UTC m=+1.537483882",
"last_updated": "2020-05-14T03:16:31.343Z",
"secret": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret",
"name": "ROOTCA",
"validation_context": {
"trusted_ca": {
"inline_bytes": "LS0tLS1CRUdJTiBDRVJUSUZJQ0XXXXXXXXXXXXXXXXXXXXJQ0FURS0tLS0tCg==" // 用于验证通信对方证书的 CA 根证书
}
}
}
}
]
}
通过上面的配置,我们可以看到,虽然需要在 Enovy 边车配置文件中不同的位置为 Envoy 配置服务器、客户端证书以及验证对方的 CA 根证书,但 Istio 中实际上只采用了一个服务证书和 CA 根证书。Isito 将名称为 default 的证书被同时用于 Inbound Listener 的服务器证书和 Outbound Cluster 的客户端证书,并将名称为 ROOTCA 的证书被用于验证下游客户端证书和上游服务器证书的根证书。
除了需要和网格内部的服务进行通信之外,Ingress Gateway 和 Egress Gateway 还需要连接到网格外部的系统。如果这些外部连接也需要采用 TLS,则 Gateway 中也要配置这些外部系统的相关证书。
Ingress Gateway 中需要如下证书相关的配置:
Egress Gateway 中需要如下证书相关的配置:
由于 Gateway 中配置的和外部系统相关的证书不是通过 SDS 从 Istio CA 获取的,而是采用第三方 CA 颁发的,因此到期后并不能自动更新,而需要手动进行更新。因此需要注意这些证书的有效期,在证书过期前及时重新申请证书并更新到 Gateway 配置中,以避免影响业务。
在 Gateway 上配置第三方证书的方法是采用 Kubernetes Secret 和 Istio Gateway CRD。例如我们可以采用下面的步骤在 Ingress Gateway 上配置对外提供服务使用的服务器证书。
首先创建一个 secret,该 secret 中包含了服务器证书和私钥:
kubectl create -n istio-system secret tls bookinfo-credential --key=bookinfo.example.com.key --cert=bookinfo.example.com.crt
然后通过 Gateway CRD 定义一个对外提供服务的虚拟主机,并指定使用刚才定义的 secret。
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: mygateway
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: httpbin-credential # 在此处设置包含了服务器证书和私钥的 secret
hosts:
- bookinfo.example.com
Istio 将此配置通过 xDS 接口下发到 Ingress Gateway Pod 中的 Envoy 上,可以在该 Envoy 的配置导出中看到 Ingress 网关对外提供的 443 端口上的证书配置(配置文件中的端口是8443,这是因为 Pod 内使用了8443端口,但对外暴露的 LoadBalancer 上的端口是443)。
{
"name": "0.0.0.0_8443",
"active_state": {
"version_info": "2020-05-27T07:43:51Z/21",
"listener": {
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "0.0.0.0_8443",
"address": {
"socket_address": {
"address": "0.0.0.0",
"port_value": 8443
}
},
"filter_chains": [
{
"filter_chain_match": {
"server_names": [
"bookinfo.example.com"
]
},
"filters": [...],
"transport_socket": {
"name": "envoy.transport_sockets.tls",
"typed_config": {
"@type": "type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext",
"common_tls_context": {
"alpn_protocols": [
"h2",
"http/1.1"
],
"tls_certificate_sds_secret_configs": [
{
"name": "bookinfo-credential", // Ingress Gateway 中配置的 Kubernetes secret
"sds_config": {
"api_config_source": {
"api_type": "GRPC",
"grpc_services": [
{
"google_grpc": {
"target_uri": "unix:/var/run/ingress_gateway/sds", // 从本地 unix domain socket 上的 SDS 服务器获取服务器证书
"stat_prefix": "sdsstat"
}
}
]
}
}
}
]
},
"require_client_certificate": false
}
}
}
],
},
}
}
从配置中可以看出,Ingress Gateway 使用的服务器证书也是通过 SDS 服务获取的。Pilot-agent 在路径unix:/var/run/ingress_gateway/sds
上为 Ingress Gateway 提供了一个基于 unix domain socket 的 SDS 服务。Ingress Gateway 中的 Envoy 向该 SDS 服务器请求上述配置文件中的 secret,Pilot-agent 从 Kubernetes 中查到该 同名 secret,然后转换为 SDS 消息返回给 Envoy。
备注:
图4. Ingress Gateway 证书获取流程
下图中以 bookinfo 来举例说明 Istio 在数据面使用到的所有证书。为了方便说明 Gateway 的证书配置,我们假设在 Ingress Gateway 上以 bookinfo.example.com 的主机名对外提供服务,并且 ratings 服务通过 Egress Gateway 访问了一个网格外部的第三方 TLS 服务。
图中不同颜色边框的图标代表了不同的证书。该示例中一共使用了七个不同的证书,分别为3个服务的证书(同时用作服务器和客户端证书),Ingress Gateway 自身的客户端证书,Ingress Gateway 对外部提供服务的服务器证书,Egress Gateway 自身的服务器证书,Egress Gateway 访问外部服务使用的客户端证书。
除了 Ingress Gateway 对外提供服务的服务器证书和 Egress Gateway 访问第三方服务的客户端证书之外,其他证书都是 Envoy 通过 SDS 服务从 Istio CA 获取的,因此都使用 Istio Root CA 证书进行验证。这两个第三方证书则需要采用第三方 CA 根证书进行验证。
图5. Istio 数据面使用到的所有证书
微服务应用本质上是一个分布式的网络程序,在微服务应用内存在大量的服务间网络通信。在云化部署环境中,服务间的身份认证和安全通信是微服务面临的一大挑战。Istio 建立了一套以数字证书为基础的服务认证安全框架,在不修改应用的前提下提供了服务之间的身份认证和安全通信,并以身份认证为基础提供了强大的授权机制。