07.gRPC的TLS认证
前面的章节实现的 gRPC 示例 Client/Server 之间的通信都是明文传输的,数据很容易被监听,甚至篡改。其实 gRPC 是鼓励开发者使用安全的通信的,只是在开始的章节中,客户端在创建连接时,使用 grpc.WithInsecure()
这个 Option 使 gRPC 以 Insecure 的方式连接。
本节主要介绍为 gRPC 添加 TLS 支持的过程,关于 TLS 的内容,请自行搜索相关资料。
本节的代码基于 grpc-testing v0.0.1
生成证书
$ cd grpc-testing-0.0.1
$ mkdir -p keys/{ca,server,client}
# CA
## 生成 ca.key 私钥文件
$ openssl genrsa -out keys/ca/ca.key 4096
## 生成 ca.csr 证书签名请求文件
$ openssl req -new -key keys/ca/ca.key \
-out keys/ca/ca.csr \
-subj "/O=cloudfstrife/CN=www.bitlogs.tech"
## 自签名生成 ca.crt 证书文件
$ openssl req -new -x509 -days 3650 \
-key keys/ca/ca.key \
-out keys/ca/ca.crt \
-subj "/O=cloudfstrife/CN=www.bitlogs.tech"
# server
## 生成 server.key 私钥文件
$ openssl genrsa -out keys/server/server.key 4096
## 生成 SAN 证书签名请求文件 Go 1.15 版本开始废弃 CommonName 并且推荐使用 SAN 证书
$ openssl req -new -reqexts SAN \
-key keys/server/server.key \
-out keys/server/server.csr \
-subj "/O=cloudfstrife/CN=www.bitlogs.tech" \
-config <(cat /usr/lib/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
## 使用 CA 证书对 server.csr 签名
$ openssl x509 -req -days 3650 \
-extensions SAN \
-in keys/server/server.csr \
-out keys/server/server.crt \
-CA keys/ca/ca.crt \
-CAkey keys/ca/ca.key \
-CAcreateserial \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
# client
## 生成 client.key 私钥文件
$ openssl genrsa -out keys/client/client.key 4096
## 生成 SAN 证书签名请求文件
$ openssl req -new -reqexts SAN \
-key keys/client/client.key \
-out keys/client/client.csr \
-subj "/O=cloudfstrife/CN=www.bitlogs.tech" \
-config <(cat /usr/lib/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
## 使用 CA 证书对 client.csr 签名
$ openssl x509 -req -days 3650 \
-extensions SAN \
-in keys/client/client.csr \
-out keys/client/client.crt \
-CA keys/ca/ca.crt \
-CAkey keys/ca/ca.key \
-CAcreateserial \
-extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.bitlogs.tech"))
生成的Key如下:
keys
├── ca
│ ├── ca.crt
│ ├── ca.csr
│ ├── ca.key
│ └── ca.srl
├── client
│ ├── client.crt
│ ├── client.csr
│ └── client.key
└── server
├── server.crt
├── server.csr
└── server.key
仅服务端 TLS
仅服务端的 TLS 实现过程主要由以下三步
- 生成服务端证书
- 服务端启动时加载 server 证书
- 客户端创建连接时使用 CA 证书校验服务端证书
使用到的证书文件有以下几个:
- ca.crt
- server.key
- server.crt
拷贝证书到各目录
$ mkdir -p grpc-testing-{server,client}/cert
$ cp keys/server/server.{key,crt} grpc-testing-server/cert
$ cp keys/ca/ca.crt grpc-testing-client/cert/
修改服务端代码
- 使用
credentials.NewServerTLSFromFile
创建TransportCredentials
NewServer
时,通过grpc.Creds
指定 Creds
func main() {
log.Println("Go")
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建 TransportCredentials
creds, err := credentials.NewServerTLSFromFile("./cert/server.crt", "./cert/server.key")
if err != nil {
log.Fatal("create server TLS", err)
}
// 通过 Option 启动 TLS 支持
s := grpc.NewServer(grpc.Creds(creds))
echo.RegisterEchoServer(s, &EchoServer{})
if err := s.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
修改客户端代码
- 使用
credentials.NewClientTLSFromFile
创建 TransportCredentials - 创建连接时,使用
grpc.WithTransportCredentials
指定建立安全连接
func main() {
// 客户端通过 CA 证书来验证服务的提供的证书
creds, err := credentials.NewClientTLSFromFile("./cert/ca.crt", "*.bitlogs.tech")
if err != nil {
log.Fatalf("failed to load credentials: %v", err)
}
// 建立连接时指定使用 TLS
conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(creds), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := echo.NewEchoClient(conn)
ctx, cancle := context.WithCancel(context.Background())
defer cancle()
r, err := c.Echo(ctx, &echo.EchoRequest{Msg: "hello"})
if err != nil {
log.Fatalf("echo failed :%#v", err)
}
log.Print(r.GetMsg())
}
双向 TLS
上面的示例,为 Server 启用了 TLS 功能,客户端却没有使用证书。下面的示例,为客户端也添加证书认证。
使用到的证书文件有以下几个:
- ca.crt
- server.key
- server.crt
- client.key
- client.crt
拷贝证书
$ cp keys/ca/ca.crt grpc-testing-server/cert/
$ cp keys/client/client.{key,crt} grpc-testing-client/cert
修改服务端代码
- 使用
tls.LoadX509KeyPair
创建Certificate
- 使用
x509.NewCertPool
创建用于验签的CertPool
并把 CA 证书添加在其中 credentials.NewTLS
使用创建TransportCredentials
NewServer
时,通过grpc.Creds
指定 Creds
func main() {
log.Println("Go")
listen, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 创建 Certificate
certificate, err := tls.LoadX509KeyPair("./cert/server.crt", "./cert/server.key")
if err != nil {
log.Fatal("create server TLS", err)
}
// 创建 CertPool 并把 CA 证书添加在其中
p := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
log.Fatal("read ca.crt", err)
}
if !p.AppendCertsFromPEM(ca) {
log.Fatal("append ca cert ")
}
// 创建 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 证书链,可以是多个
Certificates: []tls.Certificate{certificate},
// 要求必须校验客户端的证书
ClientAuth: tls.RequireAndVerifyClientCert,
// 设置根证书的集合
ClientCAs: p,
})
// 通过 Option 启动 TLS 支持
s := grpc.NewServer(grpc.Creds(creds))
echo.RegisterEchoServer(s, &EchoServer{})
if err := s.Serve(listen); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
修改客户端代码
客户端的过程和服务端几乎没什么区别,只在创建 TransportCredentials
时的一些区别。
- 使用
tls.LoadX509KeyPair
创建Certificate
- 使用
x509.NewCertPool
创建用于验签的CertPool
并把 CA 证书添加在其中 credentials.NewTLS
使用创建TransportCredentials
- 创建连接时,使用
grpc.WithTransportCredentials
指定建立安全连接
func main() {
// 创建 Certificate
certificate, err := tls.LoadX509KeyPair("./cert/client.crt", "./cert/client.key")
if err != nil {
log.Fatal("load x509 key pair", err)
}
// 创建 CertPool 并把 CA 证书添加在其中
p := x509.NewCertPool()
ca, err := ioutil.ReadFile("./cert/ca.crt")
if err != nil {
log.Fatal("read ca.crt", err)
}
if !p.AppendCertsFromPEM(ca) {
log.Fatal("append ca cert ")
}
// 创建 TransportCredentials
creds := credentials.NewTLS(&tls.Config{
// 证书链,可以是多个
Certificates: []tls.Certificate{certificate},
// 要求必须校验客户端的证书
ServerName: "*.bitlogs.tech",
// 设置根证书的集合
RootCAs: p,
})
// 建立连接时指定使用 TLS
conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithTransportCredentials(creds), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := echo.NewEchoClient(conn)
ctx, cancle := context.WithCancel(context.Background())
defer cancle()
r, err := c.Echo(ctx, &echo.EchoRequest{Msg: "hello"})
if err != nil {
log.Fatalf("echo failed :%#v", err)
}
log.Print(r.GetMsg())
}