Istio Egress 出口网关使用

2023-11-20 13:32:15 浏览数 (3)

签名我们了解了位于服务网格内部的应用应如何访问网格外部的 HTTP 和 HTTPS 服务,我们学习了如何通过 ServiceEntry 对象配置 Istio 以受控的方式访问外部服务,这种方式实际上是通过 Sidecar 直接调用的外部服务,但是有时候我们可能需要通过专用的 Egress Gateway 服务来调用外部服务,这种方式可以更好的控制对外部服务的访问。

Istio 使用 Ingress 和 Egress Gateway 配置运行在服务网格边缘的负载均衡,Ingress Gateway 允许定义网格所有入站流量的入口。 Egress Gateway 是一个与 Ingress Gateway 对称的概念,它定义了网格的出口。Egress Gateway 允许我们将 Istio 的功能(例如,监视和路由规则)应用于网格的出站流量。

使用场景

比如有一个对安全要求非常严格的团队,要求服务网格所有的出站流量必须经过一组专用节点。专用节点运行在专门的机器上,与集群中运行应用程序的其他节点隔离,这些专用节点用于实施 Egress 流量的策略,并且受到比其余节点更严密地监控。

另一个使用场景是集群中的应用节点没有公有 IP,所以在该节点上运行的网格服务都无法访问互联网,那么我们就可以通过定义 Egress gateway,将公有 IP 分配给 Egress Gateway 节点,用它引导所有的出站流量,可以使应用节点以受控的方式访问外部服务。

接下来我们就来学习下在 Istio 中如何配置使用 Egress Gateway。

准备工作

如果你使用的 demo 这个配置文件安装 Istio,那么 Egress Gateway 已经默认安装了,可以通过下面的命令来查看:

代码语言:javascript复制
$ kubectl get pod -l istio=egressgateway -n istio-system
NAME                                   READY   STATUS    RESTARTS        AGE
istio-egressgateway-556f6f58f4-hkzdd   1/1     Running   0               14d

如果没有 Pod 返回,可以通过下面的步骤来部署 Istio Egress Gateway。如果你使用 IstioOperator 安装 Istio,请在配置中添加以下字段:

代码语言:javascript复制
spec:
  components:
    egressGateways:
      - name: istio-egressgateway
        enabled: true

否则使用如下的 istioctl install 命令来安装:

代码语言:javascript复制
$ istioctl install <flags-you-used-to-install-Istio> 
                   --set components.egressGateways[0].name=istio-egressgateway 
                   --set components.egressGateways[0].enabled=true

同样我们还是使用 sleep 示例做为发送请求的测试源,如果启用了自动 Sidecar 注入,运行以下命令部署示例应用程序:

代码语言:javascript复制
kubectl apply -f samples/sleep/sleep.yaml

否则,在使用以下命令部署 sleep 应用程序之前,手动注入 Sidecar:

代码语言:javascript复制
kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)

为了发送请求,您需要创建 SOURCE_POD 环境变量来存储源 Pod 的名称:

代码语言:javascript复制
export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

用 Egress gateway 发起 HTTP 请求

首先创建一个 ServiceEntry 对象来允许流量直接访问外部的 edition.cnn.com 服务。

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cnn
spec:
  hosts:
    - edition.cnn.com
  ports:
    - number: 80
      name: http-port
      protocol: HTTP
    - number: 443
      name: https
      protocol: HTTPS
  resolution: DNS

发送 HTTPS 请求到 https://edition.cnn.com/politics 验证 ServiceEntry 是否已正确应用。

代码语言:javascript复制
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
# 输出如下内
HTTP/1.1 301 Moved Permanently
# ......
location: https://edition.cnn.com/politics
# ......

HTTP/2 200
Content-Type: text/html; charset=utf-8
# ......

然后为 edition.cnn.com 的 80 端口创建一个 egress Gateway,并为指向 Egress Gateway 的流量创建一个 DestinationRule 规则,如下所示:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway # 匹配 Egress Gateway Pod 的标签
  servers:
    - port:
        number: 80
        name: http
        protocol: HTTP
      hosts:
        - edition.cnn.com # 也支持通配符 * 的形式
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-cnn
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local # 目标规则为 Egress Gateway
  subsets:
    - name: cnn # 定义一个子集 cnn,没有指定 labels,则 subset 会包含所有符合 host 字段指定的服务的 Pod

在上面的对象中我们首先定义了一个 Gateway 对象,不过这里我们定义的是一个 Egress Gateway,通过 istio: egressgateway 匹配 Egress Gateway Pod 的标签,并在 servers 中定义了 edition.cnn.com 服务的 80 端口。然后定义了一个 DestinationRule 对象,指定了目标规则为 istio-egressgateway.istio-system.svc.cluster.local,并定义了一个子集 cnn

这里的子集名称是 cnn,但没有指定 labels。这意味着,这个 subset 会涵盖所有属于 istio-egressgateway.istio-system.svc.cluster.local 服务的 Pod。这种情况下,subset 的作用主要是为了在其他 Istio 配置中提供一个方便的引用名称,而不是为了区分不同的 Pod 子集。

如何再定义一个 VirtualService 对象将流量从 Sidecar 引导至 Egress Gateway,再从 Egress Gateway 引导至外部服务,如下所示:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-cnn-through-egress-gateway
spec:
  hosts:
    - edition.cnn.com
  gateways:
    - istio-egressgateway # Egress Gateway
    - mesh # 网格内部的流量
  http:
    - match:
        - gateways:
            - mesh # 这条规则适用于从服务网格内发出的流量
          port: 80
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local # 流量将被路由到 egress gateway
            subset: cnn
            port:
              number: 80
          weight: 100
    - match:
        - gateways:
            - istio-egressgateway # 这条规则适用于通过 istio-egressgateway 的流量
          port: 80
      route:
        - destination:
            host: edition.cnn.com # 流量将被路由到外部服务
            port:
              number: 80
          weight: 100

在上面的 VirtualService 对象中通过 hosts 指定 edition.cnn.com,表示该虚拟服务用于该服务的请求,gateways 字段中定义了 istio-egressgatewaymesh 两个值,istio-egressgateway 是上面我们定义的 Egress Gateway,mesh 表示该虚拟服务用于网格内部的流量,也就是说这个虚拟服务指定了如何处理来自服务网格内部以及通过 istio-egressgateway 的流量。

mesh 是一个特殊的关键字,在 Istio 中表示服务网格内的所有 Sidecar 代理。当使用 mesh 作为网关时,这意味着 VirtualService 中定义的路由规则适用于服务网格内的所有服务,即所有装有 Istio sidecar 代理的服务。

http 字段中定义了两个 match,第一个 match 用于匹配 mesh 网关,第二个 match 用于匹配 istio-egressgateway 网关,然后在 route 中定义了两个 destination,第一个 destination 用于将流量引导至 Egress Gateway 的 cnn 子集,第二个 destination 用于将流量引导至外部服务。

总结来说,这个 VirtualService 的作用是控制服务网格内部到 edition.cnn.com 的流量。当流量起始于服务网格内时,它首先被路由到 istio-egressgateway,然后再路由到 edition.cnn.com,这种配置有助于统一和控制从服务网格内部到外部服务的流量,可以用于流量监控、安全控制或实施特定的流量策略。

应用上面的资源对象后,我们再次向 edition.cnn.com/politics 端点发出 curl 请求:

代码语言:javascript复制
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
# ......
HTTP/1.1 301 Moved Permanently

location: https://edition.cnn.com/politics
# ......

HTTP/2 200
Content-Type: text/html; charset=utf-8
# ......

正常和前面的一次测试输出结果是一致的,但是这次在请求是经过 istio-egressgateway Pod 发出的,我们可以查看日志来验证:

代码语言:javascript复制
kubectl logs -l istio=egressgateway -c istio-proxy -n istio-system | tail

正常会看到一行类似于下面这样的内容:

代码语言:javascript复制
[2023-11-15T08:48:38.683Z] "GET /politics HTTP/2" 301 - via_upstream - "-" 0 0 204 203 "10.244.1.73" "curl/7.81.0-DEV" "6c2c4550-92d4-955c-b6cb-83bf2b0e06f4" "edition.cnn.com" "151.101.3.5:80" outbound|80||edition.cnn.com 10.244.2.184:46620 10.244.2.184:8080 10.244.1.73:49924 - -

因为我们这里只是将 80 端口的流量重定向到 Egress Gateway 了,所以重定向后 443 端口的 HTTPS 流量将直接进入 edition.cnn.com,所以没有看到 443 端口的日志,但是我们可以通过 SOURCE_POD 的 Sidecar 代理的日志来查看到:

代码语言:javascript复制
$ kubectl logs "$SOURCE_POD" -c istio-proxy | tail
# ......
[2023-11-15T08:55:55.513Z] "GET /politics HTTP/1.1" 301 - via_upstream - "-" 0 0 191 191 "-" "curl/7.81.0-DEV" "12ce15aa-1247-9b7e-8185-4224f96f5ea0" "edition.cnn.com" "10.244.2.184:8080" outbound|80|cnn|istio-egressgateway.istio-system.svc.cluster.local 10.244.1.73:49926 151.101.195.5:80 10.244.1.73:41576 - -
[2023-11-15T08:55:55.753Z] "- - -" 0 - - - "-" 839 2487786 1750 - "-" "-" "-" "-" "151.101.195.5:443" outbound|443||edition.cnn.com 10.244.1.73:45246 151.101.67.5:443 10.244.1.73:42998 edition.cnn.com -

用 Egress gateway 发起 HTTPS 请求

上面我们已经学习了如何通过 Egress Gateway 发起 HTTP 请求,接下来我们再来学习下如何通过 Egress Gateway 发起 HTTPS 请求。

原理都是一样的,只是我们需要在相应的 ServiceEntryEgress GatewayVirtualService 中指定 TLS 协议的端口 443。

首先为 edition.cnn.com 定义 ServiceEntry 服务:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cnn
spec:
  hosts:
    - edition.cnn.com
  ports:
    - number: 443
      name: tls
      protocol: TLS
  resolution: DNS

应用该资源对象后,发送 HTTPS 请求到 https://edition.cnn.com/politics,验证该 ServiceEntry 是否已正确生效。

代码语言:javascript复制
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics
...
HTTP/2 200
Content-Type: text/html; charset=utf-8
...

接下来同样的方式为 edition.cnn.com 创建一个 Egress Gateway。除此之外还需要创建一个目标规则和一个虚拟服务,用来引导流量通过 Egress Gateway,并通过 Egress Gateway 与外部服务通信。

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
    - port:
        number: 443
        name: tls
        protocol: TLS
      hosts:
        - edition.cnn.com
      tls:
        mode: PASSTHROUGH # 透传
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-cnn
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: cnn
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-cnn-through-egress-gateway
spec:
  hosts:
    - edition.cnn.com
  gateways:
    - mesh
    - istio-egressgateway
  tls:
    - match:
        - gateways:
            - mesh
          port: 443
          sniHosts:
            - edition.cnn.com
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: cnn
            port:
              number: 443
    - match:
        - gateways:
            - istio-egressgateway
          port: 443
          sniHosts:
            - edition.cnn.com
      route:
        - destination:
            host: edition.cnn.com
            port:
              number: 443
          weight: 100

上面对象中定义的 Gateway 对象和前面的一样,只是将端口改为了 443,然后在 tls 中指定了 mode: PASSTHROUGH,表示该 Gateway 对象用于 TLS 协议的请求。然后在后面的 VirtualService 对象中就是配置 spec.tls 属性,用于指定 TLS 协议的请求的路由规则,配置方法和前面 HTTP 方式类似,只是注意要将端口改为 443,并且在 match 中指定 sniHostsedition.cnn.com,表示该虚拟服务用于处理 edition.cnn.com 的 TLS 请求。

应用上面的资源对象后,我们现在发送 HTTPS 请求到 https://edition.cnn.com/politics,输出结果应该和之前一样。

代码语言:javascript复制
$ kubectl exec "$SOURCE_POD" -c sleep -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics
...
HTTP/2 200
Content-Type: text/html; charset=utf-8
...

检查 Egress Gateway 代理的日志,则打印日志的命令是:

代码语言:javascript复制
kubectl logs -l istio=egressgateway -n istio-system

应该会看到类似于下面的内容:

代码语言:javascript复制
[2023-11-15T08:59:55.513Z] "- - -" 0 - 627 1879689 44 - "-" "-" "-" "-" "151.101.129.67:443" outbound|443||edition.cnn.com 172.30.109.80:41122 172.30.109.80:443 172.30.109.112:59970 edition.cnn.com

到这里我们就实现了通过 Egress Gateway 发起 HTTPS 请求。最后记得清理上面创建的资源对象:

代码语言:javascript复制
$ kubectl delete serviceentry cnn
$ kubectl delete gateway istio-egressgateway
$ kubectl delete virtualservice direct-cnn-through-egress-gateway
$ kubectl delete destinationrule egressgateway-for-cnn

需要注意的是,Istio 无法强制让所有出站流量都经过 Egress Gateway, Istio 只是通过 Sidecar 代理实现了这种流向。攻击者只要绕过 Sidecar 代理, 就可以不经 Egress Gateway 直接与网格外的服务进行通信,从而避开了 Istio 的控制和监控。出于安全考虑,集群管理员和云供应商必须确保网格所有的出站流量都要经过 Egress Gateway。这需要通过 Istio 之外的机制来满足这一要求。例如,集群管理员可以配置防火墙,拒绝 Egress Gateway 以外的所有流量。Kubernetes NetworkPolicy 也能禁止所有不是从 Egress Gateway 发起的出站流量,但是这个需要 CNI 插件的支持。 此外,集群管理员和云供应商还可以对网络进行限制,让运行应用的节点只能通过 gateway 来访问外部网络。要实现这一限制,可以只给 Gateway Pod 分配公网 IP,并且可以配置 NAT 设备, 丢弃来自 Egress Gateway Pod 之外的所有流量。

Egress TLS Origination

接下来我们将学习如何通过配置 Istio 去实现对发往外部服务的流量的 TLS Origination(TLS 发起)。 若此时原始的流量为 HTTP,则 Istio 会将其转换为 HTTPS 连接。TLS Origination 的概念前面我们也已经介绍过了。

TLS Origination

假设有一个传统应用正在使用 HTTP 和外部服务进行通信,如果有一天突然有一个新的需求,要求必须对所有外部的流量进行加密。此时,使用 Istio 便可通过修改配置实现此需求,而无需更改应用中的任何代码。该应用可以发送未加密的 HTTP 请求,由 Istio 为请求进行加密。

从应用源头发起未加密的 HTTP 请求,并让 Istio 执行 TLS 升级的另一个好处是可以产生更好的遥测并为未加密的请求提供更多的路由控制。

下面我们就来学习下如何配置 Istio 实现 TLS Origination。

同样我们这里使用 sleep 示例应用来作为测试源,如果启用了自动注入 Sidecar,那么可以直接部署 sleep 应用:

代码语言:javascript复制
kubectl apply -f samples/sleep/sleep.yaml

否则在部署 sleep 应用之前,必须手动注入 Sidecar:

代码语言:javascript复制
kubectl apply -f <(istioctl kube-inject -f samples/sleep/sleep.yaml)

创建一个环境变量来保存用于将请求发送到外部服务 Pod 的名称:

代码语言:javascript复制
export SOURCE_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})

配置对外部服务的访问

首先使用 ServiceEntry 对象来配置对外部服务 edition.cnn.com 的访问。这里我们将使用单个 ServiceEntry 来启用对服务的 HTTP 和 HTTPS 访问。创建一个如下所示的 ServiceEntry 对象:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: edition-cnn-com
spec:
  hosts:
    - edition.cnn.com
  ports:
    - number: 80
      name: http-port
      protocol: HTTP
    - number: 443
      name: https-port
      protocol: HTTPS
  resolution: DNS

上面的 ServiceEntry 对象中我们指定了 edition.cnn.com 服务的主机名,然后在 ports 中指定了需要暴露的端口及其属性,表示该 ServiceEntry 对象代表对 edition.cnn.com 的访问,这里我们定义了 80443 两个端口,分别对应 httphttps 服务,resolution: DNS 定义了如何解析指定的 hosts,这里我们使用 DNS 来解析。

直接应用该资源对象,然后向外部的 HTTP 服务发送请求:

代码语言:javascript复制
$ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
# 输出如下结果
HTTP/1.1 301 Moved Permanently
# ......
location: https://edition.cnn.com/politics

HTTP/2 200
content-type: text/html; charset=utf-8
# ......

上面我们在使用 curl 命令的时候添加了一个 -L 标志,该标志指示 curl 将遵循重定向。 在这种情况下,服务器将对到 http://edition.cnn.com/politics 的 HTTP 请求进行重定向响应,而重定向响应将指示客户端使用 HTTPS 向 https://edition.cnn.com/politics 重新发送请求,对于第二个请求,服务器则返回了请求的内容和 200 状态码。

尽管 curl 命令简明地处理了重定向,但是这里有两个问题。第一个问题是请求冗余,它使获取 http://edition.cnn.com/politics 内容的延迟加倍,第二个问题是 URL 中的路径(在本例中为 politics)被以明文的形式发送。如果有人嗅探你的应用与 edition.cnn.com 之间的通信,他将会知晓该应用获取了此网站中哪些特定的内容。出于隐私的原因,我们可能希望阻止这些内容被嗅探到。通过配置 Istio 执行 TLS 发起,则可以解决这两个问题

用于 egress 流量的 TLS 发起

edition.cnn.com 创建一个出口网关,端口为 80,接收 HTTP 流量,如下所示:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
    - port:
        number: 80
        name: tls-origination-port
        protocol: HTTP
      hosts:
        - edition.cnn.com

然后为 istio-egressgateway 创建一个 DestinationRule 对象,如下所示:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-cnn
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
    - name: cnn

接着我们只需要创建一个 VirtualService 对象,将流量从 Sidecar 引导至 Egress Gateway,再从 Egress Gateway 引导至外部服务,如下所示:

代码语言:javascript复制
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-cnn-through-egress-gateway
spec:
  hosts:
    - edition.cnn.com
  gateways:
    - istio-egressgateway # Egress Gateway
    - mesh # 网格内部的流量
  http:
    - match:
        - gateways:
            - mesh
          port: 80
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            subset: cnn
            port:
              number: 80
          weight: 100
    - match:
        - gateways:
            - istio-egressgateway
          port: 80
      route:
        - destination:
            host: edition.cnn.com
            port:
              number: 443 # 443 端口
          weight: 100
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: originate-tls-for-edition-cnn-com
spec:
  host: edition.cnn.com
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
      - port:
          number: 443
        tls:
          mode: SIMPLE # initiates HTTPS for connections to edition.cnn.com

需要注意的是上面最后针对 edition.cnn.comDestinationRule 对象,在 trafficPolicy 中指定了 portLevelSettings 用于对不同的端口定义不同的流量策略,这里我们定义了 443 端口的 tls 模式为 SIMPLE,表示当访问 edition.cnn.com 的 HTTP 请求时执行 TLS 发起。

应用上面的资源对象,然后再次向 http://edition.cnn.com/politics 发送 HTTP 请求:

代码语言:javascript复制
$ kubectl exec "${SOURCE_POD}" -c sleep -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
# 直接输出200状态码
HTTP/1.1 200 OK
content-length: 2474958
content-type: text/html; charset=utf-8
# ......

这次将会收到唯一的 200 OK 响应,因为 Istio 为 curl 执行了 TLS 发起,原始的 HTTP 被升级为 HTTPS 并转发到 edition.cnn.com。服务器直接返回内容而无需重定向,这消除了客户端与服务器之间的请求冗余,使网格保持加密状态,隐藏了你的应用获取 edition.cnn.compolitics 端点的信息。

如果我们在代码中有去访问外部服务,那么我们就可以不用修改代码了,只需要通过配置 Istio 来获得 TLS 发起即可,而无需更改一行代码。

到这里我们就学习了如何通过配置 Istio 实现对外部服务的 TLS 发起。

TLS 与 mTLS 基本概念

前面我们学习了如何通过配置 Istio 实现对外部服务的 TLS 发起,这里其实还有一个 mTLS 的概念呢,由于 TLS 本身就比较复杂,对于双向 TLS(mTLS)就更复杂了。

TLS 是一个连接层协议,旨在为 TCP 连接提供安全保障。TLS 在连接层工作,可以与任何使用 TCP 的应用层协议结合使用。例如,HTTPS 是 HTTP 与 TLS 的结合(HTTPS 中的 S 指的是 SSL,即 TLS 的前身),TLS 认证的流程大致如下所示:

  • 首先向第三方机构 CA 提交组织信息、个人信息(域名)等信息并申请认证。
  • CA 通过多种手段验证申请者提供信息的真实性,如组织是否存在、企业是否合法,是否拥有域名的所有权等。如信息审核通过,CA 会向申请者签发认证文件-证书。
  • 证书包含以下信息:申请者公钥、申请者的组织信息和个人信息、签发机构 CA 的信息、有效时间、证书序列号等信息的明文,同时包含一个签名。其中签名的产生算法:首先,使用散列函数计算公开的明文信息的信息摘要,然后,采用 CA 的私钥对信息摘要进行加密,密文即签名。
  • 客户端向服务端发出请求时,服务端返回证书文件。
  • 客户端读取证书中的相关的明文信息,采用相同的散列函数计算得到信息摘要,然后,利用对应 CA 的公钥解密签名数据,对比证书的信息摘要,如果一致,则可以确认证书的合法性。
  • 客户端还会验证证书相关的域名信息、有效时间等信息; 客户端会内置信任 CA 的证书信息(包含公钥),比如浏览器基本上都带有知名公共 CA 机构的证书,如 VerisignDigicert 等,这些证书在发布时被打包在一起,当我们下载浏览器时,就经把正确的证书放进了浏览器,如果 CA 不被信任,则找不到对应 CA 的证书,证书也会被判定非法。

认证过程

当然 HTTPS 的工作流程和这个过程基本就一致了:

  • 1.客户端发起一个 HTTPS 请求。
  • 2.服务端把配置好的证书返回给客户端。
  • 3.客户端验证证书:比如是否在有效期内,证书的用途是不是匹配 Client 请求的站点,是不是在 CRL 吊销列表里面,它的上一级证书是否有效等。
  • 4.客户端使用伪随机数生成对称密钥,并通过证书里服务器的公钥进行加密,后续使用该对称密钥进行传输信息。
  • 5.服务端使用自己的私钥解密这个消息,得到对称密钥。至此,客户端和服务端都持有了相同的对称密钥。
  • 6.服务端使用对称密钥加密明文内容 A,发送给客户端。
  • 7.客户端使用对称密钥解密响应的密文,得到明文内容 A
  • 8.客户端再次发起 HTTPS 的请求,使用对称密钥加密请求的明文内容 B,然后服务器使用对称密钥解密密文,得到明文内容 B

HTTPS 工作流程

当然双向 TLS 就更为复杂了,Mutual TLS(双向 TLS),或称 mTLS,对于常规的 TLS,只需要服务端认证,mTLS 相对来说有一个额外的规定:客户端也要经过认证。在 mTLS 中,客户端和服务器都有一个证书,并且双方都使用它们的公钥/私钥对进行身份验证。

TLS 保证了真实性,但默认情况下,这只发生在一个方向上:客户端对服务器进行认证,但服务器并不对客户端进行认证。为什么 TLS 的默认只在一个方向进行认证?因为客户端的身份往往是不相关的。例如我们在访问优点知识的时候,你的浏览器已经验证了要访问的网站服务端的身份,但服务端并没有验证你的浏览器的身份,它实际上并不关心你的浏览器的身份,这对于互联网上的 Web 项目来说足够了。但是在某些情况下,服务器确实需要验证客户端的身份,例如,当客户端需要访问某些敏感数据时,服务器可能需要验证客户端的身份,以确保客户端有权访问这些数据,这就是 mTLS 的用武之地,mTLS 是保证微服务之间跨服务通信安全的好方法。

  • 首先,你想要安全的通信。当我们把我们的应用程序拆分为多个服务时,我们最终会在这些服务之间的网络上发送敏感数据。任何能够进入网络的人都有可能读取这些敏感数据并伪造请求。
  • 其次,你关心客户端的身份。首先,你要确保你能知道调用是什么时候发生的,以便进行诊断,并正确记录指标等事项。此外,你可能想对这些身份进行授权(允许 A 调用 B 吗)。当然授权是另外的话题了。

接下来我们就来测试下如何通过 egress 网关发起双向 TLS 连接。

通过 egress 网关发起双向 TLS 连接

首先使用 openssl 命令生成客户端和服务器的证书与密钥,为你的服务签名证书创建根证书和私钥:

代码语言:javascript复制
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt
# 生成 CA 证书和私钥

my-nginx.mesh-external.svc.cluster.local 创建证书和私钥:

代码语言:javascript复制
# 为 my-nginx.mesh-external.svc.cluster.local 创建私钥和证书签名请求
$ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:2048 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization"
# 使用 CA 公钥和私钥以及证书签名请求为 my-nginx.mesh-external.svc.cluster.local 创建证书
$ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in my-nginx.mesh-external.svc.cluster.local.csr -out my-nginx.mesh-external.svc.cluster.local.crt

然后生成客户端证书和私钥:

代码语言:javascript复制
# 为 client.example.com 创建私钥和证书签名请求
$ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization"
# 使用相同的 CA 公钥和私钥以及证书签名请求为 client.example.com 创建证书
$ openssl x509 -req -sha256 -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 1 -in client.example.com.csr -out client.example.com.crt

接着我们来部署一个双向 TLS 服务器,为了模拟一个真实的支持双向 TLS 协议的外部服务,我们在 Kubernetes 集群中部署一个 NGINX 服务,该服务运行在 Istio 服务网格之外,比如运行在一个没有开启 Istio Sidecar proxy 注入的命名空间中。

创建一个命名空间 mesh-external 表示 Istio 网格之外的服务,注意在这个命名空间中,Sidecar 自动注入是没有开启的,不会在 Pod 中自动注入 Sidecar proxy。

代码语言:javascript复制
kubectl create namespace mesh-external

然后创建 Kubernetes Secret,保存服务器和 CA 的证书。

代码语言:javascript复制
$ kubectl create -n mesh-external secret tls nginx-server-certs --key my-nginx.mesh-external.svc.cluster.local.key --cert my-nginx.mesh-external.svc.cluster.local.crt
$ kubectl create -n mesh-external secret generic nginx-ca-certs --from-file=example.com.crt

生成 NGINX 服务器的配置文件:

代码语言:javascript复制
$ cat <<EOF > ./nginx.conf
events {
}

http {
  log_format main '$remote_addr - $remote_user [$time_local]  $status '
  '"$request" $body_bytes_sent "$http_referer" '
  '"$http_user_agent" "$http_x_forwarded_for"';
  access_log /var/log/nginx/access.log main;
  error_log  /var/log/nginx/error.log;

  server {
    listen 443 ssl;

    root /usr/share/nginx/html;
    index index.html;

    server_name my-nginx.mesh-external.svc.cluster.local;
    ssl_certificate /etc/nginx-server-certs/tls.crt;
    ssl_certificate_key /etc/nginx-server-certs/tls.key;
    ssl_client_certificate /etc/nginx-ca-certs/example.com.crt;
    ssl_verify_client on;
  }
}
EOF

生成 Kubernetes ConfigMap 保存 NGINX 服务器的配置文件:

代码语言:javascript复制
kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf

然后就可以部署 NGINX 服务了:

代码语言:javascript复制
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  namespace: mesh-external
  labels:
    run: my-nginx
spec:
  ports:
  - port: 443
    protocol: TCP
  selector:
    run: my-nginx
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  namespace: mesh-external
spec:
  selector:
    matchLabels:
      run: my-nginx
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 443
        volumeMounts:
        - name: nginx-config
          mountPath: /etc/nginx
          readOnly: true
        - name: nginx-server-certs
          mountPath: /etc/nginx-server-certs
          readOnly: true
        - name: nginx-ca-certs
          mountPath: /etc/nginx-ca-certs
          readOnly: true
      volumes:
      - name: nginx-config
        configMap:
          name: nginx-configmap
      - name: nginx-server-certs
        secret:
          secretName: nginx-server-certs
      - name: nginx-ca-certs
        secret:
          secretName: nginx-ca-certs
EOF

现在如果我们在网格内部去直接访问这个 my-nginx 服务,是无法访问的,第一是没有内置 CA 证书,另外是 my-nginx 服务开启了 mTLS,需要客户端证书才能访问,现在我们的网格中是没有对应的客户端证书的,会出现 400 错误。

开启了双向认证

为 egress 流量配置双向 TLS

创建 Kubernetes Secret 保存客户端证书:

代码语言:javascript复制
kubectl create secret -n istio-system generic client-credential --from-file=tls.key=client.example.com.key 
  --from-file=tls.crt=client.example.com.crt --from-file=ca.crt=example.com.crt

Secret 所在的命名空间必须与出口网关部署的位置一致,我们这里是 istio-system 命名空间。

然后为 my-nginx.mesh-external.svc.cluster.local 创建一个端口为 443 的 Egress Gateway,以及目标规则和虚拟服务来引导流量流经 egress 网关并从 egress 网关流向外部服务。

代码语言:javascript复制
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
  - port:
      number: 443
      name: https
      protocol: HTTPS
    hosts:
    - my-nginx.mesh-external.svc.cluster.local
    tls:
      mode: ISTIO_MUTUAL
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: egressgateway-for-nginx
spec:
  host: istio-egressgateway.istio-system.svc.cluster.local
  subsets:
  - name: nginx
    trafficPolicy:
      loadBalancer:
        simple: ROUND_ROBIN
      portLevelSettings:
      - port:
          number: 443
        tls:
          mode: ISTIO_MUTUAL
          sni: my-nginx.mesh-external.svc.cluster.local
EOF

上面我们定义的 Gateway 对象和前面的一样,只是将端口改为了 443,然后在 tls 中指定了 mode: ISTIO_MUTUAL,表示该 Gateway 对象用于 TLS 双向认证协议的请求。

然后同样在后面的 DestinationRule 对象中配置了通过 istio-egressgateway 的流量的规则,这里我们定义了 443 端口的 tls 模式为 ISTIO_MUTUAL,表示当访问 my-nginx.mesh-external.svc.cluster.local 的 TLS 请求时执行 TLS 双向认证。

最后我们定义一个 VirtualService 对象来引导流量流经 egress 网关:

代码语言:javascript复制
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: direct-nginx-through-egress-gateway
spec:
  hosts:
  - my-nginx.mesh-external.svc.cluster.local
  gateways:
  - istio-egressgateway
  - mesh  # 网格内部的流量
  http:
  - match:
    - gateways:
      - mesh
      port: 80
    route:
    - destination:
        host: istio-egressgateway.istio-system.svc.cluster.local
        subset: nginx
        port:
          number: 443
      weight: 100
  - match:
    - gateways:
      - istio-egressgateway
      port: 443
    route:
    - destination:
        host: my-nginx.mesh-external.svc.cluster.local
        port:
          number: 443
      weight: 100
EOF

上面的 VirtualService 对象定义网格内部对 my-nginx.mesh-external.svc.cluster.local 服务的访问引导至 istio-egressgateway,然后再由 istio-egressgateway 引导流量流向外部服务。

添加 DestinationRule 执行双向 TLS:

代码语言:javascript复制
$ kubectl apply -n istio-system -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: originate-mtls-for-nginx
spec:
  host: my-nginx.mesh-external.svc.cluster.local
  trafficPolicy:
    loadBalancer:
      simple: ROUND_ROBIN
    portLevelSettings:
    - port:
        number: 443
      tls:
        mode: MUTUAL
        credentialName: client-credential # 这必须与之前创建的用于保存客户端证书的 Secret 相匹配
        sni: my-nginx.mesh-external.svc.cluster.local
EOF

发送一个 HTTP 请求至 http://my-nginx.mesh-external.svc.cluster.local

代码语言:javascript复制
$ kubectl exec "$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})" -c sleep -- curl -sS http://my-nginx.mesh-external.svc.cluster.local
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

检查 istio-egressgateway Pod 日志,有一行与请求相关的日志记录。如果 Istio 部署在命名空间 istio-system 中,打印日志的命令为:

代码语言:javascript复制
kubectl logs -l istio=egressgateway -n istio-system | grep 'my-nginx.mesh-external.svc.cluster.local' | grep HTTP

将显示类似如下的一行:

代码语言:javascript复制
[2023-11-17T08:23:51.203Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 615 17 16 "10.244.1.100" "curl/7.81.0-DEV" "434b5755-54da-9924-9e2a-a204b5a2124c" "my-nginx.mesh-external.svc.cluster.local" "10.244.1.106:443" outbound|443||my-nginx.mesh-external.svc.cluster.local 10.244.2.239:35198 10.244.2.239:8443 10.244.1.100:56448 my-nginx.mesh-external.svc.cluster.local -

双向认证

即使我们直接在网格中访问的是 HTTP 的服务,但是通过配置 Istio,我们也可以实现对外部服务的双向 TLS 认证。

参考文档:https://istio.io/latest/docs/tasks/traffic-management/egress/

0 人点赞