本文记录了gRPC 中如何通过 TLS 证书建立安全连接,让数据能够加密处理,包括证书制作和CA签名校验等。
原文作者:意琦行
原文链接:gRPC(Go)教程(四)—通过SSL/TLS建立安全连接 | 指月小筑|意琦行的个人博客
1. 概述
gRPC 内置了以下 encryption 机制:
- SSL / TLS:通过证书进行数据加密;
- ALTS:Google开发的一种双向身份验证和传输加密系统。
- 只有运行在 Google Cloud Platform 才可用,一般不用考虑。
gRPC 中的连接类型一共有以下 3 种:
- insecure connection:不使用 TLS 加密;
- server-side TLS:仅服务端 TLS 加密;
- mutual TLS:客户端、服务端都使用 TLS 加密。
我们之前的实例中都是使用 insecure connection:
1
| conn, err := grpc.Dial(":8972", grpc.WithInsecure())
|
通过指定 WithInsecure option 来建立 insecure connection,不建议在生产环境使用。
本章将记录如何使用 server-side TLS
和mutual TLS
来建立安全连接。
2. server-side TLS
1. 流程
服务端 TLS 具体包含以下几个步骤:
- 制作证书,包含服务端证书和 CA 证书;
- 服务端启动时加载证书;
- 客户端连接时使用CA 证书校验服务端证书有效性。
也可以不使用 CA证书,即服务端证书自签名。
2. 制作证书
CA 证书
1
2
3
4
5
6
7
8
| # 生成.key 私钥文件
$ openssl genrsa -out ca.key 2048
# 生成.csr 证书签名请求文件
$ openssl req -new -key ca.key -out ca.csr -subj "/C=GB/L=China/O=lixd/CN=www.lvmo.work"
# 自签名生成.crt 证书文件
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/C=GB/L=China/O=lixd/CN=www.lvmo.work
|
在生成.csr
证书时提示如下报错:
1
2
| Can't load /root/.rnd into RNG
281473565963904:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/root/.rnd
|
解决办法
1
2
| cd /root
openssl rand -writerand .rnd
|
服务端证书
和生成 CA证书类似,不过最后一步由 CA 证书进行签名,而不是自签名。
然后openssl 配置文件可能位置不同,需要自己修改一下。
1
| $ find / -name "openssl.cnf"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 生成.key 私钥文件
$ openssl genrsa -out server.key 2048
# 生成.csr 证书签名请求文件
$ openssl req -new -key server.key -out server.csr \
-subj "/C=GB/L=China/O=lixd/CN=www.lvmo.work" \
-reqexts SAN \
-config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lvmo.work"))
# 签名生成.crt 证书文件
$ openssl x509 -req -days 3650 \
-in server.csr -out server.crt \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lvmo.work"))
|
到此会生成以下 7 个文件:
-rw-r--r-- 1 root root 1245 Apr 20 16:51 ca.crt
-rw-r--r-- 1 root root 956 Apr 20 16:49 ca.csr
-rw------- 1 root root 1679 Apr 20 16:47 ca.key
-rw-r--r-- 1 root root 41 Apr 20 16:52 ca.srl
-rw-r--r-- 1 root root 1168 Apr 20 16:52 server.crt
-rw-r--r-- 1 root root 1013 Apr 20 16:51 server.csr
-rw------- 1 root root 1679 Apr 20 16:51 server.key
会用到的有下面这3个:
- ca.crt
- server.key
- server.crt
3. 服务端
服务端代码修改点如下:
- NewServerTLSFromFile 加载证书
- NewServer 时指定 Creds。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| func main() {
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 指定使用服务端证书创建一个 TLS credentials。
creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server.crt"), data.Path("x509/server.key"))
if err != nil {
log.Fatalf("failed to create credentials: %v", err)
}
// 指定使用 TLS credentials。
s := grpc.NewServer(grpc.Creds(creds))
ecpb.RegisterEchoServer(s, &ecServer{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
|
4. 客户端
客户端代码主要修改点:
- NewClientTLSFromFile 指定使用 CA 证书来校验服务端的证书有效性。
- 注意:第二个参数域名就是前面生成服务端证书时指定的CN参数。
- 建立连接时 指定建立安全连接 WithTransportCredentials。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func main() {
flag.Parse()
// 客户端通过ca证书来验证服务的提供的证书
creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca.crt"), "www.lvmo.work")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// 建立连接时指定使用 TLS
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
rgc := ecpb.NewEchoClient(conn)
callUnaryEcho(rgc, "hello world")
}
|
5. Test
Server
1
2
| grpc-study/features/encryption/server-side-TLS/server$ go run main.go
2022/04/20 17:10:32 Server gRPC on 0.0.0.0:50051
|
Client
1
2
| grpc-study/features/encryption/server-side-TLS/client$ go run main.go
UnaryEcho: hello world
|
可以看到成功开启了 TLS。
3. mutual TLS
server-side TLS 中虽然服务端使用了证书,但是客户端却没有使用证书,本章节会给客户端也生成一个证书,并完成 mutual TLS。
1. 制作证书
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 生成.key 私钥文件
openssl genrsa -out client.key 2048
# 生成.csr 证书签名请求文件
openssl req -new -key client.key -out client.csr \
-subj "/C=GB/L=China/O=lixd/CN=www.lvmo.work" \
-reqexts SAN \
-config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lvmo.work"))
# 签名生成.crt 证书文件
openssl x509 -req -days 3650 \
-in client.csr -out client.crt \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-extensions SAN \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lvmo.work"))
|
这里又会生成3个文件,需要的是下面这两个:
到此为止,我们已经有了如下5个需要使用的文件:
- ca.crt
- client.crt
- client.key
- server.crt
- server.key
2. 服务端
mutual TLS 中服务端、客户端改动都比较多。
具体步骤如下:
- 加载服务端证书
- 构建用于校验客户端证书的 CertPool
- 使用上面的参数构建一个 TransportCredentials
- newServer 是指定使用前面创建的 creds。
具体改动如下:
看似改动很大,其实如果你仔细查看了前面 NewServerTLSFromFile 方法做的事的话,就会发现是差不多的,只有极个别参数不同。
修改点如下:
- tls.Config的参数ClientAuth,这里改成了tls.RequireAndVerifyClientCert,即服务端也必须校验客户端的证书,之前使用的默认值(即不校验)
- tls.Config的参数ClientCAs,由于之前都不校验客户端证书,所以也没有指定用什么证书来校验,这里改成了之前创建的CertPool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| func main() {
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
certificate, err := tls.LoadX509KeyPair(data.Path("x509/server.crt"), data.Path("x509/server.key"))
if err != nil {
log.Fatal(err)
}
// 创建CertPool,后续就用池里的证书来校验客户端证书有效性
// 所以如果有多个客户端 可以给每个客户端使用不同的 CA 证书,来实现分别校验的目的
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(data.Path("x509/ca.crt"))
if err != nil {
log.Fatal(err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append certs")
}
// 构建基于 TLS 的 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{certificate},
// 要求必须校验客户端的证书 可以根据实际情况选用其他参数
ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
ClientCAs: certPool,
})
s := grpc.NewServer(grpc.Creds(creds))
ecpb.RegisterEchoServer(s, &ecServer{})
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Println("Serving gRPC on 0.0.0.0" + fmt.Sprintf(":%d", *port))
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
|
3. 客户端
客户端改动和前面服务端差不多,具体步骤都一样,除了不能指定校验策略之外基本一样。
大概是因为客户端必校验服务端证书,所以没有提供可选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| func main() {
// 加载客户端证书
certificate, err := tls.LoadX509KeyPair(data.Path("x509/client.crt"), data.Path("x509/client.key"))
if err != nil {
log.Fatal(err)
}
// 构建CertPool以校验服务端证书有效性
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(data.Path("x509/ca.crt"))
if err != nil {
log.Fatal(err)
}
if ok := certPool.AppendCertsFromPEM(ca); !ok {
log.Fatal("failed to append ca certs")
}
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "www.lixueduan.com", // NOTE: this is required!
RootCAs: certPool,
})
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("DialContext error:%v", err)
}
defer conn.Close()
client := ecpb.NewEchoClient(conn)
callUnaryEcho(client, "hello world")
}
|
4. Test
Server
1
2
| grpc_study/features/encryption/mutual-TLS/server$ go run main.go
2022/04/20 17:38:46 Server gRPC on 0.0.0.0:50051
|
Client
1
2
| grpc_study/features/encryption/mutual-TLS/client$ go run main.go
UnaryEcho: hello world
|
一切正常,大功告成。
4. FAQ
Go 1.15 版本开始废弃 CommonName 并且推荐使用 SAN 证书,导致依赖 CommonName 的证书都无法使用了。
解决方案
- 开启兼容:设置环境变量 GODEBUG 为
x509ignoreCN=0
- 使用 SAN 证书
教程使用 SAN 证书,所以不会遇到该问题。
5. 小结
本章主要讲解了 gRPC 中三种类型的连接,及其具体配置方式。
- insecure connection
- server-side TLS
- mutual TLS
6. 参考
https://grpc.io/docs/guides/auth
https://dev.to/techschoolguru/how-to-secure-grpc-connection-with-ssl-tls-in-go-4ph
https://www.openssl.org/docs/manmaster/
https://www.jianshu.com/p/37ded4da1095
https://www.cnblogs.com/rickiyang/p/14981374.html