七叶笔记 » golang编程 » 用Go和TLS保护gRPC连接的实用指南 – 第2部分

用Go和TLS保护gRPC连接的实用指南 – 第2部分

在上一篇文章中,我们检查了不同的(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

相关文章