9Istio授权和认证
上一节课我们深入探讨了如何利用分布式追踪和Kiali可视化来理解服务网格的运行状态,这为我们今天讨论安全打下了基础。今天,我们将聚焦于Istio如何实现轻松的安全,特别是如何保护我们宝贵的应用数据。这不仅仅是添加防火墙那么简单,而是要确保只有授权的客户端才能访问资源,并且在传输过程中,数据是加密的,防止被窃听或篡改。这就像给你的数据加上了双重保险,一个认证,一个授权,再加上全程加密。我们先快速回顾一下应用安全的基本概念。想象一下你去银行取钱,柜员需要先确认你是谁,这就是认证。你可以用密码、银行卡或者指纹,这就是所谓的知道什么、拥有什么、是什么。确认了你是谁之后,柜员会问你:你想取多少钱?或者你想办理什么业务?这就是授权,决定你被允许做什么。在微服务里,这个过程发生在服务之间。一个服务请求另一个服务,比如订单服务请求库存服务,它必须先证明自己是谁,然后才能被允许访问库存数据,甚至修改它。那么,怎么证明身份呢?在互联网世界里,我们用的是数字证书,这背后有一整套叫做公钥基础设施的体系。你可以把它想象成一个数字身份认证系统。服务器会有一个证书,里面包含了它的公钥,就像它的身份证一样。客户端拿到这个证书,可以验证它是不是真的,是不是被篡改过。然后,客户端用这个公钥来加密发给服务器的数据,只有服务器自己手里的私钥才能解密,这就保证了数据在传输过程中的安全性。简单说,就是你用我的身份证,我才能读懂你的密信。TLS协议,全称传输层安全协议,就是我们前面提到的PKI体系在网络通信中的具体应用。它主要负责两件事:第一,验证服务器的身份,防止你连接到钓鱼网站;第二,加密你和服务器之间的数据传输。这个过程的核心是TLS握手。你可以把它想象成你和服务器之间的一次秘密对话。在这次对话中,双方会互相确认身份,然后安全地交换一个对称密钥。这个密钥就像一把钥匙,之后所有的数据传输都会用这把钥匙来加密,大大提高了效率。这个握手过程具体是怎么进行的呢?看这张图,挺清晰的。客户端首先发起请求,告诉服务器:嘿,我要建立安全连接。服务器回应,亮出自己的身份证也就是X点509证书,里面包含了公钥。客户端会仔细检查这个证书是不是真的,有没有被伪造。确认无误后,客户端会生成一个随机数,这个数很重要,是后续加密的密钥。客户端用服务器的公钥把这个随机数加密,发给服务器。服务器用自己的私钥解开,得到这个随机数。现在,双方都拿到了这个随机数,用它来生成一个对称密钥,之后所有的数据都用这个密钥加密。你看,整个过程都是加密的,非常安全。刚才我们讲的是用户访问网站,也就是终端用户认证。但在服务网格里,更重要的是服务间认证。想象一下,你的微服务架构里有成百上千个服务,它们互相调用,就像一个庞大的团队内部协作。这时候,每个服务都需要向对方证明自己的身份,不能随便就让其他服务访问你的数据。这就是双向认证,简称mTLS。客户端和服务器都得拿出自己的证书来证明我是谁。认证通过了,服务器才能决定,这个客户端服务,你有权访问我的数据吗?这就是服务间的授权。为什么微服务架构在安全上带来了新的挑战?对比一下单体应用和微服务。以前的单体应用,就像一个大家庭,所有功能都住在同一个房子里,服务之间联系不多,IP地址相对固定,用IP地址就能大致判断身份。但微服务就不一样了,它像一个庞大的社区,服务数量多,而且动态变化,随时可能有新的服务加入,旧的退出。服务可能分布在不同的云平台、不同的网络里,甚至有些还在本地机房。在这种情况下,还用老办法,比如IP地址,来识别身份,那简直太不可靠了!你需要一种更智能、更灵活的方式来管理这些服务的身份。为了解决微服务环境下的身份难题,业界提出了一个叫做SPIFFE的标准。你可以把它理解为微服务世界的身份证标准。它定义了一套规范,让每个服务都能获得一个独一无二的、可信赖的身份标识。这个身份标识叫SPIFFE ID,就像人的身份证号。为了获取这个身份,SPIFFE定义了两个关键角色:一个是工作负载端点,负责发起身份申请;另一个是工作负载API,通常是证书颁发机构,负责签发包含SPIFFE ID的证书。这个证书就是SVID,也就是SPIFFE可验证身份文档。Istio正是基于这个SPIFFE规范来实现它的自动mTLS功能的。这个SPIFFE ID长什么样呢?它遵循一个URI格式:spiffe://trust-domain/path。trust-domain是信任域,你可以把它理解为一个组织或者一个大的项目组,比如你的公司。path就是在这个组织内部,用来唯一标识这个工作负载的路径。比如,你可以用服务的名字、命名空间、甚至服务账户来构成这个路径。Istio就巧妙地利用了Kubernetes的Service Account,把服务账户的名字和命名空间组合起来,作为path的一部分,这样就能在Kubernetes环境中方便地为每个Pod生成唯一的SPIFFE ID。我们再深入看看SPIFFE的两个关键角色。首先是Workload Endpoint。你可以把它想象成每个服务Pod里的一个信使,它紧挨着服务本身。它的主要职责是:首先,它要能验证自己这个Pod是不是真的属于它声称的那个服务,这个过程叫工作负载验证。验证通过后,它会拿着这个验证结果,去请求Workload API也就是CA签发身份证书。拿到证书后,它还要负责把这个证书安全地传递给服务本身,通常是通过一个安全的通道。所以,Workload Endpoint是离服务最近的,负责发起和完成身份获取过程的。另一个关键角色是Workload API。这个角色通常是证书颁发机构(CA)。它的职责是:签发证书!它会根据Workload Endpoint提供的信息,生成一个包含SPIFFE ID的证书。这个证书就是SVID,也就是服务的身份证。Workload API需要非常安全,因为它掌握着签发身份凭证的权力。它不能直接暴露在公网,必须通过安全的通道与Workload Endpoint通信。这样,只有经过验证的工作负载才能通过安全的信使(Workload Endpoint)从可信的CA那里获取到合法的身份证明(SVID)。整个身份获取的过程是怎样的呢?看这张流程图。第一步,Workload Endpoint先验证自己这个Pod是不是真的,比如通过查询Kubernetes API。第二步,根据验证结果,它会生成一个SPIFFE ID,比如 spiffe://cluster.local/ns/default/sa/my-service。第三步,它会拿着这个ID,生成一个证书签名请求,也就是CSR。第四步,它把CSR和验证信息一起发送给Workload API。第五步,Workload API验证通过后,就会签发一个证书,这个证书就是SVID。最后一步,Workload Endpoint把这个SVID安全地交给服务本身,通常是Envoy代理。这样,服务就拥有了合法的身份证明。SVID,也就是SPIFFE可验证身份文档,是整个身份体系的核心。它的最大特点就是可验证。你拿到一张证书,怎么知道它是不是真的?是不是被别人伪造了?SVID必须能回答这个问题。SPIFFE标准定义了两种类型的SVID:一种是X.509证书,这是Istio采用的,它包含了SPIFFE ID、签名信息,以及一个公钥,用来建立加密通道。另一种是JWT令牌,也是一种可验证的声明。无论哪种,都必须包含SPIFFE ID,有有效的签名,这样才能保证身份的可信度。现在我们来看看Istio是如何具体实现SPIFFE的。还记得SPIFFE的两个核心角色吗?Workload Endpoint和Workload API。在Istio里,谁扮演了这些角色呢?Istio的Envoy Proxy,也就是那个被注入到每个Pod里的sidecar,它就承担了Workload Endpoint的角色。它负责验证自己,然后去跟Istiod也就是Istio的控制平面组件打交道。而Istiod,它内部集成了一个证书颁发机构,就扮演了Workload API的角色。所以,Istio的Envoy Proxy会自动去向Istiod申请证书,获取自己的SVID,也就是身份证明。这意味着,只要你使用Istio,它就会自动帮你处理掉这些繁琐的身份配置工作。这张图更直观地展示了Istio组件和SPIFFE规范之间的对应关系。左边是Istio的组件,右边是SPIFFE的规范。可以看到,Istio Proxy,也就是Envoy,它就负责了Workload Endpoint的工作,包括验证、获取证书、传递给服务。而Istiod,它内部的Istio CA,就负责了Workload API的工作,签发证书。这样,整个流程就被Istio内部的组件无缝地实现了。对于开发者来说,你只需要关心你的服务,Istio会帮你搞定身份认证的底层细节。我们来详细拆解一下Istio是如何自动获取证书的。这个过程非常巧妙。首先,每个Kubernetes Pod都有一个Service Account Token,里面包含了关于这个Pod的元数据。Istio的Pilot Agent,就是那个Envoy旁边的信使,它会读取这个Token,从中提取出命名空间、服务账户等信息。然后,它会用这些信息生成一个SPIFFE ID,比如 spiffe://cluster.local/ns/default/sa/default。接着,它会生成一个证书签名请求,把SPIFFE ID放进去。然后,这个请求会被发送到Istiod。Istiod收到请求后,会先去验证Token是不是真的,是不是由Kubernetes API发的。验证通过后,Istiod就会用它的私钥签发一个证书,然后把这个证书返回给Pilot Agent。最后,Pilot Agent通过一个叫做SUDS的机制,把这个证书和私钥配置给Envoy Proxy。整个过程,对用户来说是完全透明的。通过前面的介绍,我们可以看到,Istio的自动mTLS功能非常强大。它不仅自动为你的服务颁发了身份证书,还自动配置了服务之间的加密通信。这意味着,一旦你部署了服务,它们之间默认就是加密的,而且是双向认证的,除非你明确配置不允许。这极大地简化了安全设置,避免了手动配置证书的麻烦,也减少了人为错误。更重要的是,它为你的应用数据提供了坚实的安全保障,尤其是在处理敏感数据时。可以说,Istio的自动mTLS是服务网格安全的核心价值之一。虽然Istio默认会启用mTLS,但有时候我们需要更精细的控制。比如,你可能希望某个服务只允许内部经过认证的服务访问,而拒绝外部明文请求。或者你想逐步过渡到全量mTLS,先允许一些明文流量。这时候,就需要用到Istio的PeerAuthentication资源。你可以把它想象成一个交通规则制定器。它定义了服务对入站流量的mTLS要求。你可以设置为严格,要求所有请求必须是加密的;也可以设置为宽容,允许加密和非加密的请求,方便过渡。甚至可以设置为禁用,让流量绕过Istio Proxy,直接访问应用。这些规则可以在整个网格、特定的命名空间,甚至针对特定的服务标签来应用。理论讲完了,我们来动手实践一下。首先,我们清理一下环境,确保我们从一个干净的状态开始。然后创建一个新的命名空间,叫 istioinaction。在这个命名空间里,我们会部署三个服务:一个是 apigateway,它会模拟一个入口网关;一个是 catalog,它代表一个内部服务;还有一个是 sleep,它代表一个遗留系统,还没有加入Istio的mTLS。注意看命令,apigateway 和 catalog 都使用了 kubectl kube inject,这是为了自动注入Istio Proxy,而 sleep 服务没有使用,它将作为遗留服务。这样,我们就能模拟不同安全状态下的服务交互。部署完服务后,我们先验证一下默认情况。还记得吗?Istio默认是允许明文流量的。所以我们现在用 sleep 服务去访问 apigateway 服务,看看能不能成功。我们用 curl 命令从 sleep 容器内部发起请求。如果一切正常,你会看到返回状态码 200。这说明,在当前的默认配置下,即使没有经过 mTLS 加密,请求也能顺利到达 apigateway。这在初期部署或者迁移过程中,提供了一个友好的过渡。现在,我们来应用一个全局性的策略。我们创建一个 PeerAuthentication 资源,放在 Istio 的安装命名空间 istio-system,名字必须是 default。在这个策略里,我们把 mTLS 的 mode 设置为 STRICT。这意味着,从现在开始,整个服务网格里的所有服务,都必须要求入站流量是经过 mTLS 加密的。我们应用这个配置。策略生效后,我们再用 sleep 去访问 apigateway。这次会发生什么?因为我们刚刚设置了全局严格模式,apigateway 会要求所有入站请求都必须是 mTLS 加密的。而 sleep 服务没有经过 mTLS,所以它发送的是明文 HTTP 请求。结果就是,请求被拒绝了。你会看到一个错误,比如 56,表示连接失败。这证明了我们刚才的策略是有效的,全局严格模式确实阻止了明文流量。全局严格模式虽然安全,但可能对现有系统冲击太大。我们通常希望更灵活地控制。比如,我们只想让 istioinaction 这个命名空间里的服务,可以接受明文流量,但其他地方还是严格模式。怎么办?很简单,我们在 istioinaction 命名空间里创建一个同名的 PeerAuthentication 资源,名字也叫 default,然后把 mode 设置为 PERMISSIVE。这个策略会覆盖掉全局的严格策略,只对这个命名空间内的服务生效。我们再次测试。现在,sleep 服务去访问 apigateway,请求应该能成功了,因为 apigateway 位于 istioinaction 命名空间,它被设置为宽容模式。但是!如果 sleep 服务直接去访问 catalog 服务,注意,catalog 服务也在 istioinaction 命名空间,但是它没有被我们这个新的命名空间策略覆盖,因为它继承了全局的严格策略。所以,对 catalog 的请求仍然会被拒绝。这说明我们的命名空间策略只影响了 apigateway。我们还可以更进一步,只对特定的服务应用策略。比如,我们只希望 apigateway 保持宽容模式,允许 sleep 的明文访问,但同时希望 catalog 服务保持严格模式,拒绝明文访问。这可以通过创建一个针对特定服务的 PeerAuthentication 资源来实现。我们创建一个策略,名字叫 apigateway,放在 istioinaction 命名空间,然后用 selector.matchLabels 来指定这个策略只适用于标签为 app: apigateway 的服务。在这个策略里,我们把 mode 设置为 PERMISSIVE。应用这个策略后,我们再做测试。sleep 服务访问 apigateway,因为 apigateway 的策略是宽容的,所以请求成功。但是,当 sleep 服务尝试直接访问 catalog 服务时,请求仍然失败。这是因为 catalog 服务没有被这个策略选中,它继承的是全局的严格策略。这样,我们就能精确地控制不同服务的 mTLS 要求,实现精细化的安全管理。这些 PeerAuthentication 策略是怎么生效的呢?背后是 Istio 的控制平面组件 Istiod 在默默工作。Istiod 会一直监听 Kubernetes 中有没有新的 PeerAuthentication 资源被创建或修改。一旦有变化,Istiod 就会把这种策略转换成 Istio Proxy 也就是 Envoy 能理解的配置,然后通过一个叫做 L D S 的机制,把这些配置推送到对应的 Envoy Proxy 上。这样,每个 Proxy 就知道该对哪些请求执行什么安全策略了。整个过程是自动化的,不需要手动重启服务。我们已经通过 curl 命令验证了请求是否成功,但要真正确认 mTLS 是否在加密通信,我们需要更直接的证据。这里我们用 tcpdump 工具来抓包分析。不过,为了能抓到网络流量,我们需要给 Istio Proxy 一些特殊的权限,让它能访问网络接口。注意,这只是为了调试,生产环境绝对不能这么干!我们修改 Istio 安装配置,启用 privileged 模式,然后重新部署 apigateway 服务。等新的 Pod 启动后,我们进入 apigateway 的 istio proxy 容器,运行 tcpdump 命令来抓取进出这个容器的网络流量。在另一个终端里,我们再次用 sleep 服务去访问 apigateway。回到我们运行 tcpdump 的那个终端,看看输出。你会发现,tcpdump 显示了 sleep 服务发送给 apigateway 的请求,比如 GET /api/catalog HTTP/1.1,这些信息是明文的!但是,紧接着 apigateway 发送给 catalog 服务的流量,你会发现大量的 TLS 握手信息和加密数据。这清晰地表明,apigateway 和 catalog 之间是通过 mTLS 加密的,而 sleep 到 apigateway 的连接是明文的。这再次验证了我们之前用 PeerAuthentication 设置的策略是有效的。最后,我们来验证一下 Istio 签发的证书是不是真的包含了正确的 SPIFFE ID。我们用 openssl 命令连接到 catalog 服务,获取它的证书。然后用 openssl x509 命令查看证书内容。重点关注 Subject Alternative Name 这一项,它应该包含一个 URI,格式类似 URI:spiffe://cluster.local/ns/istioinaction/sa/default。这个 URI 就是我们之前看到的 SPIFFE ID,它证明了这个证书确实是由 Istio 为 catalog 服务签发的,且包含了正确的身份标识。这确保了我们服务的身份是真实可信的。