草庐IT

gRPC(五)进阶:通过TLS建立安全连接

lin钟一 2023-06-08 原文

目录

前言

个人网站:https://linzyblog.netlify.app/
示例代码已经上传到github:点击跳转
gRPC官方文档:点击跳转

一、明文传输

先前的例子中 gRPC Client/Server 都是明文传输的,在明文通讯的情况下,你的请求就是裸奔的,有可能被第三方恶意篡改或者伪造为“非法”的数据。

我们抓个包查看一下:


是明文传输,后面我们开始gRPC通过 TLS 证书建立安全连接,让数据能够加密处理,包括证书制作和CA签名校验等。

二、TLS概述

传输层安全 (TLS) 对通过 Internet 发送的数据进行加密,以确保窃听者和黑客无法看到您传输的内容,这对于密码、信用卡号和个人通信等私人和敏感信息特别有用。

1、什么是TLS?

传输层安全 (TLS) 是一种 Internet 工程任务组 ( IETF ) 标准协议,可在两个通信计算机应用程序之间提供身份验证、隐私和数据完整性。它是当今使用最广泛部署的安全协议,最适合需要通过网络安全交换数据的 Web 浏览器和其他应用程序。这包括 Web 浏览会话、文件传输、虚拟专用网络 (VPN) 连接、远程桌面会话和 IP 语音 (VoIP)。最近,TLS 被集成到包括 5G 在内的现代蜂窝传输技术中,以保护整个无线电接入网络 ( RAN ) 的核心网络功能。

2、TLS的工作流程

TLS 使用客户端-服务器握手机制来建立加密和安全的连接,并确保通信的真实性。

  • 通信设备交换加密功能。
  • 使用数字证书进行身份验证过程以帮助证明服务器是它声称的实体。
  • 发生会话密钥交换。在此过程中,客户端和服务器必须就密钥达成一致,以建立安全会话确实在客户端和服务器之间的事实——而不是在中间试图劫持会话的东西。

三、gRPC建立安全连接

1、概述

gRPC建立在HTTP/2协议之上,对TLS提供了很好的支持。当不需要证书认证时,可通过grpc.WithInsecure()选项跳过了对服务器证书的验证,没有启用证书的gRPC服务和客户端进行的是明文通信,信息面临被任何第三方监听的风险。为了保证gRPC通信不被第三方监听、篡改或伪造,可以对服务器启动TLS加密特性。

gRPC 内置了以下 encryption 机制:

  • SSL / TLS:通过证书进行数据加密;
  • ALTS:Google开发的一种双向身份验证和传输加密系统。
    • 只有运行在 Google Cloud Platform 才可用,一般不用考虑。

2、gRPC 加密类型

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

我们前面的例子都是明文传输的,使用的都是 insecure connection,通过指定 WithInsecure option 来建立 insecure connection,不建议在生产环境使用。

后面我们了解如何使用 TLS 来建立安全连接。

3、server-side TLS

1)流程

服务端 TLS 具体包含以下几个步骤:

  • 制作证书,包含服务端证书和 CA 证书;
  • 服务端启动时加载证书;
  • 客户端连接时使用CA 证书校验服务端证书有效性。

也可以不使用 CA证书,即服务端证书自签名。

2)什么是CA?CA证书又是什么?

  • CA是Certificate Authority的缩写,也叫“证书授权中心”。它是负责管理和签发证书的第三方机构,作用是检查证书持有者身份的合法性,并签发证书,以防证书被伪造或篡改。

CA实际上是一个机构,负责“证件”印制核发。就像负责颁发身份证的公安局、负责发放行驶证、驾驶证的车管所。

  • CA 证书就是CA颁发的证书。我们常听到的数字证书就是CA证书,CA证书包含信息有:证书拥有者的身份信息,CA机构的签名,公钥和私钥。

    • 身份信息: 用于证明证书持有者的身份
    • CA机构的签名: 用于保证身份的真实性
    • 公钥和私钥: 用于通信过程中加解密,从而保证通讯信息的安全性

3)什么是SAN?

SAN(Subject Alternative Name)是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

我们在用go 1.15版本以上,用gRPC通过TLS建立安全连接时,会出现证书报错问题:

panic: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate
is not valid for any names, but wanted to match localhost"

造成这个panic的原因是从go 1.15 版本开始废弃 CommonName,我们没有使用官方推荐的 SAN 证书(默认是没有开启SAN扩展)而出现的错误,导致客户端和服务端无法建立连接。

4)目录结构

go-grpc-example
├── client
│  	└──TLS_client
│   │   └──client.go
├── conf
│   └──ca.conf
│   └──server.conf
├── proto
│   └──search
│   │   └──search.proto
├── server
│  	└──TLS_server
│   │   └──server.go
├── Makefile

5)生成CA根证书

在ca.conf里写入内容如下:

[ req ]
default_bits       = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = GB
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = linzyblog.netlify.app
commonName_max              = 64
commonName_default          = linzyblog.netlify.app
  1. 生成ca私钥,得到ca.key
openssl genrsa -out ca.key 4096

openssl genrsa:生成RSA私钥,命令的最后一个参数,将指定生成密钥的位数,如果没有指定,默认512

  1. 生成ca证书签发请求,得到ca.csr
$ openssl req -new -sha256 -out ca.csr -key ca.key -config ca.conf
GB [CN]:
State or Province Name (full name) [ZheJiang]:
Locality Name (eg, city) [HuZhou]:
Organization Name (eg, company) [Step]:
linzyblog.netlify.app [linzyblog.netlify.app]:

这里一直回车就好了
openssl req:生成自签名证书,-new指生成证书请求、-sha256指使用sha256加密、-key指定私钥文件、-x509指输出证书、-days 3650为有效期,此后则输入证书拥有者信息

  1. 生成ca根证书,得到ca.crt
openssl x509 -req -days 3650 -in ca.csr -signkey ca.key -out ca.crt

6)生成终端用户证书

在server.conf写入以下内容:

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = go-grpc-example(这里很重要,客户端需要此字段做匹配)
IP      = 127.0.0.1
  1. 生成私钥,得到server.key
openssl genrsa -out server.key 4096
  1. 生成证书签发请求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

这里也一直回车就好。

  1. 用CA证书生成终端用户证书,得到server.crt
openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.pem -extensions req_ext -extfile server.conf

7)server

const PORT = "8888"

func main() {
	// 根据服务端输入的证书文件和密钥构造 TLS 凭证
	c, err := credentials.NewServerTLSFromFile("./conf/server.pem", "./conf/server.key")
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}
	// 返回一个 ServerOption,用于设置服务器连接的凭据。
	// 用于 grpc.NewServer(opt ...ServerOption) 为 gRPC Server 设置连接选项
	lis, err := net.Listen("tcp", ":"+PORT) //创建 Listen,监听 TCP 端口
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}
	search.RegisterSearchServiceServer(s, &service{})

	s.Serve(lis)
}

8)client

const PORT = "8888"

func main() {
	// 根据客户端输入的证书文件和密钥构造 TLS 凭证。
	// 第二个参数 serverNameOverride 为服务名称。
	c, err := credentials.NewClientTLSFromFile("./conf/server.pem", "go-grpc-example")
	if err != nil {
		log.Fatalf("credentials.NewClientTLSFromFile err: %v", err)
	}
	// 返回一个配置连接的 DialOption 选项。
	// 用于 grpc.Dial(target string, opts ...DialOption) 设置连接选项
	conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
	if err != nil {
		log.Fatalf("grpc.Dial err: %v", err)
	}
	defer conn.Close()
	client := pb.NewSearchServiceClient(conn)
	resp, err := client.Search(context.Background(), &pb.SearchRequest{
		Request: "gRPC",
	})
	if err != nil {
		log.Fatalf("client.Search err: %v", err)
	}

	log.Printf("resp: %s", resp.GetResponse())
}

8)启动 & 请求

# 启动服务端
$ go run server.go
API server listening at: 127.0.0.1:53981

# 启动客户端
$ go run client.go 
API server listening at: 127.0.0.1:54328
2022/11/03 19:35:10 resp: gRPC Server

抓个包再看看

4、mutual TLS

1)生成服务端证书

新增server.conf

[ req ]
default_bits       = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = ZheJiang
localityName                = Locality Name (eg, city)
localityName_default        = HuZhou
organizationName            = Organization Name (eg, company)
organizationName_default    = Step
commonName                  = CommonName (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = linzyblog.netlify.app

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = go-grpc-example(这里很重要,客户端需要此字段做匹配)
IP      = 127.0.0.1
// 1. 生成私钥,得到server.key
openssl genrsa -out server.key 2048

//2. 生成证书签发请求,得到server.csr
openssl req -new -sha256 -out server.csr -key server.key -config server.conf

//3. 用CA证书生成终端用户证书,得到server.crt
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365 -in server.csr -out se
rver.crt -extensions req_ext -extfile server.conf

2)生成客户端证书

// 1. 生成私钥,得到client.key
openssl genrsa -out client.key 2048

//2. 生成证书签发请求,得到client.csr
openssl req -new -key client.key -out client.csr 


//3. 用CA证书生成客户端证书,得到client.crt
 openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 365  -in client.csr -out client.crt

3)整理目录

conf
├── ca.conf
├── ca.crt
├── ca.csr
├── ca.key
├── client
│   ├── client.csr
│   ├── client.key
│   └── client.pem
├── server
│   ├── server.conf
|   └── server.crt
│   ├── server.csr
|   ├── server.key
└─server_side_TLS

4)server

const PORT = "8888"

func main() {
	// 公钥中读取和解析公钥/私钥对
	cert, err := tls.LoadX509KeyPair("./conf/server/server.crt", "./conf/server/server.key")
	if err != nil {
		fmt.Println("LoadX509KeyPair error", err)
		return
	}
	// 创建一组根证书
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("./conf/ca.crt")
	if err != nil {
		fmt.Println("read ca pem error ", err)
		return
	}
	// 解析证书
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Println("AppendCertsFromPEM error ")
		return
	}

	c := credentials.NewTLS(&tls.Config{
		//设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		//要求必须校验客户端的证书
		ClientAuth: tls.RequireAndVerifyClientCert,
		//设置根证书的集合,校验方式使用ClientAuth设定的模式
		ClientCAs: certPool,
	})
	s := grpc.NewServer(grpc.Creds(c))
	lis, err := net.Listen("tcp", ":"+PORT) //创建 Listen,监听 TCP 端口
	if err != nil {
		log.Fatalf("credentials.NewServerTLSFromFile err: %v", err)
	}
	//将 SearchService(其包含需要被调用的服务端接口)注册到 gRPC Server 的内部注册中心。
	//这样可以在接受到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理
	search.RegisterSearchServiceServer(s, &service{})

	//gRPC Server 开始 lis.Accept,直到 Stop 或 GracefulStop
	s.Serve(lis)
}

5)client

const PORT = "8888"

func main() {
	// 公钥中读取和解析公钥/私钥对
	cert, err := tls.LoadX509KeyPair("./conf/client/client.crt", "./conf/client/client.key")
	if err != nil {
		fmt.Println("LoadX509KeyPair error ", err)
		return
	}
	// 创建一组根证书
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("./conf/ca.crt")
	if err != nil {
		fmt.Println("ReadFile ca.crt error ", err)
		return
	}
	// 解析证书
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		fmt.Println("certPool.AppendCertsFromPEM error ")
		return
	}

	c := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert},
		ServerName:   "go-grpc-example",
		RootCAs:      certPool,
	})

	conn, err := grpc.Dial(":"+PORT, grpc.WithTransportCredentials(c))
	if err != nil {
		log.Fatalf("grpc.Dial err: %v", err)
	}
	defer conn.Close()

	client := pb.NewSearchServiceClient(conn)
	resp, err := client.Search(context.Background(), &pb.SearchRequest{
		Request: "gRPC",
	})
	if err != nil {
		log.Fatalf("client.Search err: %v", err)
	}

	log.Printf("resp: %s", resp.GetResponse())
}

6)启动 & 请求

# 启动服务端
$ go run server.go
API server listening at: 127.0.0.1:56036

# 启动客户端
$ go run client.go 
API server listening at: 127.0.0.1:56364
2022/11/03 20:21:55 resp: gRPC Server

# 更改ServerName为linzy
$ go run client.go 
API server listening at: 127.0.0.1:56424
2022/11/03 20:23:17 client.Search err: rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: cer
tificate is valid for go-grpc-example, not linzy"

抓个包看看

有关gRPC(五)进阶:通过TLS建立安全连接的更多相关文章

  1. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  2. ruby - 通过 erb 模板输出 ruby​​ 数组 - 2

    我正在使用puppet为ruby​​程序提供一组常量。我需要提供一组主机名,我的程序将对其进行迭代。在我之前使用的bash脚本中,我只是将它作为一个puppet变量hosts=>"host1,host2"我将其提供给bash脚本作为HOSTS=显然这对ruby​​不太适用——我需要它的格式hosts=["host1","host2"]自从phosts和putsmy_array.inspect提供输出["host1","host2"]我希望使用其中之一。不幸的是,我终其一生都无法弄清楚如何让它发挥作用。我尝试了以下各项:我发现某处他们指出我需要在函数调用前放置“function_”……这

  3. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  4. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  5. ruby - 通过 ruby​​ 进程共享变量 - 2

    我正在编写一个gem,我必须在其中fork两个启动两个webrick服务器的进程。我想通过基类的类方法启动这个服务器,因为应该只有这两个服务器在运行,而不是多个。在运行时,我想调用这两个服务器上的一些方法来更改变量。我的问题是,我无法通过基类的类方法访问fork的实例变量。此外,我不能在我的基类中使用线程,因为在幕后我正在使用另一个不是线程安全的库。所以我必须将每个服务器派生到它自己的进程。我用类变量试过了,比如@@server。但是当我试图通过基类访问这个变量时,它是nil。我读到在Ruby中不可能在分支之间共享类变量,对吗?那么,还有其他解决办法吗?我考虑过使用单例,但我不确定这是

  6. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  7. ruby-on-rails - Enumerator.new 如何处理已通过的 block ? - 2

    我在理解Enumerator.new方法的工作原理时遇到了一些困难。假设文档中的示例:fib=Enumerator.newdo|y|a=b=1loopdoy[1,1,2,3,5,8,13,21,34,55]循环中断条件在哪里,它如何知道循环应该迭代多少次(因为它没有任何明确的中断条件并且看起来像无限循环)? 最佳答案 Enumerator使用Fibers在内部。您的示例等效于:require'fiber'fiber=Fiber.newdoa=b=1loopdoFiber.yieldaa,b=b,a+bendend10.times.m

  8. ruby - 无法在 60 秒内获得稳定的 Firefox 连接 (127.0.0.1 :7055) - 2

    我使用的是Firefox版本36.0.1和Selenium-Webdrivergem版本2.45.0。我能够创建Firefox实例,但无法使用脚本继续进行进一步的操作无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055)错误。有人能帮帮我吗? 最佳答案 我遇到了同样的问题。降级到firefoxv33后一切正常。您可以找到旧版本here 关于ruby-无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055),我们在StackOverflow上找到一个类

  9. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  10. 通过 MacPorts 的 RubyGems 是个好主意吗? - 2

    从MB升级到新的MBP后,Apple的迁移助手没有移动我的gem。我这次是通过macports安装ruby​​gems,希望在下次升级时避免这种情况。有什么我应该注意的陷阱吗? 最佳答案 如果你想把你的gems安装在你的主目录中(在传输过程中应该复制过来,作为一个附带的好处,会让你以你自己的身份运行geminstall,而不是root),将gemhome:键设置为您在~/.gemrc中的主目录中的路径. 关于通过MacPorts的RubyGems是个好主意吗?,我们在StackOverf

随机推荐