使用 SDS 为 Gateway 提供 HTTPS 加密支持

    双向 TLS 所需的私钥、服务器证书以及根证书都由 Secret 发现服务(SDS)完成配置。

    1. 首先执行 Ingress 任务的初始化步骤,然后执行 部分中获取 Ingress 的地址和端口,在完成这些步骤之后,也就是完成了 Istio 和 的部署,并设置了 和 SECURE_INGRESS_PORT 两个环境变量的值。

    2. macOS 用户应该检查一下本机的 curl 是否是使用 LibreSSL 库进行编译的:

      如果上面的命令输出了一段 LibreSSL 的版本信息,就说明你的 curl 命令可以完成本任务的内容。否则就要想办法换一个不同的 curl 了,例如可以换用一台运行 Linux 的工作站。

    如果使用配置了 ingress gateway ,并且想要迁移 ingress gateway 使用 SDS 方法。无需其他步骤。

    可以使用各种常用工具来生成证书和私钥。这个例子中用了一个来自 https://github.com/nicholasjackson/mtls-go-example 的来完成工作。

    1. 克隆示例代码库

      1. $ git clone https://github.com/nicholasjackson/mtls-go-example
    2. 进入代码库文件夹:

      1. $ pushd mtls-go-example
    3. httpbin.example.com 生成证书。注意要把下面命令中的 password 替换为其它值。

      1. $ ./generate.sh httpbin.example.com <password>

      看到提示后,所有问题都输入 Y 即可。这个命令会生成四个目录:1_root2_intermediate3_application 以及 4_client。这些目录中包含了后续过程所需的客户端和服务端证书。

    4. 把证书移动到 httpbin.example.com 目录之中:

      1. $ mkdir ../httpbin.example.com && mv 1_root 2_intermediate 3_application 4_client ../httpbin.example.com
    5. 返回之前的目录:

      1. $ popd

    可以配置 TLS Ingress Gateway ,让它从 Ingress Gateway 代理通过 SDS 获取凭据。Ingress Gateway 代理和 Ingress Gateway 在同一个 Pod 中运行,监视 Ingress Gateway 所在命名空间中新建的 Secret。在 Ingress Gateway 中启用 SDS 具有如下好处:

    • Ingress Gateway 无需重启,就可以动态的新增、删除或者更新密钥/证书对以及根证书。

    • 无需加载 Secret 卷。创建了 kubernetes Secret 之后,这个 Secret 就会被 Gateway 代理捕获,并以密钥/证书对和根证书的形式发送给 Ingress Gateway 。

    • Gateway 代理能够监视多个密钥/证书对。只需要为每个主机名创建 Secret 并更新 Gateway 定义就可以了。

    1. 在 Ingress Gateway 上启用 SDS,并部署 Ingress Gateway 代理。 这个功能缺省是禁用的,因此需要在 Helm 中打开 ,然后生成 istio-ingressgateway.yaml 文件:

      1. $ istioctl manifest generate \
      2. --set values.gateways.istio-egressgateway.enabled=false \
      3. --set values.gateways.istio-ingressgateway.sds.enabled=true > \
      4. $HOME/istio-ingressgateway.yaml
      5. $ kubectl apply -f $HOME/istio-ingressgateway.yaml
      1. $ export SECURE_INGRESS_PORT=$(kubectl -n istio-system \
      2. get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="https")].port}')
      3. $ export INGRESS_HOST=$(kubectl -n istio-system \
      4. get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    1. 启动 httpbin 样例:

      1. $ cat <<EOF | kubectl apply -f -
      2. apiVersion: v1
      3. kind: Service
      4. metadata:
      5. name: httpbin
      6. labels:
      7. app: httpbin
      8. spec:
      9. ports:
      10. - name: http
      11. port: 8000
      12. selector:
      13. app: httpbin
      14. ---
      15. apiVersion: apps/v1
      16. kind: Deployment
      17. metadata:
      18. name: httpbin
      19. spec:
      20. replicas: 1
      21. selector:
      22. matchLabels:
      23. app: httpbin
      24. version: v1
      25. template:
      26. metadata:
      27. labels:
      28. app: httpbin
      29. version: v1
      30. spec:
      31. containers:
      32. - image: docker.io/citizenstig/httpbin
      33. imagePullPolicy: IfNotPresent
      34. name: httpbin
      35. ports:
      36. - containerPort: 8000
      37. EOF
    2. 为 Ingress Gateway 创建 Secret:

      1. $ kubectl create -n istio-system secret generic httpbin-credential \
      2. --from-file=key=httpbin.example.com/3_application/private/httpbin.example.com.key.pem \
      3. --from-file=cert=httpbin.example.com/3_application/certs/httpbin.example.com.cert.pem

      secret name 不能istio 或者 prometheus为开头, 且 secret 不能 包含 token 字段。

    3. 创建一个 Gateway ,其 servers: 字段的端口为 443,设置 credentialName 的值为 httpbin-credential。这个值就是 Secret 的名字。TLS 模式设置为 SIMPLE

      1. $ cat <<EOF | kubectl apply -f -
      2. apiVersion: networking.istio.io/v1alpha3
      3. kind: Gateway
      4. metadata:
      5. name: mygateway
      6. spec:
      7. selector:
      8. istio: ingressgateway # use istio default ingress gateway
      9. servers:
      10. - port:
      11. number: 443
      12. name: https
      13. protocol: HTTPS
      14. tls:
      15. mode: SIMPLE
      16. credentialName: "httpbin-credential" # must be the same as secret
      17. hosts:
      18. - "httpbin.example.com"
      19. EOF
    4. 配置 Gateway 的 Ingress 流量路由,并配置对应的 VirtualService

      1. $ cat <<EOF | kubectl apply -f -
      2. apiVersion: networking.istio.io/v1alpha3
      3. kind: VirtualService
      4. metadata:
      5. name: httpbin
      6. spec:
      7. hosts:
      8. - "httpbin.example.com"
      9. gateways:
      10. - mygateway
      11. http:
      12. - match:
      13. - uri:
      14. prefix: /status
      15. - uri:
      16. prefix: /delay
      17. route:
      18. - destination:
      19. port:
      20. number: 8000
      21. host: httpbin
      22. EOF
    5. 用 HTTPS 协议访问 httpbin 服务:

      httpbin 服务会返回 418 I’m a Teapot

    6. 删除 Gateway 的 Secret,并新建另外一个,然后修改 Ingress Gateway 的凭据:

        1. $ pushd mtls-go-example
        2. $ ./generate.sh httpbin.example.com <password>
        3. $ mkdir ../httpbin.new.example.com && mv 1_root 2_intermediate 3_application 4_client ../httpbin.new.example.com
        4. $ popd
        5. $ kubectl create -n istio-system secret generic httpbin-credential \
        6. --from-file=key=httpbin.new.example.com/3_application/private/httpbin.example.com.key.pem \
        7. --from-file=cert=httpbin.new.example.com/3_application/certs/httpbin.example.com.cert.pem
      1. 使用 curl 访问 httpbin 服务:

        1. $ curl -v -HHost:httpbin.example.com \
        2. --resolve httpbin.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert httpbin.new.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. https://httpbin.example.com:$SECURE_INGRESS_PORT/status/418
        5. ...
        6. HTTP/2 418
        7. ...
        8. -=[ teapot ]=-
        9. _...._
        10. .' _ _ `.
        11. | ."` ^ `". _,
        12. \_;`"---"`|//
        13. | ;/
        14. \_ _/
        15. `"""`
      2. 如果尝试使用之前的证书链来再次访问 httpbin,就会得到失败的结果:

        1. $ curl -v -HHost:httpbin.example.com \
        2. --resolve httpbin.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert httpbin.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. https://httpbin.example.com:$SECURE_INGRESS_PORT/status/418
        5. ...
        6. * TLSv1.2 (OUT), TLS handshake, Client hello (1):
        7. * TLSv1.2 (IN), TLS handshake, Server hello (2):
        8. * TLSv1.2 (IN), TLS handshake, Certificate (11):
        9. * TLSv1.2 (OUT), TLS alert, Server hello (2):
        10. * SSL certificate problem: unable to get local issuer certificate

      可以把多个主机名配置到同一个 Ingress Gateway 上,例如 httpbin.example.comhelloworld-v1.example.com。Ingress Gateway 会为每个 credentialName 获取一个唯一的凭据。

      1. 要恢复 “httpbin” 的凭据,请删除对应的 secret 并重新创建。

        1. $ kubectl -n istio-system delete secret httpbin-credential
        2. $ kubectl create -n istio-system secret generic httpbin-credential \
        3. --from-file=key=httpbin.example.com/3_application/private/httpbin.example.com.key.pem \
        4. --from-file=cert=httpbin.example.com/3_application/certs/httpbin.example.com.cert.pem
      2. 启动 hellowworld-v1 示例:

        1. $ cat <<EOF | kubectl apply -f -
        2. apiVersion: v1
        3. kind: Service
        4. metadata:
        5. name: helloworld-v1
        6. labels:
        7. app: helloworld-v1
        8. spec:
        9. ports:
        10. - name: http
        11. port: 5000
        12. selector:
        13. app: helloworld-v1
        14. ---
        15. apiVersion: apps/v1
        16. kind: Deployment
        17. metadata:
        18. name: helloworld-v1
        19. spec:
        20. replicas: 1
        21. selector:
        22. matchLabels:
        23. app: helloworld-v1
        24. version: v1
        25. template:
        26. metadata:
        27. labels:
        28. app: helloworld-v1
        29. version: v1
        30. spec:
        31. containers:
        32. - name: helloworld
        33. image: istio/examples-helloworld-v1
        34. resources:
        35. requests:
        36. cpu: "100m"
        37. imagePullPolicy: IfNotPresent #Always
        38. ports:
        39. - containerPort: 5000
        40. EOF
      3. 为 Ingress Gateway 创建一个 Secret。如果已经创建了 httpbin-credential,就可以创建 helloworld-credential Secret 了。

        1. $ pushd mtls-go-example
        2. $ ./generate.sh helloworld-v1.example.com <password>
        3. $ mkdir ../helloworld-v1.example.com && mv 1_root 2_intermediate 3_application 4_client ../helloworld-v1.example.com
        4. $ popd
        5. $ kubectl create -n istio-system secret generic helloworld-credential \
        6. --from-file=key=helloworld-v1.example.com/3_application/private/helloworld-v1.example.com.key.pem \
        7. --from-file=cert=helloworld-v1.example.com/3_application/certs/helloworld-v1.example.com.cert.pem
      4. 定义一个 Gateway ,其中包含了两个 server,都开放了 443 端口。两个 credentialName 字段分别赋值为 httpbin-credentialhelloworld-credential。设置 TLS 的 mode 为 SIMPLE

        1. $ cat <<EOF | kubectl apply -f -
        2. apiVersion: networking.istio.io/v1alpha3
        3. kind: Gateway
        4. metadata:
        5. name: mygateway
        6. spec:
        7. selector:
        8. istio: ingressgateway # use istio default ingress gateway
        9. servers:
        10. - port:
        11. number: 443
        12. name: https-httpbin
        13. protocol: HTTPS
        14. tls:
        15. mode: SIMPLE
        16. credentialName: "httpbin-credential"
        17. hosts:
        18. - "httpbin.example.com"
        19. - port:
        20. number: 443
        21. name: https-helloworld
        22. protocol: HTTPS
        23. tls:
        24. mode: SIMPLE
        25. credentialName: "helloworld-credential"
        26. hosts:
        27. - "helloworld-v1.example.com"
        28. EOF
      5. 配置 Gateway 的流量路由,配置 VirtualService

        1. $ cat <<EOF | kubectl apply -f -
        2. apiVersion: networking.istio.io/v1alpha3
        3. name: helloworld-v1
        4. spec:
        5. hosts:
        6. - "helloworld-v1.example.com"
        7. gateways:
        8. - mygateway
        9. http:
        10. - match:
        11. - uri:
        12. exact: /hello
        13. route:
        14. - destination:
        15. host: helloworld-v1
        16. port:
        17. number: 5000
        18. EOF
      6. helloworld-v1.example.com 发送 HTTPS 请求:

        1. $ curl -v -HHost:helloworld-v1.example.com \
        2. --resolve helloworld-v1.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert helloworld-v1.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. https://helloworld-v1.example.com:$SECURE_INGRESS_PORT/hello
        5. HTTP/2 200
        1. $ curl -v -HHost:httpbin.example.com \
        2. --resolve httpbin.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert httpbin.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. https://httpbin.example.com:$SECURE_INGRESS_PORT/status/418
        5. -=[ teapot ]=-
        6. _...._
        7. .' _ _ `.
        8. | ."` ^ `". _,
        9. \_;`"---"`|//
        10. | ;/
        11. \_ _/
        12. `"""`

      可以对 Gateway 的定义进行扩展,加入 的支持。要修改 Ingress Gateway 的凭据,就要删除并重建对应的 Secret。服务器会使用 CA 证书对客户端进行校验,因此需要使用 cacert 字段来保存 CA 证书:

      1. 修改 Gateway 定义,设置 TLS 的模式为 MUTUAL

        1. $ cat <<EOF | kubectl apply -f -
        2. apiVersion: networking.istio.io/v1alpha3
        3. kind: Gateway
        4. metadata:
        5. name: mygateway
        6. spec:
        7. selector:
        8. istio: ingressgateway # use istio default ingress gateway
        9. servers:
        10. - port:
        11. number: 443
        12. name: https
        13. protocol: HTTPS
        14. tls:
        15. mode: MUTUAL
        16. credentialName: "httpbin-credential" # must be the same as secret
        17. hosts:
        18. - "httpbin.example.com"
        19. EOF
      2. 使用前面的方式尝试发出 HTTPS 请求,会看到失败的过程:

        1. $ curl -v -HHost:httpbin.example.com \
        2. --resolve httpbin.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert httpbin.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. https://httpbin.example.com:$SECURE_INGRESS_PORT/status/418
        5. * TLSv1.3 (OUT), TLS handshake, Client hello (1):
        6. * TLSv1.3 (IN), TLS handshake, Server hello (2):
        7. * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
        8. * TLSv1.3 (IN), TLS handshake, Request CERT (13):
        9. * TLSv1.3 (IN), TLS handshake, Certificate (11):
        10. * TLSv1.3 (IN), TLS handshake, CERT verify (15):
        11. * TLSv1.3 (IN), TLS handshake, Finished (20):
        12. * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
        13. * TLSv1.3 (OUT), TLS handshake, Certificate (11):
        14. * TLSv1.3 (OUT), TLS handshake, Finished (20):
        15. * TLSv1.3 (IN), TLS alert, unknown (628):
        16. * OpenSSL SSL_read: error:1409445C:SSL routines:ssl3_read_bytes:tlsv13 alert certificate required, errno 0
      3. curl 命令中加入客户端证书和私钥的参数,重新发送请求。(客户端证书参数为 --cert,私钥参数为 --key

        1. $ curl -v -HHost:httpbin.example.com \
        2. --resolve httpbin.example.com:$SECURE_INGRESS_PORT:$INGRESS_HOST \
        3. --cacert httpbin.example.com/2_intermediate/certs/ca-chain.cert.pem \
        4. --cert httpbin.example.com/4_client/certs/httpbin.example.com.cert.pem \
        5. --key httpbin.example.com/4_client/private/httpbin.example.com.key.pem \
        6. https://httpbin.example.com:$SECURE_INGRESS_PORT/status/418
        7. -=[ teapot ]=-
        8. _...._
        9. .' _ _ `.
        10. | ."` ^ `". _,
        11. \_;`"---"`|//
        12. | ;/
        13. \_ _/
      4. 如果不想用 httpbin-credential secret 来存储所有的凭据, 可以创建两个单独的 secret :

        • httpbin-credential 用来存储服务器的秘钥和证书
        • httpbin-credential-cacert 用来存储客户端的 CA 证书且一定要有 -cacert 后缀

        使用以下命令创建两个单独的 secret :

        1. $ kubectl -n istio-system delete secret httpbin-credential
        2. $ kubectl create -n istio-system secret generic httpbin-credential \
        3. --from-file=key=httpbin.example.com/3_application/private/httpbin.example.com.key.pem \
        4. --from-file=cert=httpbin.example.com/3_application/certs/httpbin.example.com.cert.pem
        5. $ kubectl create -n istio-system secret generic httpbin-credential-cacert \
        6. --from-file=cacert=httpbin.example.com/2_intermediate/certs/ca-chain.cert.pem
      • 查看 INGRESS_HOSTSECURE_INGRESS_PORT 环境变量。根据下面的输出内容,确认其中是否包含了有效的值:

        1. $ kubectl get svc -n istio-system
        2. $ echo INGRESS_HOST=$INGRESS_HOST, SECURE_INGRESS_PORT=$SECURE_INGRESS_PORT
      • 检查 istio-ingressgateway 控制器的日志,搜寻其中的错误信息:

        1. $ kubectl logs -n istio-system $(kubectl get pod -l istio=ingressgateway \
        2. -n istio-system -o jsonpath='{.items[0].metadata.name}') -c istio-proxy
      • 如果使用的是 macOS,检查其编译信息,确认其中包含 LibreSSL,具体步骤在一节中有具体描述。

      • 校验在 istio-system 命名空间中是否成功创建了 Secret

        1. $ kubectl -n istio-system get secrets

        httpbin-credentialhelloworld-credential 都应该出现在列表之中。

      • 检查日志,看 Ingress Gateway 代理是否已经成功的把密钥和证书对推送给了 Ingress Gateway :

        1. $ kubectl logs -n istio-system $(kubectl get pod -l istio=ingressgateway \
        2. -n istio-system -o jsonpath='{.items[0].metadata.name}') -c ingress-sds

        正常情况下,日志中应该显示 httpbin-credential 已经成功创建。如果使用的是双向 TLS,还应该看到 httpbin-credential-cacert。通过对日志的查看,能够验证 Ingress Gateway 代理从 Ingress Gateway 收到了 SDS 请求,资源名称是 httpbin-credential,Ingress Gateway 最后得到了应有的密钥/证书对。如果使用的是双向 TLS,日志会显示出密钥/证书对已经发送给 Ingress Gateway ,Gateway 代理接收到了资源名为 httpbin-credential-cacert 的 SDS 请求,Ingress Gateway 用这种方式获取根证书。

      1. 删除 Gateway 配置、VirtualService 以及 Secret

        1. $ kubectl delete gateway mygateway
        2. $ kubectl delete virtualservice httpbin
        3. $ kubectl delete --ignore-not-found=true -n istio-system secret httpbin-credential \
        4. helloworld-credential
        5. $ kubectl delete --ignore-not-found=true virtualservice helloworld-v1
      2. 删除证书目录以及用于生成证书的代码库:

        1. $ rm -rf httpbin.example.com helloworld-v1.example.com mtls-go-example
      3. 删除用于重新部署 Ingress Gateway 的文件:

        1. $ rm -f $HOME/istio-ingressgateway.yaml