用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 CFSSLAWS 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类型验证钩子GetCertificateGetClientCertificate方法; 先前收集的信息,防止为每个传入连接请求新证书方法,以及登录插件(在该示例中)。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 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-vaultmake 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稳定性承诺。[ 文件 ]

在规范的要求而言,第一步是声明一个ManagerPrompt指示帐户注册过程中接受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。敬请关注!

转: https://www.jianshu.com/writer#/notebooks/35830493/notes/51571844/preview

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容