gRPC(Go)入门教程(四)—数据安全之–通过SSL/TLS建立安全连接

作者: adm 分类: go 发布时间: 2024-05-17

1. 概述
gRPC 系列相关代码见 Github

gRPC 内置了以下 encryption 机制:

1)SSL / TLS:通过证书进行数据加密;
2)ALTS:Google开发的一种双向身份验证和传输加密系统。

只有运行在 Google Cloud Platform 才可用,一般不用考虑。
gRPC 中的连接类型一共有以下3种:

1)insecure connection:不使用TLS加密
2)server-side TLS:仅服务端TLS加密
3)mutual TLS:客户端、服务端都使用TLS加密

我们之前案例中使用的都是 insecure connection,

conn, err := grpc.Dial(addr,grpc.WithInsecure())

通过指定 WithInsecure option 来建立 insecure connection,不建议在生产环境使用。

本章将记录如何使用 server-side TLS 和mutual TLS来建立安全连接。

2. server-side TLS
1. 流程
服务端 TLS 具体包含以下几个步骤:

1)制作证书,包含服务端证书和 CA 证书;
2)服务端启动时加载证书;
3)客户端连接时使用CA 证书校验服务端证书有效性。
也可以不使用 CA证书,即服务端证书自签名。

2. 制作证书
1. 使用自签名证书
如果你使用的是自签名证书,客户端需要信任这个自签名证书或其根证书。你可以通过将自签名证书或根证书添加到客户端的信任库中来实现这一点。

生成自签名证书
确保你的自签名证书包含正确的 SAN(Subject Alternative Name),并且你已经按照之前的步骤生成了证书。

# openssl.cnf
[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext
prompt             = no

[ req_distinguished_name ]
C  = US
ST = State
L  = City
O  = Organization
OU = Organizational Unit
CN = grpc.xxx.com

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = grpc.xxx.com
DNS.2 = localhost
IP.1  = 00.11.22.33
IP.2  = 127.0.0.1
# 生成私钥
openssl genrsa -out server.key 2048

# 生成证书签名请求 (CSR)
openssl req -new -key server.key -out server.csr -config openssl.cnf

# 自签名证书
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365 -extfile openssl.cnf -extensions req_ext

配置 gRPC 服务器
确保 gRPC 服务器监听在所有网络接口上,并配置 TLS 证书。

package main

import (
	"crypto/tls"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 加载服务器的证书和私钥
	cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatalf("failed to load key pair: %v", err)
	}

	// 创建 TLS 配置
	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert},
		ClientAuth:   tls.NoClientCert, // 如果需要 mTLS,可以设置为 tls.RequireAndVerifyClientCert
	})

	// 创建监听器,监听所有网络接口
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建新的 gRPC 服务器并注册服务
	s := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterYourServiceServer(s, &server{})

	// 启动服务器
	log.Printf("Server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3. 配置 gRPC 客户端
在客户端,你需要配置 TLS 证书以信任服务器的证书。如果你使用的是自签名证书,客户端需要加载根证书(ca.crt)。

package main

import (
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 加载 CA 证书(即自签名证书或根证书)
	caCert, err := ioutil.ReadFile("server.crt") // 或者是 ca.crt
	if err != nil {
		log.Fatalf("failed to read CA certificate: %v", err)
	}

	// 创建证书池并添加 CA 证书
	certPool := x509.NewCertPool()
	if ok := certPool.AppendCertsFromPEM(caCert); !ok {
		log.Fatal("failed to append CA certificate")
	}

	// 创建 TLS 配置
	creds := credentials.NewTLS(&tls.Config{
		RootCAs: certPool,
                //InsecureSkipVerify: true, // 禁用证书,仅用于测试,不要在生产环境中使用
	})

	// 连接到 gRPC 服务器,使用域名
	conn, err := grpc.Dial("my-service.example.com:50051", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	// 创建客户端并进行 RPC 调用
	client := pb.NewYourServiceClient(conn)
	// 这里调用你的 RPC 方法
}

2. 使用受信任的 CA 签发的证书
如果你有多个客户端或希望避免手动分发自签名证书,建议使用由受信任的证书颁发机构(如 Let’s Encrypt、DigiCert 等)签发的证书。这样,客户端默认会信任这些证书,而不需要额外配置。

生成证书
你可以使用 Let’s Encrypt 等免费的证书颁发机构来获取证书。例如,使用 Certbot 工具:

Sh

sudo certbot certonly --standalone -d my-service.example.com

这将生成证书文件,通常位于 /etc/letsencrypt/live/my-service.example.com/ 目录下。

配置 gRPC 服务器
在服务器端,加载由 CA 签发的证书和私钥:

Go

package main

import (
	"crypto/tls"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 加载服务器的证书和私钥
	cert, err := tls.LoadX509KeyPair("/etc/letsencrypt/live/my-service.example.com/fullchain.pem", "/etc/letsencrypt/live/my-service.example.com/privkey.pem")
	if err != nil {
		log.Fatalf("failed to load key pair: %v", err)
	}

	// 创建 TLS 配置
	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert},
		ClientAuth:   tls.NoClientCert, // 如果需要 mTLS,可以设置为 tls.RequireAndVerifyClientCert
	})

	// 创建监听器,监听所有网络接口
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建新的 gRPC 服务器并注册服务
	s := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterYourServiceServer(s, &server{})

	// 启动服务器
	log.Printf("Server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

配置 gRPC 客户端
由于证书是由受信任的 CA 签发的,客户端默认会信任这些证书,因此你不需要显式加载 CA 证书。

Go

package main

import (
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 创建 TLS 配置,默认信任系统 CA
	creds := credentials.NewTLS(&tls.Config{
		InsecureSkipVerify: false, // 不要跳过证书验证
	})

	// 连接到 gRPC 服务器,使用域名
	conn, err := grpc.Dial("my-service.example.com:50051", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	// 创建客户端并进行 RPC 调用
	client := pb.NewYourServiceClient(conn)
	// 这里调用你的 RPC 方法
}

3. 调试证书问题
如果你仍然遇到问题,可以尝试以下调试步骤:

检查证书链:确保服务器发送了完整的证书链。你可以使用 openssl 命令行工具来检查服务器的证书:
Sh

openssl s_client -connect my-service.example.com:50051 -showcerts

这将显示服务器发送的所有证书。确保其中包含了根证书和中间证书(如果有的话)。
检查 SAN:确保服务器证书的 SAN 中包含了你使用的域名。你可以使用 openssl 查看证书的内容:
Sh

openssl x509 -in server.crt -text -noout

检查 Subject Alternative Name 字段,确保它包含了正确的域名(如 DNS:my-service.example.com)。
检查客户端的信任库:确保客户端的操作系统或应用程序信任你使用的 CA。你可以通过查看操作系统的信任库或应用程序的配置来确认这一点。
禁用证书验证(仅用于测试):如果你只是在开发环境中测试,可以暂时禁用证书验证,但这不适用于生产环境,因为它会暴露安全风险。
Go

creds := credentials.NewTLS(&tls.Config{
    InsecureSkipVerify: true, // 仅用于测试,不要在生产环境中使用
})

5. Test
Server

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/server-side-TLS/server$ go run main.go 
2021/01/24 18:00:25 Server gRPC on 0.0.0.0:50051

Client

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/server-side-TLS/client$ go run main.go 
UnaryEcho:  hello world

抓包结果如下

![grpc-tls-wireshark][grpc-tls-wireshark]

可以看到成功开启了 TLS。

3. mutual TLS
server-side TLS 中虽然服务端使用了证书,但是客户端却没有使用证书,本章节会给客户端也生成一个证书,并完成 mutual TLS。

在 Go gRPC 中实现相互 TLS (mTLS) 可以确保客户端和服务器之间的双向身份验证。mTLS 不仅要求客户端验证服务器的证书,还要求服务器验证客户端的证书。这提供了更强的安全性,适用于需要严格身份验证的场景。

以下是详细的步骤,帮助你在 Go gRPC 中配置 mTLS。

1. 生成证书
首先,你需要为服务器和客户端生成证书。我们将使用 OpenSSL 来生成自签名的 CA 证书、服务器证书和客户端证书。

1.1 生成根 CA 证书
Sh

# 生成 CA 私钥
openssl genrsa -out ca.key 4096

# 生成 CA 证书
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=My CA"

1.2 生成服务器证书
创建一个 server.cnf 文件,用于指定服务器证书的 SAN(Subject Alternative Name):

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext
prompt             = no

[ req_distinguished_name ]
C  = US
ST = State
L  = City
O  = Organization
OU = Organizational Unit
CN = my-service.example.com

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = my-service.example.com
IP.1  = 192.0.2.1

生成服务器私钥和证书签名请求 (CSR):

Sh

# 生成服务器私钥
openssl genrsa -out server.key 2048

# 生成服务器 CSR
openssl req -new -key server.key -out server.csr -config server.cnf

# 使用 CA 签发服务器证书
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -sha256 -extfile server.cnf -extensions req_ext

1.3 生成客户端证书
创建一个 client.cnf 文件,用于指定客户端证书的 SAN:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name
req_extensions     = req_ext
prompt             = no

[ req_distinguished_name ]
C  = US
ST = State
L  = City
O  = Organization
OU = Organizational Unit
CN = client

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = client

生成客户端私钥和证书签名请求 (CSR):

Sh

# 生成客户端私钥
openssl genrsa -out client.key 2048

# 生成客户端 CSR
openssl req -new -key client.key -out client.csr -config client.cnf

# 使用 CA 签发客户端证书
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256 -extfile client.cnf -extensions req_ext

2. 配置 gRPC 服务器
在服务器端,你需要配置 TLS 以启用 mTLS,并加载服务器证书和 CA 证书池。

Go

package main

import (
	"crypto/tls"
	"log"
	"net"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 加载服务器的证书和私钥
	serverCert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatalf("failed to load server key pair: %v", err)
	}

	// 加载 CA 证书池
	caCert, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatalf("failed to read CA certificate: %v", err)
	}
	certPool := x509.NewCertPool()
	if ok := certPool.AppendCertsFromPEM(caCert); !ok {
		log.Fatal("failed to append CA certificate")
	}

	// 创建 TLS 配置,启用 mTLS
	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{serverCert},
		ClientAuth:   tls.RequireAndVerifyClientCert,
		ClientCAs:    certPool,
	})

	// 创建监听器,监听所有网络接口
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 创建新的 gRPC 服务器并注册服务
	s := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterYourServiceServer(s, &server{})

	// 启动服务器
	log.Printf("Server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3. 配置 gRPC 客户端
在客户端,你需要加载客户端证书、私钥和 CA 证书,并配置 TLS 以启用 mTLS。

Go

package main

import (
	"crypto/tls"
	"crypto/x509"
	"io/ioutil"
	"log"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	pb "path/to/your/protobuf" // 替换为你的 protobuf 包路径
)

func main() {
	// 加载客户端的证书和私钥
	clientCert, err := tls.LoadX509KeyPair("client.crt", "client.key")
	if err != nil {
		log.Fatalf("failed to load client key pair: %v", err)
	}

	// 加载 CA 证书池
	caCert, err := ioutil.ReadFile("ca.crt")
	if err != nil {
		log.Fatalf("failed to read CA certificate: %v", err)
	}
	certPool := x509.NewCertPool()
	if ok := certPool.AppendCertsFromPEM(caCert); !ok {
		log.Fatal("failed to append CA certificate")
	}

	// 创建 TLS 配置,启用 mTLS
	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{clientCert},
		RootCAs:      certPool,
	})

	// 连接到 gRPC 服务器,使用域名
	conn, err := grpc.Dial("my-service.example.com:50051", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	// 创建客户端并进行 RPC 调用
	client := pb.NewYourServiceClient(conn)
	// 这里调用你的 RPC 方法
}

一切正常,大功告成。

4. FAQ
问题

error:rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"

由于之前使用的不是 SAN 证书,在Go版本升级到1.15后出现了该问题。

原因

Go 1.15 版本开始废弃 CommonName 并且推荐使用 SAN 证书,导致依赖 CommonName 的证书都无法使用了。

解决方案

1)开启兼容:设置环境变量 GODEBUG 为 x509ignoreCN=0
2)使用 SAN 证书
本教程已经修改成了 SAN 证书,所以不会遇到该问题了。

5. 小结
本章主要讲解了 gRPC 中三种类型的连接,及其具体配置方式。

1)insecure connection
2)server-side TLS
3)mutual TLS

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!