本文记录了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 具体包含以下几个步骤:

  1. 制作证书,包含服务端证书和 CA 证书;
  2. 服务端启动时加载证书;
  3. 客户端连接时使用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. 服务端

服务端代码修改点如下:

  1. NewServerTLSFromFile 加载证书
  2. 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. 客户端

客户端代码主要修改点:

  1. NewClientTLSFromFile 指定使用 CA 证书来校验服务端的证书有效性。
    • 注意:第二个参数域名就是前面生成服务端证书时指定的CN参数
  2. 建立连接时 指定建立安全连接 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个文件,需要的是下面这两个:

  • client.crt
  • client.key

到此为止,我们已经有了如下5个需要使用的文件:

  • ca.crt
  • client.crt
  • client.key
  • server.crt
  • server.key

2. 服务端

mutual TLS 中服务端、客户端改动都比较多。

具体步骤如下:

  1. 加载服务端证书
  2. 构建用于校验客户端证书的 CertPool
  3. 使用上面的参数构建一个 TransportCredentials
  4. newServer 是指定使用前面创建的 creds。

具体改动如下:

看似改动很大,其实如果你仔细查看了前面 NewServerTLSFromFile 方法做的事的话,就会发现是差不多的,只有极个别参数不同。

修改点如下:

  1. tls.Config的参数ClientAuth,这里改成了tls.RequireAndVerifyClientCert,即服务端也必须校验客户端的证书,之前使用的默认值(即不校验)
  2. 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