出站 TLS 发起
任务 访问外部服务 演示了如何从服务网格内的应用程序访问外部(即服务网格之外)的 HTTP 和 HTTPS 服务。如该任务所述,ServiceEntry
用于配置 Istio 以受控方式访问外部服务。本示例展示了如何配置 Istio 以对外部服务的流量执行 TLS 起始。Istio 将对外部服务打开 HTTPS 连接,而原始流量是 HTTP。
用例
假设一个遗留应用程序执行对外部站点的 HTTP 调用。假设运营该应用程序的组织收到一项新要求,要求所有外部流量必须加密。使用 Istio,只需进行配置即可实现这一要求,无需更改应用程序中的任何代码。应用程序可以发送未加密的 HTTP 请求,然后 Istio 将为应用程序对其进行加密。
从源发送未加密的 HTTP 请求,并让 Istio 执行 TLS 升级的另一个好处是,Istio 可以生成更好的遥测,并为未加密的请求提供更多路由控制。
开始之前
按照 安装指南 中的说明设置 Istio。
启动将用作外部调用测试源的 curl 示例。
如果您启用了 自动边车注入,请部署
curl
应用程序$ kubectl apply -f @samples/curl/curl.yaml@
否则,您必须在部署
curl
应用程序之前手动注入边车$ kubectl apply -f <(istioctl kube-inject -f @samples/curl/curl.yaml@)
请注意,您可以在其中执行
exec
和curl
的任何 Pod 都会满足以下过程的要求。创建一个 shell 变量来保存用于向外部服务发送请求的源 Pod 的名称。如果您使用了 curl 示例,请运行
$ export SOURCE_POD=$(kubectl get pod -l app=curl -o jsonpath={.items..metadata.name})
配置对外部服务的访问
首先,使用与任务 访问外部服务 中展示的相同技术,配置对外部服务 edition.cnn.com
的访问。但是,这次使用单个 ServiceEntry
同时启用对该服务的 HTTP 和 HTTPS 访问。
创建一个
ServiceEntry
来启用对edition.cnn.com
的访问$ kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1 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 EOF
向外部 HTTP 服务发出请求
$ kubectl exec "${SOURCE_POD}" -c curl -- 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 ...
输出应与上述类似(某些详细信息用省略号替换)。
请注意 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
之间的通信,攻击者将知道应用程序获取了 edition.cnn.com
的哪些具体主题。出于隐私原因,您可能希望阻止此类泄露。
通过配置 Istio 执行 TLS 起始,可以解决这两个问题。
出站流量的 TLS 发起
重新定义上一节中的
ServiceEntry
以将 HTTP 请求重定向到端口 443,并添加一个DestinationRule
来执行 TLS 起始$ kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: name: edition-cnn-com spec: hosts: - edition.cnn.com ports: - number: 80 name: http-port protocol: HTTP targetPort: 443 - number: 443 name: https-port protocol: HTTPS resolution: DNS --- apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: edition-cnn-com spec: host: edition.cnn.com trafficPolicy: portLevelSettings: - port: number: 80 tls: mode: SIMPLE # initiates HTTPS when accessing edition.cnn.com EOF
上面的
DestinationRule
将对端口 80 上的 HTTP 请求执行 TLS 起始,然后ServiceEntry
将将端口 80 上的请求重定向到目标端口 443。向
http://edition.cnn.com/politics
发送 HTTP 请求,与上一节中的操作相同$ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - http://edition.cnn.com/politics HTTP/1.1 200 OK ...
这次您收到 200 OK 作为第一个也是唯一的响应。Istio 为 curl 执行了 TLS 起始,因此将原始 HTTP 请求作为 HTTPS 转发到
edition.cnn.com
。服务器直接返回了内容,无需重定向。您消除了客户端和服务器之间的双重往返,并且请求在离开网格时已加密,不会泄露您的应用程序获取了edition.cnn.com
的 politics 部分这一事实。请注意,您使用了与上一节中相同的命令。对于以编程方式访问外部服务的应用程序,无需更改代码。通过配置 Istio,无需更改一行代码即可获得 TLS 起始带来的好处。
请注意,以前使用 HTTPS 访问外部服务的应用程序将继续按原样工作
$ kubectl exec "${SOURCE_POD}" -c curl -- curl -sSL -o /dev/null -D - https://edition.cnn.com/politics HTTP/2 200 ...
其他安全注意事项
由于应用程序 Pod 与本地主机上的边车代理之间的流量仍然未加密,因此能够渗透到应用程序节点的攻击者仍然能够看到节点本地网络上的未加密通信。在某些环境中,严格的安全要求可能规定所有流量都必须加密,即使在节点的本地网络上也是如此。在这种严格要求下,应用程序应仅使用 HTTPS(TLS)。本示例中描述的 TLS 起始不足以满足此要求。
还要注意,即使应用程序发起了 HTTPS,攻击者也可以通过检查 服务器名称指示 (SNI) 来知道正在发送到 edition.cnn.com
的请求。SNI 字段在 TLS 握手期间以明文形式发送。使用 HTTPS 可以防止攻击者知道具体的主题和文章,但不能防止攻击者知道正在访问 edition.cnn.com
。
清理 TLS 发起配置
删除您创建的 Istio 配置项
$ kubectl delete serviceentry edition-cnn-com
$ kubectl delete destinationrule edition-cnn-com
出站流量的双向 TLS 发起
本节介绍如何配置边车以对外部服务执行 TLS 起始,这次使用需要双向 TLS 的服务。本示例要复杂得多,因为它需要以下设置
- 生成客户端和服务器证书
- 部署支持双向 TLS 协议的外部服务
- 配置客户端(curl Pod)以使用步骤 1 中创建的凭据
完成此设置后,您可以配置外部流量通过边车,边车将执行 TLS 起始。
生成客户端和服务器证书和密钥
对于此任务,您可以使用您喜欢的工具来生成证书和密钥。以下命令使用 openssl
创建一个根证书和私钥,用于签署服务的证书
$ 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
为
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" $ 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
您也可以选择向证书中添加
SubjectAltNames
,如果您希望为目标启用 SAN 验证。例如$ cat > san.conf <<EOF [req] distinguished_name = req_distinguished_name req_extensions = v3_req x509_extensions = v3_req prompt = no [req_distinguished_name] countryName = US [v3_req] keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth, clientAuth basicConstraints = critical, CA:FALSE subjectAltName = critical, @alt_names [alt_names] DNS = my-nginx.mesh-external.svc.cluster.local EOF $ $ 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 $ 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
生成客户端证书和私钥
$ openssl req -out client.example.com.csr -newkey rsa:2048 -nodes -keyout client.example.com.key -subj "/CN=client.example.com/O=client organization" $ 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 边车代理注入的命名空间中运行。
创建一个命名空间来表示 Istio 网格之外的服务,即
mesh-external
。请注意,由于自动边车注入未在该命名空间上 启用,因此边车代理不会自动注入到该命名空间中的 Pod 中。$ kubectl create namespace mesh-external
创建 Kubernetes Secret 来保存服务器和 CA 证书。
$ 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 服务器创建一个配置文件
$ 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 服务器的配置
$ kubectl create configmap nginx-configmap -n mesh-external --from-file=nginx.conf=./nginx.conf
部署 NGINX 服务器
$ kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: name: my-nginx namespace: mesh-external labels: run: my-nginx annotations: "networking.istio.io/exportTo": "." # simulate an external service by not exporting outside this namespace 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 replicas: 1 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
配置客户端(curl pod)
创建 Kubernetes Secret 来保存客户端的证书
$ kubectl create secret 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
必须在与客户端 Pod 部署的命名空间相同的命名空间中创建 Secret,在本例中为
default
。创建必要的
RBAC
以确保上述步骤中创建的 Secret 可供客户端 Pod 访问,在本例中为curl
。$ kubectl create role client-credential-role --resource=secret --verb=list $ kubectl create rolebinding client-credential-role-binding --role=client-credential-role --serviceaccount=default:curl
在边车上配置出站流量的双向 TLS 发起
添加一个
ServiceEntry
以将 HTTP 请求重定向到端口 443,并添加一个DestinationRule
来执行双向 TLS 起始$ kubectl apply -f - <<EOF apiVersion: networking.istio.io/v1 kind: ServiceEntry metadata: name: originate-mtls-for-nginx spec: hosts: - my-nginx.mesh-external.svc.cluster.local ports: - number: 80 name: http-port protocol: HTTP targetPort: 443 - number: 443 name: https-port protocol: HTTPS resolution: DNS --- apiVersion: networking.istio.io/v1 kind: DestinationRule metadata: name: originate-mtls-for-nginx spec: workloadSelector: matchLabels: app: curl host: my-nginx.mesh-external.svc.cluster.local trafficPolicy: loadBalancer: simple: ROUND_ROBIN portLevelSettings: - port: number: 80 tls: mode: MUTUAL credentialName: client-credential # this must match the secret created earlier to hold client certs, and works only when DR has a workloadSelector sni: my-nginx.mesh-external.svc.cluster.local # subjectAltNames: # can be enabled if the certificate was generated with SAN as specified in previous section # - my-nginx.mesh-external.svc.cluster.local EOF
上面的
DestinationRule
将对端口 80 上的 HTTP 请求执行 mTLS 起始,然后ServiceEntry
将将端口 80 上的请求重定向到目标端口 443。验证凭据是否已提供给边车并处于活动状态。
$ istioctl proxy-config secret deploy/curl | grep client-credential kubernetes://client-credential Cert Chain ACTIVE true 1 2024-06-04T12:15:20Z 2023-06-05T12:15:20Z kubernetes://client-credential-cacert Cert Chain ACTIVE true 10792363984292733914 2024-06-04T12:15:19Z 2023-06-05T12:15:19Z
向
http://my-nginx.mesh-external.svc.cluster.local
发送 HTTP 请求$ 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 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
检查
curl
Pod 的日志,查找与我们的请求相对应的行。$ kubectl logs -l app=curl -c istio-proxy | grep 'my-nginx.mesh-external.svc.cluster.local'
您应该会看到类似于以下内容的行
[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 发起配置
删除已创建的 Kubernetes 资源
$ kubectl delete secret nginx-server-certs nginx-ca-certs -n mesh-external $ kubectl delete secret client-credential $ kubectl delete rolebinding client-credential-role-binding $ kubectl delete role client-credential-role $ kubectl delete configmap nginx-configmap -n mesh-external $ kubectl delete service my-nginx -n mesh-external $ kubectl delete deployment my-nginx -n mesh-external $ kubectl delete namespace mesh-external $ kubectl delete serviceentry originate-mtls-for-nginx $ kubectl delete destinationrule originate-mtls-for-nginx
删除证书和私钥
$ 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
删除本示例中使用的生成配置文件
$ rm ./nginx.conf
清理常见配置
删除 curl
服务和部署
$ kubectl delete service curl
$ kubectl delete deployment curl