首页 > 解决方案 > 使用 mTLS 进行 GKE gRPC 入口运行状况检查

问题描述

我正在尝试使用双向 TLS 身份验证在 GKE(v1.11.2-gke.18)上实现 gRPC 服务。

当不强制执行客户端身份验证时,GKE 自动创建的 HTTP2 健康检查会响应,并且一切都连接问题。

当我打开相互身份验证时,运行状况检查失败 - 可能是因为它缺少客户端证书和密钥而无法完成连接。

与往常一样,文档很简单且相互冲突。我需要一个完全编程的解决方案(即没有控制台调整),但除了手动将运行状况检查更改为 TCP 之外,我还没有找到解决方案。

据我所见,我猜我要么需要:

或者也许还有其他一些我没有考虑过的事情?下面的配置非常适合带有 TLS 的 REST 和 gRPC,但会与 mTLS 中断。

服务.yaml

apiVersion: v1
kind: Service
metadata:
  name: grpc-srv
  labels:
    type: grpc-srv
  annotations:
    service.alpha.kubernetes.io/app-protocols: '{"grpc":"HTTP2"}'
spec:
  type: NodePort
  ports:
  - name: grpc
    port: 9999
    protocol: TCP
    targetPort: 9999
  - name: http
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: myapp

入口.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: io-ingress
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "grpc-ingress"
    kubernetes.io/ingress.allow-http: "true"
spec:
  tls:
  - secretName: io-grpc
  - secretName: io-api
  rules:
  - host: grpc.xxx.com
    http:
      paths:
      - path: /*
        backend:
          serviceName: grpc-srv
          servicePort: 9999
  - host: rest.xxx.com
    http:
      paths:
      - path: /*
        backend:
          serviceName: grpc-srv
          servicePort: 8080

标签: grpckubernetes-ingressgoogle-kubernetes-enginemutual-authenticationkubernetes-health-check

解决方案


似乎目前没有办法使用 GKE L7 入口来实现这一点。但我已经成功部署了NGINX Ingress Controller谷歌有一个关于如何在这里部署的不错的教程。

这将安装一个 L4 TCP 负载均衡器,不对服务进行健康检查,让 NGINX 处理 L7 终止和路由。这为您提供了更多的灵活性,但魔鬼在细节中,而细节并不容易获得。我发现的大部分内容都是从 github 问题中学到的。

我设法实现的是让 NGINX 处理 TLS 终止,并且仍然将证书传递到后端,因此您可以通过 CN 处理诸如用户身份验证之类的事情,或者根据 CRL 检查证书序列。

下面是我的入口文件。注释是实现 mTLS 身份验证所需的最低要求,并且仍然可以访问后端的证书。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: grpc-ingress
  namespace: master
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/auth-tls-verify-client: "on"
    nginx.ingress.kubernetes.io/auth-tls-secret: "master/auth-tls-chain"
    nginx.ingress.kubernetes.io/auth-tls-verify-depth: "2"
    nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "GRPCS"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/grpc-backend: "true"
spec:
  tls:
    - hosts:
        - grpc.example.com
      secretName: auth-tls-chain
  rules:
    - host: grpc.example.com
      http:
        paths:
          - path: /grpc.AwesomeService
            backend:
              serviceName: awesome-srv
              servicePort: 9999
          - path: /grpc.FantasticService
            backend:
              serviceName: fantastic-srv
              servicePort: 9999

需要注意的几点:

  • 秘密auth-ls-chain包含 3 个文件。ca.crt是证书链,应该包括任何中间证书。tls.crt包含您的服务器证书并tls.key包含您的私钥。
  • 如果这个秘密位于与 NGINX 入口不同的命名空间中,那么您应该在注释中提供完整路径。
  • 我的验证深度是 2,但那是因为我使用的是中间证书。如果您使用自签名,那么您只需要 1 的深度。
  • backend-protocol: "GRPCS"需要防止 NGINX 终止 TLS。如果您想让 NGINX 终止 TLS 并在不加密的情况下运行您的服务,请使用GRPC作为协议。
  • grpc-backend: "true"需要让 NGINX 知道将 HTTP2 用于后端请求。
  • 您可以列出多个路径并指向多个服务。与 GKE 入口不同,这些路径不应有正斜杠或星号后缀。

最好的部分是如果你有多个命名空间,或者如果你也在运行一个 REST 服务(例如 gRPC 网关),NGINX 将重用相同的负载均衡器。与 GKE 入口相比,这可以节省一些费用,因为 GKE 入口将为每个入口使用单独的 LB。

上面来自主命名空间,下面是来自暂存命名空间的 REST 入口。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  namespace: staging
  annotations:
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  tls:
    - hosts:
      - api-stage.example.com
      secretName: letsencrypt-staging
  rules:
    - host: api-stage.example.com
      http:
        paths:
          - path: /awesome
            backend:
              serviceName: awesom-srv
              servicePort: 8080
          - path: /fantastic
            backend:
              serviceName: fantastic-srv
              servicePort: 8080

对于 HTTP,我使用的是 LetsEncrypt,但是有很多关于如何设置它的信息。

如果你执行到ingress-nginxpod 中,你将能够看到 NGINX 是如何配置的:

...
        server {
                server_name grpc.example.com ;
                listen 80;
                set $proxy_upstream_name "-";
                set $pass_access_scheme $scheme;
                set $pass_server_port $server_port;
                set $best_http_host $http_host;
                set $pass_port $pass_server_port;

                listen 442 proxy_protocol   ssl http2;

                # PEM sha: 142600b0866df5ed9b8a363294b5fd2490c8619d
                ssl_certificate                         /etc/ingress-controller/ssl/default-fake-certificate.pem;
                ssl_certificate_key                     /etc/ingress-controller/ssl/default-fake-certificate.pem;

                ssl_certificate_by_lua_block {
                        certificate.call()
                }

                # PEM sha: 142600b0866df5ed9b8a363294b5fd2490c8619d
                ssl_client_certificate                  /etc/ingress-controller/ssl/master-auth-tls-chain.pem;
                ssl_verify_client                       on;
                ssl_verify_depth                        2;

                error_page 495 496 = https://help.example.com/auth;

                location /grpc.AwesomeService {

                        set $namespace      "master";
                        set $ingress_name   "grpc-ingress";
                        set $service_name   "awesome-srv";
                        set $service_port   "9999";
                        set $location_path  "/grpc.AwesomeServices";

                        rewrite_by_lua_block {
                                lua_ingress.rewrite({
                                        force_ssl_redirect = true,
                                        use_port_in_redirects = false,
                                })
                                balancer.rewrite()
                                plugins.run()
                        }

                        header_filter_by_lua_block {
                                plugins.run()
                        }
                        body_filter_by_lua_block {
                        }

                        log_by_lua_block {
                                balancer.log()
                                monitor.call()
                                plugins.run()
                        }

                        if ($scheme = https) {
                                more_set_headers                        "Strict-Transport-Security: max-age=15724800; includeSubDomains";
                        }

                        port_in_redirect off;
                        set $proxy_upstream_name    "master-analytics-srv-9999";
                        set $proxy_host             $proxy_upstream_name;
                        client_max_body_size                    1m;
                        grpc_set_header Host                   $best_http_host;

                        # Pass the extracted client certificate to the backend
                        grpc_set_header ssl-client-cert        $ssl_client_escaped_cert;
                        grpc_set_header ssl-client-verify      $ssl_client_verify;
                        grpc_set_header ssl-client-subject-dn  $ssl_client_s_dn;
                        grpc_set_header ssl-client-issuer-dn   $ssl_client_i_dn;

                        # Allow websocket connections
                        grpc_set_header                        Upgrade           $http_upgrade;
                        grpc_set_header                        Connection        $connection_upgrade;
                        grpc_set_header X-Request-ID           $req_id;
                        grpc_set_header X-Real-IP              $the_real_ip;
                        grpc_set_header X-Forwarded-For        $the_real_ip;
                        grpc_set_header X-Forwarded-Host       $best_http_host;
                        grpc_set_header X-Forwarded-Port       $pass_port;
                        grpc_set_header X-Forwarded-Proto      $pass_access_scheme;
                        grpc_set_header X-Original-URI         $request_uri;
                        grpc_set_header X-Scheme               $pass_access_scheme;
                        # Pass the original X-Forwarded-For
                        grpc_set_header X-Original-Forwarded-For $http_x_forwarded_for;
                        # mitigate HTTPoxy Vulnerability
                        # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
                        grpc_set_header Proxy                  "";

                        # Custom headers to proxied server
                        proxy_connect_timeout                   5s;
                        proxy_send_timeout                      60s;
                        proxy_read_timeout                      60s;
                        proxy_buffering                         off;
                        proxy_buffer_size                       4k;
                        proxy_buffers                           4 4k;
                        proxy_request_buffering                 on;
                        proxy_http_version                      1.1;
                        proxy_cookie_domain                     off;
                        proxy_cookie_path                       off;

                        # In case of errors try the next upstream server before returning an error
                        proxy_next_upstream                     error timeout;
                        proxy_next_upstream_tries               3;
                        grpc_pass grpcs://upstream_balancer;
                        proxy_redirect                          off;

                }
                location /grpc.FantasticService {

                        set $namespace      "master";
                        set $ingress_name   "grpc-ingress";
                        set $service_name   "fantastic-srv";
                        set $service_port   "9999";
                        set $location_path  "/grpc.FantasticService";

...

这只是生成的nginx.conf. 但是您应该能够看到单个配置如何跨多个命名空间处理多个服务。

最后一段是我们如何通过上下文获取证书的片段。从上面的配置可以看出,NGINX 将经过身份验证的证书和其他详细信息添加到 gRPC 元数据中。

meta, ok := metadata.FromIncomingContext(*ctx)
if !ok {
    return status.Error(codes.Unauthenticated, "missing metadata")
}

// Check if SSL has been handled upstream
if len(meta.Get("ssl-client-verify")) == 1 && meta.Get("ssl-client-verify")[0] == "SUCCESS" {
    if len(meta.Get("ssl-client-cert")) > 0 {
        certPEM, err := url.QueryUnescape(meta.Get("ssl-client-cert")[0])
        if err != nil {
            return status.Errorf(codes.Unauthenticated, "bad or corrupt certificate")
        }
        block, _ := pem.Decode([]byte(certPEM))
        if block == nil {
            return status.Error(codes.Unauthenticated, "failed to parse certificate PEM")
        }
        cert, err := x509.ParseCertificate(block.Bytes)
        if err != nil {
            return status.Error(codes.Unauthenticated, "failed to parse certificate PEM")
        }
        return authUserFromCertificate(ctx, cert)
    }
}
// if fallen through, then try to authenticate via the peer object for gRPCS, 
// or via a JWT in the metadata for gRPC Gateway.

推荐阅读