【问题标题】:GKE gRPC Ingress Health Check with mTLS使用 mTLS 进行 GKE gRPC 入口运行状况检查
【发布时间】:2019-04-23 23:09:06
【问题描述】:

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

当不强制执行客户端身份验证时,GKE 自动创建的 HTTP2 运行状况检查会响应,并且所有连接问题。

当我打开双向身份验证时,健康检查失败 - 可能是因为它缺少客户端证书和密钥而无法完成连接。

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

据我所知 我猜我要么需要:

  • 实施自定义 mTLS 运行状况检查,以防止 GKE 自动创建 HTTP2 检查
  • 在不使用 service.alpha.kubernetes.io/app-protocols: '{"grpc":"HTTP2"}' 专有注解的容器上找到一种替代方法来终止 SSL
  • 想办法为运行状况检查提供所需的凭据
  • 更改我的 go 实现,以某种方式在不需要 mTLS 的情况下提供运行状况检查,同时在所有其他端点上强制执行 mTLS

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

service.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

ingress.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

【问题讨论】:

    标签: grpc kubernetes-ingress google-kubernetes-engine mutual-authentication kubernetes-health-check


    【解决方案1】:

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

    这会安装一个 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-nginx pod,您将能够看到 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 的摘录。但是您应该能够看到单个配置如何跨多个命名空间处理多个服务。

    最后一部分是我们如何通过上下文获取证书的 go sn-p。从上面的配置可以看出,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.
    

    【讨论】:

    • 感谢@Passkit。我和你有同样的结论。我将尝试的另一个选择是使用带有 ESP 的 google 端点,其中还包括 nginx。所以是的 L4 LB,直到 Google 能够在 L7 上实施,这一定是一个相当大的挑战 :)
    • 嗨,谢谢你的回答,真的很有帮助,我试过了,但是现在,ingress没有得到IP,不知道为什么
    • 您是尝试从 gRPC 网关连接还是从直接 gRPC 连接获取 IP?
    • 嗯,现在它可以工作了,我有 IP 但只有 404 .. 这很奇怪,但我的入口有相同的 IP,我不明白.. 我已经设置了一个公共 github @987654323 @
    • @PassKit,抱歉这个无知的问题,但是你把 sn-p 放在哪里?它在到达中间件之前就失败了。顺便说一句,我正在使用 Amazon EKS。
    【解决方案2】:

    GKE 上的 HTTP/2 和 gRPC 支持尚不可用。请参阅limitation。为了解决这个问题,已经有一个feature request 在工作中。

    【讨论】:

    • 这是否意味着只要在GRPC部署或者nodeport服务前面有一个GCP负载均衡器就不能用GKE做gRPC?
    • 我们设法通过添加一个健康检查可执行文件来做到这一点。我会发布作为答案。
    • @PassKit 谢谢!当我尝试使用 GKE 托管证书时,仍在为此苦苦挣扎。我不需要 mTls,只需要 tls。我的健康检查有问题。文档太可怕了
    • @unludo 检查我修改后的答案。如果您使用 NGINX 入口控制器,这是可能的,但目前使用 GKE 的入口仍然是不可能的。
    猜你喜欢
    • 1970-01-01
    • 2021-05-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-06
    • 2019-03-04
    相关资源
    最近更新 更多