在上一篇文章中,我们检查了不同的(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.Config
Cache
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 http://bit.ly/go-grpc-tls-ca ...
// 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 \
[https://localhost:8200/v1/pki/ca/pem](https://localhost:8200/v1/pki/ca/pem?source=post_page---------------------------) \
-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:https://localhost:8200/v1/pki/ca
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交谈时,他提到:
一般情况下,我建议人们不要将let的加密证书用于gRPC或其他内部RPC服务。在我看来,使用minica生成单一用途的内部CA并使用它生成服务器和客户端证书既简单又安全。这样你就不必将你的RPC服务器打开到外部互联网,而且你将信任范围限制在内部RPC所需的范围内,而且你可以拥有更长的证书生命周期,而且你可以获得撤销作品。
让加密使用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的加密交互。另一方面,客户端只需要一个指向空tls
config(&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。敬请关注!
转: https://www.jianshu.com/writer#/notebooks/35830493/notes/51571844/preview