Istio(八):istio安全之认证,启用mTLS

一.模块概览

在Kubernetes集群中,可以使用token进行认证,或者使用kubeconfig进行认证;对于istio来说,有两种认证方式:对等认证和请求认证。

在 Istio 中,有多个组件参与提供安全功能:

  • 用于管理钥匙和证书的证书颁发机构(CA)。
  • Sidecar 和周边代理:实现客户端和服务器之间的安全通信,它们作为政策执行点(Policy Enforcement Point,简称PEP)工作
  • Envoy 代理扩展:管理遥测和审计
  • 配置 API 服务器:分发认证、授权策略和安全命名信息

使用istio的前提是已经安装好了istio,关于istio的安装部署,请查看博客《Istio(二):在Kubernetes(k8s)集群上安装部署istio1.14》https://www.cnblogs.com/renshengdezheli/p/16836404.html

二.系统环境

服务器版本 docker软件版本 Kubernetes(k8s)集群版本 Istio软件版本 CPU架构
CentOS Linux release 7.4.1708 (Core) Docker version 20.10.12 v1.21.9 Istio1.14 x86_64

Kubernetes集群架构:k8scloude1作为master节点,k8scloude2,k8scloude3作为worker节点

服务器 操作系统版本 CPU架构 进程 功能描述
k8scloude1/192.168.110.130 CentOS Linux release 7.4.1708 (Core) x86_64 docker,kube-apiserver,etcd,kube-scheduler,kube-controller-manager,kubelet,kube-proxy,coredns,calico k8s master节点
k8scloude2/192.168.110.129 CentOS Linux release 7.4.1708 (Core) x86_64 docker,kubelet,kube-proxy,calico k8s worker节点
k8scloude3/192.168.110.128 CentOS Linux release 7.4.1708 (Core) x86_64 docker,kubelet,kube-proxy,calico k8s worker节点

三.istio认证

为了解释什么是认证或 authn,我们将从访问控制试图回答的问题开始:一个主体能否操作另一个对象?

如果我们把上述问题换到 Istio 和 Kubernetes 语境,那将是 "服务 X 能否操作服务 Y?"

这个问题的三个关键部分是:委托人、动作和对象

主体和对象都是 Kubernetes 中的服务。动作,假设我们谈论的是 HTTP,可以是 GET、POST 或者 PUT 请求等等。

认证是关于委托人(或者在我们的例子中是服务的身份)的。认证是验证某种凭证的行为,并确保该凭证是有效和可信的。一旦进行了认证,我们就有了一个经过认证的委托人。下次你旅行时,你向海关官员出示你的护照或身份证,他们会对其进行认证,确保你的凭证(护照或身份证)是有效和可信的。

在 Kubernetes 中,每个工作负载都被分配了一个独特的身份来与其他每个工作负载进行通信——该身份以服务账户的形式提供给工作负载。服务账户是运行时中存在的身份 Pod

Istio 使用来自服务账户的 X.509 证书,它根据名为 SPIFFE(每个人的安全生产身份框架)的规范创建一个新的身份

证书中的身份被编码在证书的 Subject alternate name 字段中,它看起来像这样。

 spiffe://cluster.local/ns/<pod namespace>/sa/<pod service account>

当两个服务开始通信时,它们需要交换带有身份信息的凭证,以相互验证自己。客户端根据安全命名信息检查服务器的身份,看它是否是服务的授权运行者。

服务器根据授权策略确定客户可以访问哪些信息。此外,服务器可以审计谁在什么时间访问了什么,并决定是否批准或拒绝客户对服务器的调用。

安全命名信息包含从服务身份到服务名称的映射。服务器身份是在证书中编码的,而服务名称是由发现服务或 DNS 使用的名称。从一个身份 A 到一个服务名称 B 的单一映射意味着 "A 被允许和授权运行服务 B"。安全命名信息由 Pilot 生成,然后分发给所有 sidecar 代理

3.1 证书创建与轮换

对于网格中的每个工作负载,Istio 提供一个 X.509 证书。一个名为 pilot-agent 的代理在每个 Envoy 代理旁边运行,并与控制平面(istiod)一起工作,自动进行密钥和证书的轮转

image-20221014160115800

Istio Agent 与 Envoy sidecar 一起工作,通过安全地传递配置和秘密,帮助它们连接到服务网格。即使 Istio 代理在每个 pod 中运行,我们也认为它是控制平面的一部分。

秘密发现服务(SDS)简化了证书管理。如果没有 SDS,证书必须作为秘密(Secret)创建,然后装入代理容器的文件系统中。当证书过期时,需要更新秘密,并重新部署代理,因为 Envoy 不会从磁盘动态重新加载证书。当使用 SDS 时,SDS 服务器将证书推送给 Envoy 实例。每当证书过期时,SDS 会推送更新的证书,Envoy 可以立即使用它们。不需要重新部署代理服务器,也不需要中断流量。在 Istio 中,Istio Agent 作为 SDS 服务器,实现了秘密发现服务接口

每次我们创建一个新的服务账户时,Citadel 都会为它创建一个 SPIFFE 身份。每当我们安排一个工作负载时,Pilot 会用包括工作负载的服务账户在内的初始化信息来配置其 sidecar。

当工作负载旁边的 Envoy 代理启动时,它会联系 Istio 代理并告诉它工作负载的服务账户。代理验证该实例,生成 CSR(证书签名请求),将 CSR 以及工作负载的服务账户证明(在 Kubernetes 中,是 pod 的服务账户 JWT)发送给 Citadel。Citadel 将执行认证和授权,并以签名的 X.509 证书作为回应。Istio 代理从 Citadel 获取响应,将密钥和证书缓存在内存中,并通过 SDS 和 Unix 域套接字将其提供给 Envoy。将密钥存储在内存中比存储在磁盘上更安全;在使用 SDS 时,Istio 绝不会将任何密钥写入磁盘。Istio 代理还定期刷新凭证,在当前凭证过期前从 Citadel 检索任何新的 SVID(SPIFFE 可验证身份文件)。

image-20221014161153254

SVID 是一个工作负载,可以用来向资源或调用者证明其身份的文件。它必须由一个权威机构签发,并包含一个 SPIFFE ID,它代表了提出该文件的服务的身份,例如,spiffe://clusterlocal/ns/my-namespace/sa/my-sa

这种解决方案是可扩展的,因为流程中的每个组件只负责一部分工作。例如,Envoy 负责过期证书,Istio 代理负责生成私钥和 CSR,Citadel 负责授权和签署证书。

3.2 对等认证和请求认证

Istio 提供两种类型的认证:对等认证和请求认证。

3.2.1 对等认证

对等认证用于服务间的认证,以验证建立连接的客户端

当两个服务试图进行通信时,双向 TLS 要求它们都向对方提供证书,因此双方都知道它们在与谁交谈。如果我们想在服务之间启用严格的双向 TLS,我们可以使用 PeerAuthentication 资源,将 mTLS 模式设置为 STRICT

使用 PeerAuthentication 资源,我们可以打开整个服务网格的双向 TLS(mTLS),而不需要做任何代码修改。

然而,Istio 也支持一种优雅的模式,我们可以选择在一个工作负载或命名空间内进入双向 TLS。这种模式被称为许可模式

当你安装 Istio 时,许可模式是默认启用的。启用许可模式后,如果客户端试图通过双向 TLS 连接到我,Istio 将提供双向 TLS。如果客户端不使用双向 TLS,Istio 也可以用纯文本响应。你可以允许客户端做或不做 mTLS。使用这种模式,你可以在你的服务网格中逐渐推出双向 TLS。

简而言之,PeerAuthentication 谈论的是工作负载或服务的通信方式,它并没有说到最终用户。那么,我们怎样才能认证用户呢?

3.2.2 请求认证

请求认证(RequestAuthentication 资源)验证了附加在请求上的凭证,它被用于终端用户认证

请求级认证是通过 JSON Web Tokens(JWT) 验证完成的。Istio 支持任何 OpenID Connect 提供商,如 Auth0、Firebase 或 Google Auth、Keycloak、ORY Hydra。因此,就像我们使用 SPIFFE 身份来验证服务一样,我们可以使用 JWT 令牌来验证用户。

3.3 mTLS

3.3.1 双向 TLS

服务中的工作负载之间的通信是通过 Envoy 代理进行的。当一个工作负载使用 mTLS 向另一个工作负载发送请求时,Istio 会将流量重新路由到 sidecar 代理(Envoy)

然后,sidecar Envoy 开始与服务器端的 Envoy 进行 mTLS 握手。在握手过程中,调用者会进行安全命名检查,以验证服务器证书中的服务账户是否被授权运行目标服务。一旦 mTLS 连接建立,Istio 就会将请求从客户端的 Envoy 代理转发到服务器端的 Envoy 代理。在服务器端的授权后,sidecar 将流量转发到工作负载。

我们可以在服务的目标规则中改变 mTLS 行为。支持的 TLS 模式有:DISABLE(无 TLS 连接)、SIMPLE(向上游端点发起 TLS 连接)、MUTUAL(通过出示客户端证书进行认证来使用 mTLS)和 ISTIO_MUTUAL(与 MUTUAL 类似,但使用 Istio 自动生成的证书进行 mTLS)。

3.3.2 允许模式

允许模式(Permissive Mode)是一个特殊的选项它允许一个服务同时接受纯文本流量和 mTLS 流量。这个功能的目的是为了改善 mTLS 的用户体验

默认情况下,Istio 使用允许模式配置目标工作负载。Istio 跟踪使用 Istio 代理的工作负载,并自动向其发送 mTLS 流量。如果工作负载没有代理,Istio 将发送纯文本流量

当使用允许模式时,服务器接受纯文本流量和 mTLS 流量,不会破坏任何东西。允许模式给了我们时间来安装和配置 sidecar,以逐步发送 mTLS 流量。

一旦所有的工作负载都安装了 sidecar,我们就可以切换到严格的 mTLS 模式。要做到这一点,我们可以创建一个 PeerAuthentication 资源。我们可以防止非双向 TLS 流量,并要求所有通信都使用 mTLS

我们可以创建 PeerAuthentication 资源,首先在每个命名空间中分别执行严格模式。然后,我们可以在根命名空间(在我们的例子中是 istio-system)创建一个策略,在整个服务网格中执行该策略:

 apiVersion: security.istio.io/v1beta1
 kind: PeerAuthentication
 metadata:
   name: default
   namespace: istio-system
 spec:
   mtls:
     mode: STRICT

此外,我们还可以指定 selector 字段,将策略仅应用于网格中的特定工作负载。下面的例子对具有指定标签的工作负载启用 STRICT 模式:

 apiVersion: security.istio.io/v1beta1
 kind: PeerAuthentication
 metadata:
   name: default
   namespace: my-namespace
 spec:
   selector:
     matchLabels:
       app: customers
   mtls:
     mode: STRICT

四.实战:启用mTLS

4.1 启用 mTLS

在这个实验中,我们将部署示例应用程序(Web Frontend 和 Customers 服务)。Web 前端的部署将不包含 Envoy 代理 sidecar,而 Customers 服务将被注入 sidecar。通过这个设置,我们将看到 Istio 如何同时发送 mTLS 和纯文本流量,以及如何将 TLS 模式改为 STRICT。

让我们从部署一个 Gateway 资源开始:

 apiVersion: networking.istio.io/v1alpha3
 kind: Gateway
 metadata:
   name: gateway
 spec:
   selector:
     istio: ingressgateway
   servers:
     - port:
         number: 80
         name: http
         protocol: HTTP
       hosts:
         - '*'

将上述 YAML 保存为 gateway.yaml,并使用 kubectl apply -f gateway.yaml 部署网关。

接下来,我们将创建 Web Frontend 和 Customers 服务的部署以及相关的 Kubernetes 服务。在开始部署之前,我们将禁用 default 命名空间中的自动 sidecar 注入,这样代理就不会被注入到 Web 前端部署中。在我们部署 Customers 服务之前,我们将再次启用注入。

 $ kubectl label namespace default istio-injection-
 namespace/default labeled

禁用注入后,部署 web-frontend

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: web-frontend
   labels:
     app: web-frontend
 spec:
   replicas: 1
   selector:
     matchLabels:
       app: web-frontend
   template:
     metadata:
       labels:
         app: web-frontend
         version: v1
     spec:
       containers:
         - image: gcr.io/tetratelabs/web-frontend:1.0.0
           imagePullPolicy: Always
           name: web
           ports:
             - containerPort: 8080
           env:
             - name: CUSTOMER_SERVICE_URL
               value: 'http://customers.default.svc.cluster.local'
 ---
 kind: Service
 apiVersion: v1
 metadata:
   name: web-frontend
   labels:
     app: web-frontend
 spec:
   selector:
     app: web-frontend
   ports:
     - port: 80
       name: http
       targetPort: 8080
 ---
 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
   name: web-frontend
 spec:
   hosts:
     - '*'
   gateways:
     - gateway
   http:
     - route:
         - destination:
             host: web-frontend.default.svc.cluster.local
             port:
               number: 80

将上述 YAML 保存为 web-frontend.yaml,并使用 kubectl apply -f web-frontend.yaml 创建部署和服务。如果我们看一下正在运行的 Pod,我们应该看到有一个 Pod 正在运行一个容器,由 READY 栏显示 1/1

 $ kubectl get po
 NAME                           READY   STATUS    RESTARTS   AGE
 web-frontend-659f65f49-cbhvl   1/1     Running   0          7m31s

启用自动注入:

 $ kubectl label namespace default istio-injection=enabled
 namespace/default labeled

部署 Customers 服务的 v1 版本:

 apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: customers-v1
   labels:
     app: customers
     version: v1
 spec:
   replicas: 1
   selector:
     matchLabels:
       app: customers
       version: v1
   template:
     metadata:
       labels:
         app: customers
         version: v1
     spec:
       containers:
         - image: gcr.io/tetratelabs/customers:1.0.0
           imagePullPolicy: Always
           name: svc
           ports:
             - containerPort: 3000
 ---
 kind: Service
 apiVersion: v1
 metadata:
   name: customers
   labels:
     app: customers
 spec:
   selector:
     app: customers
   ports:
     - port: 80
       name: http
       targetPort: 3000
 ---
 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
   name: customers
 spec:
   hosts:
     - 'customers.default.svc.cluster.local'
   http:
     - route:
         - destination:
             host: customers.default.svc.cluster.local
             port:
               number: 80

将上述内容保存为 customers-v1.yaml,并使用 kubectl apply -f customers-v1.yaml 创建部署和服务。

我们应该有两个应用程序的部署在运行——customers 服务将有两个容器,而 Web 前端服务将有一个:

 $ kubectl get po
 NAME                            READY   STATUS    RESTARTS   AGE
 customers-v1-7857944975-qrqsz   2/2     Running   0          4m1s
 web-frontend-659f65f49-cbhvl    1/1     Running   0          13m

如果我们尝试从 GATEWAY_URL 访问网页,我们会得到带有客服人员回应的网页。

访问 GATEWAY_URL 之所以有效,是因为采用了许可模式,纯文本流量被发送到没有代理的服务。在这种情况下,入口网关将纯文本流量发送到 Web 前端,因为没有代理。

如果我们用 getmesh istioctl dash kiali 打开 Kiali,看一下 Graph,你会发现 Kiali 检测到从入口网关到 Web 前端的调用。然而,对 Customers 服务的调用是来自未知的服务。这是因为 Web 前端旁边没有代理,Istio 不知道这个服务是谁、在哪里、是什么

image-20221027205100582

让我们更新 Customers 的 VirtualService 并将网关附加到它上面。这将使我们能够直接调用 Customers 的服务。

 apiVersion: networking.istio.io/v1alpha3
 kind: VirtualService
 metadata:
   name: customers
 spec:
   hosts:
     - 'customers.default.svc.cluster.local'
   gateways:
     - gateway
   http:
     - route:
         - destination:
             host: customers.default.svc.cluster.local
             port:
               number: 80

将上述内容保存到 vs-customers-gateway.yaml 中,并使用 kubectl apply -f vs-customers-gateway.yaml 更新 VirtualService。

现在我们可以指定 Host 头了,我们就可以通过入口网关(GATEWAY_URL)将请求发送到 Customers 服务

 $ curl -H "Host: customers.default.svc.cluster.local" http://$GATEWAY_URL;
 [{"name":"Jewel Schaefer"},{"name":"Raleigh Larson"},{"name":"Eloise Senger"},{"name":"Moshe Zieme"},{"name":"Filiberto Lubowitz"},{"name":"Ms.Kadin Kling"},{"name":"Jennyfer Bergstrom"},{"name":"Candelario Rutherford"},{"name":"Kenyatta Flatley"},{"name":"Gianni Pouros"}]

为了通过 Ingress 给 Web 前端和 Customers 服务产生一些流量,打开两个终端窗口,分别运行一条命令:

 // Terminal 1 
 $ while true; do curl -H "Host: customers.default.svc.cluster.local" http://$GATEWAY_URL; done
 ...
 // Terminal 2
 $ while true; do curl http://$GATEWAY_URL; done

打开 Kiali,看一下图表。在 Display 下拉菜单中,确保我们选中 Security 选项。你应该看到一个类似于下图的图表。

image-20221027205245517

注意在入口网关和 Customers 服务之间有一个挂锁图标,这意味着流量是使用 mTLS 发送的

如果你没有看到挂锁图标,请点击 Display 下拉菜单,确保 Security 选项被选中。

然而,在未知的(web 前端)和 Customers 服务之间,以及 istio-ingress-gateway 和 web 前端之间,都没有挂锁。Istio 在没有注入 sidecar 的情况下向服务发送纯文本流量

让我们看看如果我们在 STRICT 模式下启用 mTLS 会发生什么。我们预计从前端到 Customers 服务的调用会开始失败,因为没有注入代理来进行 mTLS 通信。另一方面,从入口网关到 Customers 服务的调用将继续工作。

 apiVersion: security.istio.io/v1beta1
 kind: PeerAuthentication
 metadata:
   name: default
   namespace: default
 spec:
   mtls:
     mode: STRICT

将上述 YAML 保存为 strict-mtls.yaml,并使用 kubectl apply -f strict-mtls.yaml 创建 PeerAuthentication 资源。

如果我们仍然在运行请求循环,我们将开始看到来自 web 前端的 ECONNRESET 错误信息。这个错误表明,Customers 端关闭了连接。在我们的例子中,这是因为它期待着一个 mTLS 连接。

另一方面,我们直接向 Customers 服务发出的请求继续工作,因为 Customers 服务旁边有一个 Envoy 代理在运行,它可以进行 mTLS。

如果我们删除之前部署的 PeerAuthentication 资源(kubectl delete peerauthentication default),Istio 就会恢复到默认状态(PERMISSIVE 模式),错误也会消失。

4.2 清理

删除 Deployment、Service、VirtualService 和 Gateway:

 kubectl delete deploy web-frontend customers-v1
 kubectl delete svc customers web-frontend
 kubectl delete vs customers web-frontend
 kubectl delete gateway gateway
posted @ 2022-10-30 02:08  人生的哲理  阅读(1541)  评论(1编辑  收藏  举报