Egress TLS 源

控制出口流量的任务向我们展示了位于服务网格内部的应用应如何访问外部 (即服务网格之外)的 HTTP 和 HTTPS 服务。 正如该任务所述,ServiceEntry 用于配置 Istio 以受控的方式访问外部服务。 本示例将演示如何通过配置 Istio 去实现对发往外部服务的流量的 TLS origination。 若此时原始的流量为 HTTP,则 Istio 会将其转换为 HTTPS 连接。

使用场景

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

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

开始之前

  • 根据安装指南中的说明部署 Istio。

  • 启动 curl 示例应用,该应用将用作外部调用的测试源。

    如果启用了 Sidecar 的自动注入功能, 运行以下命令部署 curl 应用:

    Zip

    1. $ kubectl apply -f @samples/curl/curl.yaml@

    否则在部署 curl 应用之前,您必须手动注入 Sidecar。

    Zip

    1. $ kubectl apply -f <(istioctl kube-inject -f @samples/curl/curl.yaml@)

    请注意,实际上任何可以执行 execcurl 的 Pod 都可以用来完成这一任务。

  • 创建一个环境变量来保存用于将请求发送到外部服务 Pod 的名称。 如果您使用的是 curl 示例应用,请运行:

    1. $ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})

配置对外部服务的访问

首先,使用与访问外部服务任务中的相同配置, 来配置对外部服务 edition.cnn.com 的访问。 但这一次我们将使用单个 ServiceEntry 来启用对服务的 HTTP 和 HTTPS 访问。

  1. 创建一个 ServiceEntry 启用对 edition.cnn.com 的访问:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: edition-cnn-com
    6. spec:
    7. hosts:
    8. - edition.cnn.com
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. - number: 443
    14. name: https-port
    15. protocol: HTTPS
    16. resolution: DNS
    17. EOF
  2. 向外部的 HTTP 服务发送请求:

    1. $ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
    2. HTTP/1.1 301 Moved Permanently
    3. ...
    4. location: https://edition.cnn.com/politics
    5. ...
    6. HTTP/2 200
    7. ...

    输出应与上面类似(某些细节用省略号代替)。

请注意 curl-L 标志,该标志指示 curl 将遵循重定向。 在这种情况下,服务器将对到 http://edition.cnn.com/politics 的 HTTP 请求返回重定向响应 (301 Moved Permanently)。 而重定向响应将指示客户端使用 HTTPS 向 https://edition.cnn.com/politics 重新发送请求。 对于第二个请求,服务器则返回了请求的内容和 200 OK 状态码。

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

通过配置 Istio 执行 TLS 发起,则可以解决这两个问题。

用于出口流量的 TLS 源

  1. 重新定义上一节的 ServiceEntryVirtualService 以重写 HTTP 请求端口, 并添加一个 DestinationRule 以执行 TLS 发起。

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: edition-cnn-com
    6. spec:
    7. hosts:
    8. - edition.cnn.com
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. targetPort: 443
    14. - number: 443
    15. name: https-port
    16. protocol: HTTPS
    17. resolution: DNS
    18. ---
    19. apiVersion: networking.istio.io/v1
    20. kind: DestinationRule
    21. metadata:
    22. name: edition-cnn-com
    23. spec:
    24. host: edition.cnn.com
    25. trafficPolicy:
    26. portLevelSettings:
    27. - port:
    28. number: 80
    29. tls:
    30. mode: SIMPLE # 访问 edition.cnn.com 时启动 HTTPS
    31. EOF

    上面的 DestinationRule 将对端口 80 和 ServiceEntry 上的 HTTP 请求执行 TLS 发起。 然后将端口 80 上的请求重定向到目标端口 443。

  2. 如上一节一样,向 http://edition.cnn.com/politics 发送 HTTP 请求:

    1. $ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics
    2. HTTP/1.1 200 OK
    3. ...

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

    请注意,您使用了一些与上一节相同的命令。 对于以编程方式访问外部服务的应用程序,不需更改代码。 您可以通过配置 Istio 来获得 TLS 发起的好处,而无需更改一行代码。

  3. 请注意,使用 HTTPS 访问外部服务的应用程序将继续像以前一样工作:

    1. $ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics
    2. HTTP/2 200
    3. ...

其它安全注意事项

由于应用程序 Pod 和本地主机上的 Sidecar 代理之间的流量仍未加密, 因此能够渗透应用程序节点的攻击者仍然能够看到该节点本地网络上的未加密通信。 在某些环境中,严格的安全性要求可能规定所有流量都必须加密,即使在节点的本地网络上也是如此。 鉴于如此严格的要求,应用程序应仅使用 HTTPS(TLS),本示例中描述的 TLS 发起还不足以满足要求。

还要注意,即使应用发起的是 HTTPS 请求,攻击者也可能会通过检查 服务器名称指示(SNI) 知道客户端正在对 edition.cnn.com 发送请求。SNI 字段在 TLS 握手过程中以未加密的形式发送。 使用 HTTPS 可以防止攻击者知道客户端访问了哪些特点的内容,但并不能阻止攻击者得知客户端访问了 edition.cnn.com 站点。

清理 TLS 发起配置

移除您创建的 Istio 配置项:

  1. $ kubectl delete serviceentry edition-cnn-com
  2. $ kubectl delete destinationrule edition-cnn-com

出口流量的双向 TLS 源

本节介绍如何配置 Sidecar 为外部服务执行 TLS 发起,这次使用需要双向 TLS 的服务。 此示例涉及许多内容,需要先执行以下前置操作:

  1. 生成客户端和服务器证书
  2. 部署支持双向 TLS 协议的外部服务
  3. 将客户端(curl Pod)配置为使用在步骤 1 中创建的凭据

完成上述前置操作后,您可以将外部流量配置为经由该 Sidecar,执行 TLS 发起。

生成客户端证书、服务器证书、客户端密钥和服务器密钥

对于此任务,您可以使用您最喜欢的工具来生成证书和密钥。 以下命令使用 openssl

  1. 创建根证书和私钥来为您的服务签署证书:

    1. $ 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
  2. my-nginx.mesh-external.svc.cluster.local 创建证书和私钥:

    1. $ 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"
    2. $ 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

    或者,如果您想要为目标启用 SAN 验证,您可以将 SubjectAltNames 添加到证书中。例如:

    1. $ cat > san.conf <<EOF
    2. [req]
    3. distinguished_name = req_distinguished_name
    4. req_extensions = v3_req
    5. x509_extensions = v3_req
    6. prompt = no
    7. [req_distinguished_name]
    8. countryName = US
    9. [v3_req]
    10. keyUsage = critical, digitalSignature, keyEncipherment
    11. extendedKeyUsage = serverAuth, clientAuth
    12. basicConstraints = critical, CA:FALSE
    13. subjectAltName = critical, @alt_names
    14. [alt_names]
    15. DNS = my-nginx.mesh-external.svc.cluster.local
    16. EOF
    17. $
    18. $ openssl req -out my-nginx.mesh-external.svc.cluster.local.csr -newkey rsa:4096 -nodes -keyout my-nginx.mesh-external.svc.cluster.local.key -subj "/CN=my-nginx.mesh-external.svc.cluster.local/O=some organization" -config san.conf
    19. $ 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 -extfile san.conf -extensions v3_req
  3. 生成客户端证书和私钥:

    1. $ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization"
    2. $ 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 代理注入的命名空间中。

  1. 创建一个命名空间来表示 Istio 网格外部的服务,命名为 mesh-external。 请注意,Sidecar 代理不会自动注入到此命名空间的 Pod 中, 因为未在其中启用自动 Sidecar 注入。

    1. $ kubectl create namespace mesh-external
  2. 创建 Kubernetes Secret 来保存服务器证书和 CA 证书。

    1. $ 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
    2. $ kubectl create -n mesh-external secret generic nginx-ca-certs --from-file=example.com.crt
  3. 为 NGINX 服务器创建配置文件:

    1. $ cat <<\EOF > ./nginx.conf
    2. events {
    3. }
    4. http {
    5. log_format main '$remote_addr - $remote_user [$time_local] $status '
    6. '"$request" $body_bytes_sent "$http_referer" '
    7. '"$http_user_agent" "$http_x_forwarded_for"';
    8. access_log /var/log/nginx/access.log main;
    9. error_log /var/log/nginx/error.log;
    10. server {
    11. listen 443 ssl;
    12. root /usr/share/nginx/html;
    13. index index.html;
    14. server_name my-nginx.mesh-external.svc.cluster.local;
    15. ssl_certificate /etc/nginx-server-certs/tls.crt;
    16. ssl_certificate_key /etc/nginx-server-certs/tls.key;
    17. ssl_client_certificate /etc/nginx-ca-certs/example.com.crt;
    18. ssl_verify_client on;
    19. }
    20. }
    21. EOF
  4. 创建一个 Kubernetes ConfigMap 来保存 NGINX 服务器的配置:

    1. $ kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
  5. 部署 NGINX 服务器:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: v1
    3. kind: Service
    4. metadata:
    5. name: my-nginx
    6. namespace: mesh-external
    7. labels:
    8. run: my-nginx
    9. annotations:
    10. "networking.istio.io/exportTo": "." # simulate an external service by not exporting outside this namespace
    11. spec:
    12. ports:
    13. - port: 443
    14. protocol: TCP
    15. selector:
    16. run: my-nginx
    17. ---
    18. apiVersion: apps/v1
    19. kind: Deployment
    20. metadata:
    21. name: my-nginx
    22. namespace: mesh-external
    23. spec:
    24. selector:
    25. matchLabels:
    26. run: my-nginx
    27. replicas: 1
    28. template:
    29. metadata:
    30. labels:
    31. run: my-nginx
    32. spec:
    33. containers:
    34. - name: my-nginx
    35. image: nginx
    36. ports:
    37. - containerPort: 443
    38. volumeMounts:
    39. - name: nginx-config
    40. mountPath: /etc/nginx
    41. readOnly: true
    42. - name: nginx-server-certs
    43. mountPath: /etc/nginx-server-certs
    44. readOnly: true
    45. - name: nginx-ca-certs
    46. mountPath: /etc/nginx-ca-certs
    47. readOnly: true
    48. volumes:
    49. - name: nginx-config
    50. configMap:
    51. name: nginx-configmap
    52. - name: nginx-server-certs
    53. secret:
    54. secretName: nginx-server-certs
    55. - name: nginx-ca-certs
    56. secret:
    57. secretName: nginx-ca-certs
    58. EOF

配置客户端 —— curl Pod

  1. 创建 Kubernetes Secret 来保存客户端的证书:

    1. $ kubectl create secret generic client-credential --from-file=tls.key=client.example.com.key \
    2. --from-file=tls.crt=client.example.com.crt --from-file=ca.crt=example.com.crt

    必须在部署客户端 Pod 的统一命名空间中创建密钥,本例为 default 命名空间。

    如有必要,凭据可以包含一个证书吊销列表 (CRL), 使用 ca.crl 作为键名。如果是这样,请在上述示例中添加另一个参数来提供 CRL:--from-file=ca.crl=/some/path/to/your-crl.pem

  2. 创建必需的 RBAC 以确保在上述步骤中创建的密钥对客户端 Pod 是可访问的,在本例中是 curl

    1. $ kubectl create role client-credential-role --resource=secret --verb=list
    2. $ kubectl create rolebinding client-credential-role-binding --role=client-credential-role --serviceaccount=default:curl

为 Sidecar 上的出口流量配置双向 TLS 源

  1. 添加 ServiceEntry 将 HTTP 请求重定向到 443 端口,并且添加 DestinationRule 以执行发起双向 TLS:

    1. $ kubectl apply -f - <<EOF
    2. apiVersion: networking.istio.io/v1
    3. kind: ServiceEntry
    4. metadata:
    5. name: originate-mtls-for-nginx
    6. spec:
    7. hosts:
    8. - my-nginx.mesh-external.svc.cluster.local
    9. ports:
    10. - number: 80
    11. name: http-port
    12. protocol: HTTP
    13. targetPort: 443
    14. - number: 443
    15. name: https-port
    16. protocol: HTTPS
    17. resolution: DNS
    18. ---
    19. apiVersion: networking.istio.io/v1
    20. kind: DestinationRule
    21. metadata:
    22. name: originate-mtls-for-nginx
    23. spec:
    24. workloadSelector:
    25. matchLabels:
    26. app: curl
    27. host: my-nginx.mesh-external.svc.cluster.local
    28. trafficPolicy:
    29. loadBalancer:
    30. simple: ROUND_ROBIN
    31. portLevelSettings:
    32. - port:
    33. number: 80
    34. tls:
    35. mode: MUTUAL
    36. credentialName: client-credential # 这必须与之前创建的用于保存客户端证书的 Secret 相匹配,并且仅当 DR 具有工作负载选择器时才有效
    37. sni: my-nginx.mesh-external.svc.cluster.local
    38. # subjectAltNames: # 如果证书是使用 SAN 生成的,则可以启用该功能(如上一节所述)
    39. # - my-nginx.mesh-external.svc.cluster.local
    40. EOF

    上面 DestinationRule 将在 80 端口对 HTTP 执行发起 mTLS 请求, 之后 ServiceEntry 将把 80 端口的请求重定向到 443 端口。

    Istio 默认启用了 auto_sniauto_san_validation。 这意味着,只要您的 DestinationRule 中没有显式设置 sni, 新上游连接的传输套接字 SNI 将根据下游 HTTP 主机/授权标头进行设置。 如果在 sni 未设置时 DestinationRule 中没有设置 subjectAltNames, 则 auto_san_validation 将启动,并且新上游连接的上游出示的证书将根据下游 HTTP 主机/授权标头自动验证。

  2. 验证凭据是否已提供给 Sidecar 并且处于活跃状态:

    1. $ istioctl proxy-config secret deploy/curl | grep client-credential
    2. kubernetes://client-credential Cert Chain ACTIVE true 1 2024-06-04T12:15:20Z 2023-06-05T12:15:20Z
    3. kubernetes://client-credential-cacert Cert Chain ACTIVE true 10792363984292733914 2024-06-04T12:15:19Z 2023-06-05T12:15:19Z
  3. 发送一个 HTTP 请求到 http://my-nginx.mesh-external.svc.cluster.local

    1. $ kubectl exec "$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})" -c curl -- curl -sS http://my-nginx.mesh-external.svc.cluster.local
    2. <!DOCTYPE html>
    3. <html>
    4. <head>
    5. <title>Welcome to nginx!</title>
    6. ...
  4. 检查 curl Pod 的日志中是否有与我们的请求相对应的行:

    1. $ kubectl logs -l app=curl -c istio-proxy | grep 'my-nginx.mesh-external.svc.cluster.local'

    您应看到一行类似以下的输出:

    1. [2022-05-19T10:01:06.795Z] "GET / HTTP/1.1" 200 - via_upstream - "-" 0 615 1 0 "-" "curl/7.83.1-DEV" "96e8d8a7-92ce-9939-aa47-9f5f530a69fb" "my-nginx.mesh-external.svc.cluster.local:443" "10.107.176.65:443"

清理双向 TLS 发起配置

  1. 移除创建的 Kubernetes 资源:

    1. $ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external
    2. $ kubectl delete secret client-credential
    3. $ kubectl delete rolebinding client-credential-role-binding
    4. $ kubectl delete role client-credential-role
    5. $ kubectl delete configmap nginx-configmap -n mesh-external
    6. $ kubectl delete service my-nginx -n mesh-external
    7. $ kubectl delete deployment my-nginx -n mesh-external
    8. $ kubectl delete namespace mesh-external
    9. $ kubectl delete serviceentry originate-mtls-for-nginx
    10. $ kubectl delete destinationrule originate-mtls-for-nginx
  2. 删除证书和私钥:

    1. $ rm example.com.crt example.com.key my-nginx.mesh-external.svc.cluster.local.crt my-nginx.mesh-external.svc.cluster.local.key my-nginx.mesh-external.svc.cluster.local.csr client.example.com.crt client.example.com.csr client.example.com.key
  3. 删除本示例中用过的和生成的那些配置文件:

    1. $ rm ./nginx.conf

清理常用配置

删除 curl Service 和 Deployment:

  1. $ kubectl delete service curl
  2. $ kubectl delete deployment curl