- 序言
- 安全需求
- 安全方案
- 敏感数据加密传输
- 认证
- 鉴权
- 数据完整性和一致性
- 证书的基本原理
- 单向证书
- 双向证书
- gRPC安全机制
- SSL/TLS认证
- GoogleOAuth2.0
- 自定义安全认证策略
序言
网络安全领域在攻和防对抗规模群体已经成熟,但是两端从业者对于安全原理掌握程度参差不齐,中间鸿沟般的差距构成了漏洞研究领域的主战场。笔者“三省吾身”,在工作中会犯错误把一些加密、认证、鉴权的概念和实现方案搞混,尤其是加解密涉及算法和公私钥机制的概念不深入细节。
最近的几个影响颇大的安全漏洞,Apache Shiro 权限绕过漏洞、CVE-2020-14882weblogic 绕过登录、微软ZeroLogon,这些漏洞原理的共同点都是和基本的安全算法、认证鉴权方案缺陷有关。也许未来的漏洞攻防将转移到安全基础领域的对抗,从业人员除了要求推进安全方案的必要性,涉及安全建设的可用性更为重要,所以特此开专栏系列,为大家普及一些安全基本功。
本文主要通过介绍gRPC的双向认证方案,理清证书领域的知识。
安全需求
RPC是一种技术思想,实现有阿里的 Dubbo/SOFA、Google gRPC、Facebook 的 Thrift,实现时的远程通信规范和协议可以用RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。这种服务间通信机制为企业内部各系统、模块之间的微服务和接口之间互相调用,RPC实现需要考虑安全性,RPC 调用安全主要涉及如下三点:
- 个人 / 企业敏感数据加密:例如针对个人的账号、密码、手机号等敏感信息进行加密传输,打印接口日志时需要做数据模糊化处理等,不能明文打印;
- 对调用方的身份认证:调用来源是否合法,是否有访问某个资源的权限,防止越权访问;
- 数据防篡改和完整性:通过对请求参数、消息头和消息体做签名,防止请求消息在传输过程中被非法篡改。
安全方案
常见的安全攻防重视rpc协议的反序列漏洞,但是如果业务方问道如果做以上的安全需求,SDL同学就傻眼了,正确的做法是区分加密传输、认证、鉴权、数据完整性和一致性四个方向:
敏感数据加密传输
基于SSL/TLS的通道加密
当存在跨网络边界的 RPC 调用时,往往需要通过 TLS/SSL 对传输通道进行加密,以防止请求和响应消息中的敏感数据泄漏。跨网络边界调用场景主要有三种:
- 后端微服务直接开放给端侧,例如手机 App、TV、多屏等,没有统一的 API 网关/SLB 做安全接入和认证;
- 后端微服务直接开放给 DMZ 部署的管理或者运维类 Portal;
- 后端微服务直接开放给第三方合作伙伴 / 渠道。
除了跨网络之外,对于一些安全等级要求比较高的业务场景,即便是内网通信,只要跨主机 /VM/ 容器通信,都强制要求对传输通道进行加密。在该场景下,即便只存在内网各模块的 RPC 调用,仍然需要做 SSL/TLS。
使用 SSL/TLS 的典型场景如下所示:
通道加密的的实现技术难度稍大,对性能有损耗,定制化程度高,但是效果显著,建设收益明显
针对敏感数据的单独加密
有些 RPC 调用并不涉及敏感数据的传输,或者敏感字段占比较低,为了最大程度的提升吞吐量,降低调用时延,通常会采用 HTTP/TCP 敏感字段单独加密的方式,既保障了敏感信息的传输安全,同时也降低了采用 SSL/TLS 加密通道带来的性能损耗,对于 JDK 原生的 SSL 类库,这种性能提升尤其明显。
它的工作原理如下所示:
敏感数据加密
通常使用 Handler 拦截机制,对请求和响应消息进行统一拦截,根据注解或者加解密标识对敏感字段进行加解密,这样可以避免侵入业务。
采用该方案的缺点主要有两个:
- 对敏感信息的识别可能存在偏差,容易遗漏或者过度保护,需要解读数据和隐私保护方面的法律法规,而且不同国家对敏感数据的定义也不同,这会为识别带来很多困难;
- 接口升级时容易遗漏,例如开发新增字段,忘记识别是否为敏感数据。
认证
内部 RPC 调用的身份认证场景,主要有如下两大类:
- 防止对方知道服务提供者的地址之后,绕过注册中心 / 服务路由策略直接访问 RPC 服务提供端;
- RPC 服务只想供内部模块调用,不想开放给其它业务系统使用(双方网络是互通的)。
身份认证的方式较多,例如 HTTP Basic Authentication、OAuth2 等,比较简单使用的是令牌认证(Token)机制,它的工作原理如下所示:
工作原理如下:
- RPC 客户端和服务端通过 HTTPS 与注册中心连接,做双向认证,以保证客户端和服务端与注册中心之间的安全;
- 服务端生成 Token 并注册到注册中心,由注册中心下发给订阅者。通过订阅 / 发布机制,向 RPC 客户端做 Token 授权;
- 服务端开启身份认证,对 RPC 调用进行 Token 校验,认证通过之后才允许调用后端服务接口。
鉴权
身份认证可以防止非法调用,如果需要对调用方进行更细粒度的权限管控,则需要做对 RPC 调用做鉴权。例如管理员可以查看、修改和删除某个后台资源,而普通用户只能查看资源,不能对资源做管理操作。
在 RPC 调用领域比较流行的是基于 OAuth2.0 的权限认证机制,它的工作原理如下:
OAuth2.0 的认证流程如下:
- 客户端向资源拥有者申请授权(例如携带用户名 密码等证明身份信息的凭证);
- 资源拥有者对客户端身份进行校验,通过之后同意授权;
- 客户端使用步骤 2 的授权凭证,向认证服务器申请资源访问令牌(access token);
- 认证服务器对授权凭证进行合法性校验,通过之后,颁发 access token;
- 客户端携带 access token(通常在 HTTP Header 中)访问后端资源,例如发起 RPC 调用;
- 服务端对 access token 合法性进行校验(是否合法、是否过期等),同时对 token 进行解析,获取客户端的身份信息以及对应的资源访问权限列表,实现对资源访问权限的细粒度管控;
- access token 校验通过,返回资源信息给客户端。
步骤 2 的用户授权,有四种方式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
需要指出的是,OAuth 2.0 是一个规范,不同厂商即便遵循该规范,实现也可能会存在细微的差异。大部分厂商在采用 OAuth 2.0 的基础之上,往往会衍生出自己特有的 OAuth 2.0 实现。
对于 access token,为了提升性能,RPC 服务端往往会缓存,不需要每次调用都与 AS 服务器做交互。同时,access token 是有过期时间的,根据业务的差异,过期时间也会不同。客户端在 token 过期之前,需要刷新 Token,或者申请一个新的 Token。
考虑到 access token 的安全,通常选择 SSL/TLS 加密传输,或者对 access token 单独做加密,防止 access token 泄漏。
关于oauth作为安全基本功系列今后还会有专栏。
数据完整性和一致性
RPC 调用,除了数据的机密性和有效性之外,还有数据的完整性和一致性需要保证,即如何保证接收方收到的数据与发送方发出的数据是完全相同的。
利用消息摘要可以保障数据的完整性和一致性,它的特点如下:
- 单向 Hash 算法,从明文到密文的不可逆过程,即只能加密而不能解密;
- 无论消息大小,经过消息摘要算法加密之后得到的密文长度都是固定的;
- 输入相同,则输出一定相同。
目前常用的消息摘要算法是 SHA-1、MD5 和 hmac,MD5 可产生一个 128 位的散列值。SHA-1 则是以 MD5 为原型设计的安全散列算法,可产生一个 160 位的散列值,安全性更高一些。hmac 除了能够保证消息的完整性,还能够保证来源的真实性。
由于 MD5 已被发现有许多漏洞,在实际应用中更多使用 SHA 和 hmac,而且往往会把数字签名和消息摘要混合起来使用。微信支付、阿里云调用是大家常用的签名机制,注意消息摘要不是加密,不是加密,不是加密。
证书的基本原理
目前使用最广的 SSL/TLS 工具 / 类库就是 OpenSSL,它是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法、常用的密钥和证书封装管理功能以及 SSL 协议。注意SSL和TLS有不同的历史和标准,HTTPS的意思是HTTP SSL/ TLS,现在的安全方案一般是tls实现,SSL标准正被淘汰。只是因为沿袭历史称呼,所以经常混用两次名词, SSL被发现存在过 POODLE, DROWN协议算法本身的漏洞,注意区分大名鼎鼎的心脏滴血漏洞Heartbleed是OpenSSL的实现TLS和DTLS的心跳处理逻辑时有bug,而不是利用SSL/TLS协议本身的缺陷。
单向证书
https是大家最熟悉的单项证书方案,由浏览器、ca中心、服务端三方实现。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。单向认证流程中,服务器端保存着公钥证书和私钥两个文件,整个握手过程如下:
单向认证流程
- 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务器端;
- 服务器端将本机的公钥证书(server.crt)发送给客户端;
- 客户端读取公钥证书(server.crt),取出了服务端公钥;
- 客户端生成一个随机数(密钥R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端;
- 服务端用自己的私钥(server.key)去解密这个密文,得到了密钥R
- 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
双向证书
双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。
双向认证流程
- 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
- 服务器端将本机的公钥证书(server.crt)发送给客户端;
- 客户端读取公钥证书(server.crt),取出了服务端公钥;
- 客户端将客户端公钥证书(client.crt)发送给服务器端;
- 服务器端解密客户端公钥证书,拿到客户端公钥;
- 客户端发送自己支持的加密方案给服务器端;
- 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
- 客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
- 服务端用自己的私钥去解密这个密文,得到了密钥R
- 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
整个双向认证的流程跑通,最终需要五个证书文件:
- 服务器端公钥证书:server.crt
- 服务器端私钥文件:server.key
- 客户端公钥证书:client.crt
- 客户端私钥文件:client.key
- 客户端集成证书(包括公钥和私钥,用于浏览器访问场景):client.p12
生成这一些列证书之前,我们需要先生成一个CA根证书,然后由这个CA根证书颁发服务器公钥证书和客户端公钥证书。
证书生成
证书实现的核心是加密,但是也可以被用来做认证,比如istio实现展示了如何用双向证书解决身份、通讯安全,:服务器身份(Server identities)被编码在证书里,但服务名称(service names)通过服务发现或 DNS 被检索。安全命名信息将服务器身份映射到服务名称。身份 A 到服务名称 B 的映射表示“授权 A 运行服务 B“。在双向 TLS 握手期间,客户端Envoy做了安全命名检查,以验证服务器证书中显示的服务帐户是否被授权运行目标服务。
gRPC安全机制
谷歌提供了可扩展的安全认证机制,以满足不同业务场景需求,它提供的授权机制主要有四类:
- 通道凭证:默认提供了基于 HTTP/2 的 TLS,对客户端和服务端交换的所有数据进行加密传输;
- 调用凭证:被附加在每次 RPC 调用上,通过 Credentials 将认证信息附加到消息头中,由服务端做授权认证;
- 组合凭证:将一个频道凭证和一个调用凭证关联起来创建一个新的频道凭证,在这个频道上的每次调用会发送组合的调用凭证来作为授权数据,最典型的场景就是使用 HTTP S 来传输 Access Token;
- Google 的 OAuth 2.0:gRPC 内置的谷歌的 OAuth 2.0 认证机制,通过 gRPC 访问 Google API 时,使用 Service Accounts 密钥作为凭证获取授权令牌。
SSL/TLS认证
用go语言显示下服务端和客户端的调用过程:
服务端使用了证书文件
代码语言:javascript复制func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// create the TLS credentials from files
creds, err := credentials.NewServerTLSFromFile("../cert/server.crt", "../cert/server.key")
if err != nil {
log.Fatalf("could not load TLS keys: %s", err)
}
// create a gRPC option array with the credentials
opts := []grpc.ServerOption{grpc.Creds(creds)}
// create a gRPC server object with server options(opts)
s := grpc.NewServer(opts...)
pb.RegisterSimpleMathServer(s, &rpcimpl.SimpleMathServer{})
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
客户端使用
代码语言:javascript复制func GreatCommonDivisor(first, second string) {
// create the client TLS credentials
creds, err := credentials.NewClientTLSFromFile("../cert/server.crt", "")
// initiate a connection with the server using creds
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewSimpleMathClient(conn)
a, _ := strconv.ParseInt(first, 10, 32)
b, _ := strconv.ParseInt(second, 10, 32)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.GreatCommonDivisor(ctx, &pb.GCDRequest{First: int32(a), Second: int32(b)})
if err != nil {
log.Fatalf("cound not compute: %v", err)
}
log.Printf("The Greatest Common Divisor of %d and %d is %d", a, b, r.Result)
}
GoogleOAuth2.0
gRPC 默认提供了多种 OAuth 2.0 认证机制,假如 gRPC 应用运行在 GCE 里,可以通过服务账号的密钥生成 Token 用于 RPC 调用的鉴权,密钥可以从环境变量 GOOGLE_APPLICATION_CREDENTIALS 对应的文件里加载。如果使用 GCE,可以在虚拟机设置的时候为其配置一个默认的服务账号,运行时可以与认证系统交互并为 Channel 生成 RPC 调用时的 access Token。
自定义安全认证策略
参考 Google 内置的 Credentials 实现类,实现自定义的 Credentials,可以扩展 gRPC 的鉴权策略。