在上一篇文章中,我们检查了不同的(SSL / TLS)证书组合以保护gRPC通道。随着端点数量的增加,这个过程很快就会变得太复杂而无法手动执行。现在是时候看看如何自动生成签名证书,我们的gRPC端点可以在没有我们干预的情况下使用它们。我们将探索私有和公共领域的替代方案。如果要直接跳转到代码中,请查看存储库。
这是一系列三篇文章的第2部分。在第1部分中,我们介绍了手动设置gRPC TLS连接。相互认证将在第3部分中讨论。
介绍
我们需要一个我们可以通过Go gRPC端点与之交互的证书颁发机构(CA)。
对于私有域,我们选择的CA将是Vault PKI Secrets Engine。为了从我们的gRPC端点生成证书签名请求(CSR)和续订,我们将使用Certify。
对于公共证书的生成和分发,我们将使用Let’s Encrypt ; 一个免费的,自动化的,开放的证书颁发机构 ……这有多酷!?他们唯一需要的是使用自动证书管理环境( ACME )协议演示对域的控制。这意味着我们需要一个ACME客户端,幸运的是,我们可以为此选择一个Go 库列表。在这个机会中,我们将使用autocert的易用性和对TLS-ALPN-01挑战的支持。
私有域: Vault 和Certify
Vault
Vault是一个秘密管理和数据保护开源项目,可以存储和控制对证书的访问,以及密码和令牌等其他秘密。它以二进制形式发布,可以放在你的任何地方$PATH。如果您想了解有关Vault的更多信息,其入门指南是一个很好的起点。此处记录了此帖子所用设置的所有详细信息。
首先,我们运行Vault vault Server -config=vault_config.hcl。配置文件(vault_config.hcl)提供storage存储Vault数据的后端。为简单起见,我们只使用本地文件。您也可以选择将其存储在内存中,云存储提供商或其他地方。查看存储Stanza中的所有选项。
storage "file" { path = ".../data" }
此外,我们还指定了Vault将绑定的地址。默认情况下启用TLS,因此我们需要提供证书和私钥对。如果您选择对这些进行自签名(请参阅这些说明以获取示例),请确保将Root证书(ca.cert)保留为方便,稍后您将需要它来向Vault(*)发出请求。tcp Listener Parameters中记录了其他TCP配置选项。
listener "tcp" {
address = " localhost :8200"
tls_cert_file = ".../vault.pem"
tls_key_file = ".../vault.key"
}
经过初始化Vault的服务器和解封Vault可以验证正在与API调用。
$ curl \
--cacert ca.cert \
-i HTTPS ://localhost:8200/v1/sys/health
HTTP/1.1 200 OK
...
{"initialized":true,"sealed":false,"standby":false, ...}
下一步是启用Vault PKI Secrets Engine后端vault secrets enable pki,生成CA证书和Vault将用于签署证书的私钥,并创建一个my-role可以为我们的域(localhost)发出请求的角色()。在这里查看所有细节。
vault write pki/roles/my-role \ allowed_domains=localhost \ allow_subdomains=true \ max_ttl=72h
证明
现在我们的证书颁发机构(CA)已准备就绪,我们可以向它发出请求,以便签署我们的证书。您可能会询问哪些证书,以及如果我们还没有它们,如何自动告诉我们的gRPC端点使用它们?输入Certify,Go库,以便 在需要时自动执行证书分发和续订 。它不仅适用于Vault作为CA后端,还适用于Cloudflare CFSSL和AWS ACM。
配置Certify的第一步是issuer在这种情况下指定后端Vault。
issuer := &vault.Issuer{ URL: &url.URL{ Scheme: "https", Host: "localhost:8200", }, TLSConfig: &tls.Config{ RootCAs: cp, }, Token: getenv("TOKEN"), Role: "my-role", }
在此示例中,我们通过提供以下内容来标识Vault实例和访问凭据:
- 我们为Vault(localhost:8200)配置的侦听器地址。
- 初始化库的服务器后,我们得到的TOKEN。
- 我们创建的角色(my-role)。
- 我们在Vault配置中提供的证书颁发者的CA证书。cp是一个x509.CertPool包括ca.cert在这种情况下,如在(*)指出。
您可以选择通过提供证书详细信息CertConfig。在这种情况下,我们这样做是为了指定我们想要使用RSA算法而不是Certify的默认值为我们的证书签名请求(CSR)生成私钥ECDSA P256。
cfg := certify.CertConfig{ SubjectAlternativeNames: []string{"localhost"}, IPSubjectAlternativeNames: [] net .IP{ net.ParseIP("127.0.0.1"), net.ParseIP("::1"), }, KeyGenerator: RSA{bits: 2048}, }
通过我们现在构建的Certify类型验证钩子GetCertificate和GetClientCertificate方法; 先前收集的信息, 防止为每个传入连接请求新证书 的 方法 ,以及登录插件(在该示例中)。tls.ConfigCache go-kit/log
c := &certify.Certify{ CommonName: "localhost", Issuer: issuer, Cache: certify.NewMemCache(), CertConfig: &cfg, RenewBefore: 24 * time.Hour, Logger: kit.New(logger), }
最后一步是创建一个tls.Config指向我们刚刚创建的GetCertificate方法Certify。然后,在我们的gRPC服务器中使用此配置。
// Client
// ... as in ...
// Server
tlsConfig := &tls.Config{
GetCertificate: c.GetCertificate,
}
s := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
// ... register gRPC services ...
if err = s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
您可以通过make run-server-vault在make run-client-ca将环境变量CAFILE指向Vault的证书文件(ca-vault.cert)后在一个选项卡和另一个选项卡中运行来重现此操作,您可以按如下方式获取该文件:
$ curl \ --cacert ca.cert \ [ \ -o ca-vault.cert
服务器:
$ make run-server-vault ... level=debug time=2019-07-15T19:37:12.694833Z caller=logger.go:36 server_name=localhost remote_addr=[::1]:64103 msg="Getting server certificate" level=debug time=2019-07-15T19:37:12.694936Z caller=logger.go:36 msg="Requesting new certificate from issuer" level=debug time=2019-07-15T19:37:12.815081Z caller=logger.go:36 serial=451331845556263599050597627925015657462097174315 expiry=2019-07-18T19:37:12Z msg="New certificate issued" level=debug time=2019-07-15T19:37:12.815115Z caller=logger.go:36 serial=451331845556263599050597627925015657462097174315 took=120.284897ms msg="Certificate found"
客户:
$ export CAFILE="ca-vault.cert" $ make run-client-ca ... User found: Nicolas
检查我们生成并自动签名的证书,将揭示我们刚刚配置的一些细节。
$ openssl x509 -in grpc-cert.pem -text -noout Certificate: Data: ... Validity Not Before: Jul 15 19:36:42 2019 GMT Not After : Jul 18 19:37:12 2019 GMT Subject: CN=localhost Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 00:bf:3c:a3:d8:8c:d8:3c:d0:bd:0c:e0:4c:9d:4d: ... X509v3 extensions: ... Authority Information Access: CA Issuers - URI: X509v3 Subject Alternative Name: DNS:localhost, DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
公共域:让我们自动完成加密
我们加密吧
我们可以使用Let’s Encrypt for gRPC吗?嗯,它确实对我有用。问题可能在于公开面对gRPC API是否是个好主意。Google Cloud似乎正在这样做,请参阅Google API。但是,这不是一种非常普遍的做法。无论如何,我在这里是如何使用我们自动从Let的加密获得的证书公开公共gRPC API。
重要的是要强调这个例子并不意味着要复制内部/私人服务。在与Let’s Encrypt的Jacob Hoffman-Andrews交谈时,他提到:
让加密使用ACME协议来验证证书申请人是否合法地代表证书中的域名。它还为其他证书管理功能提供了便利,例如证书撤销。ACME描述了用于自动化发布和域验证过程的可扩展框架,从而允许服务器和基础设施软件在没有用户交互的情况下获得证书 。[ RFC 8555 ]
简而言之,我们需要做的就是利用Let的加密来运行ACME客户端。我们将在此示例中使用autocert。
autocert
autocert软件包 可以自动访问Let’s Encrypt和任何其他基于ACME的CA的证书 。但是,请记住, 此包正在进行中,并且不会产生API稳定性承诺 。[ 文件 ]
在规范的要求而言,第一步是声明一个Manager与Prompt该 指示帐户注册过程中接受CA的服务条款的 ,一个Cache方法 来存储和检索先前获得的证书 (在这种情况下,本地文件系统的目录),一个HostPolicy使用我们可以响应的域列表,以及可选地和Email 地址来通知已颁发证书的问题 。
manager := autocert.Manager{ Prompt: autocert.AcceptTOS, Cache: autocert.DirCache("golang-autocert"), HostPolicy: autocert.HostWhitelist(host), Email: "test@example.com", }
这Manager将自动为我们创建一个TLS配置,负责与Let的加密交互。另一方面,客户端只需要一个指向空tlsconfig(&tls.Config{})的指针,默认情况下,它会加载系统CA证书,因此信任我们的CA(Let’s Encrypt)。
// Client config := &tls.Config{} conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(config))) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() // Server creds := credentials.NewTLS(manager.TLSConfig()) s := grpc.NewServer(grpc.Creds(creds)) // ... register gRPC services ... // Listener...
如果你正在密切关注,你可能已经注意到我们在这个例子中没有包含监听器部分。原因是基于ACME TLS的挑战TLS-ALPN-01如何工作。 具有应用程序级协议协商(TLS ALPN)验证方法的TLS通过要求客户端配置TLS服务器以响应利用具有标识信息的ALPN扩展的特定连接尝试来证明对域名的控制 。[ draft-ietf-acme-tls-alpn-05 ]。
作为旁注,autocert 在Let’s Encrypt宣布所有TLS-SNI-01验证支持的生命周期终止后添加了对TLS-ALPN-01的支持。
换句话说,我们需要监听HTTPS请求。好消息是autocert一应俱全,并可以创建这个特殊的监听用manager.Listener()。现在,问题是HTTPS和gRPC是否应该在同一个端口上监听?长话短说,我无法使其与独立端口一起工作,但如果两个服务都在443上听,它可以完美地工作。
gRPC和HTTPS在同一个端口上……说什么!?我知道,只因为你不能意味着你应该这样做。但是,Go gRPC库提供的ServeHTTP方法可以帮助我们将传入的请求路由到相应的服务。 请注意, *ServeHTTP* 使用Go的 *HTTP/2* 服务器实现,它与grpc-go的 *HTTP/2* 服务器完全分开。性能和功能可能因两条路径而异 。[ go-grpc ]。您可以在gRPC serveHTTP性能损失中看到一些基准。话虽如此,路由将如下所示:
func grpcHandlerFunc(g *grpc.Server, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ct := r.Header.Get("Content-Type") if r.ProtoMajor == 2 && strings.Contains(ct, "application/grpc") { g.ServeHTTP(w, r) } else { h.ServeHTTP(w, r) } }) }
所以我们可以按如下方式收听请求,注意我们提供了grpcHandlerFunc刚创建的处理程序http.Serve:
// Listener lis = manager.Listener() if err = http.Serve(lis, grpcHandlerFunc(s, httpsHandler())); err != nil { log.Fatalf("failed to serve: %v", err)) }
您可以通过make run-server-public在一个选项卡和make run-client-default另一个选项卡中运行来重现此问题。为此,您需要拥有一个域(HOST)。在我的情况下我用过:
export HOST=grpc.nleiva.com export PORT=443 make run-server-public
现在,我可以通过互联网从世界上任何地方发出gRPC请求:
$ export HOST=grpc.nleiva.com $ export PORT=443 $ make run-client-default User found: Nicolas
最后,我们可以查看通过发出HTTPS请求生成的证书。
结论
如果您利用本文中讨论的一些资源,管理和分发gRPC端点的证书应该不会有麻烦。
到目前为止,虽然连接已加密且客户端已验证服务器的完整性,但服务器尚未对客户端进行身份验证。这可能是某些微服务场景所必需的,我们将在本博客系列的下一部分中讨论相互TLS。敬请关注!
翻译自: #/notebooks/35830493/notes/51571844/preview