grpc - 使用 mTLS 进行 GKE gRPC 入口运行状况检查
问题描述
我正在尝试使用双向 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 中断。
服务.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
解决方案
似乎目前没有办法使用 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-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
. 但是您应该能够看到单个配置如何跨多个命名空间处理多个服务。
最后一段是我们如何通过上下文获取证书的片段。从上面的配置可以看出,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.
推荐阅读
- python - 使用venv时如何选择要运行的python可执行文件?
- python - 当我在同一个代码集中第二次运行 requests.get 和 json 时,为什么会得到一个空数组?
- javascript - What is the Javascript equivelant for the Visual Basic function "GetRef"?
- flutter - 如何在 PageView 内的 TabBarView 上“合并”滚动?
- c - 为什么最后一个字符串不能正确输入显示字符频率的多任务数组?
- typeorm - 如何在 TypeORM 上使用 onDelete: 'CASCADE'
- excel - 导入数据宏添加新列
- gitlab - Gitlab fork 项目
- bar-chart - 如何为 Splunk 条形图中的每一列指定特定颜色?
- c - Conflicting types error in the function declaration of a division with floating point function