You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

308 lines
20 KiB
Markdown

2 years ago
# 05 | 鉴权:如何保护你的数据安全?
你好,我是唐聪。
不知道你有没有过这样的困惑当你使用etcd存储业务敏感数据、多租户共享使用同etcd集群的时候应该如何防止匿名用户访问你的etcd数据呢多租户场景又如何最小化用户权限分配防止越权访问的
etcd鉴权模块就是为了解决以上痛点而生。
那么etcd是如何实现多种鉴权机制和细粒度的权限控制的在实现鉴权模块的过程中最核心的挑战是什么又该如何确保鉴权的安全性以及提升鉴权性能呢
今天这节课我将为你介绍etcd的鉴权模块深入剖析etcd如何解决上面的这些痛点和挑战。希望通过这节课帮助你掌握etcd鉴权模块的设计、实现精要了解各种鉴权方案的优缺点。你能在实际应用中根据自己的业务场景、安全诉求选择合适的方案保护你的etcd数据安全。同时你也可以参考其设计、实现思想应用到自己业务的鉴权系统上。
## 整体架构
在详细介绍etcd的认证、鉴权实现细节之前我先给你从整体上介绍下etcd鉴权体系。
etcd鉴权体系架构由控制面和数据面组成。
![](https://static001.geekbang.org/resource/image/30/4e/304257ac790aeda91616bfe42800364e.png?wh=1920*420)
上图是是etcd鉴权体系控制面你可以通过客户端工具etcdctl和鉴权API动态调整认证、鉴权规则AuthServer收到请求后为了确保各节点间鉴权元数据一致性会通过Raft模块进行数据同步。
当对应的Raft日志条目被集群半数以上节点确认后Apply模块通过鉴权存储(AuthStore)模块执行日志条目的内容将规则存储到boltdb的一系列“鉴权表”里面。
下图是数据面鉴权流程由认证和授权流程组成。认证的目的是检查client的身份是否合法、防止匿名用户访问等。目前etcd实现了两种认证机制分别是密码认证和证书认证。
![](https://static001.geekbang.org/resource/image/2c/55/2c8f90fd1a30fab9b9a88ba18c24c555.png?wh=1920*1136)
认证通过后为了提高密码认证性能会分配一个Token类似我们生活中的门票、通信证给clientclient后续其他请求携带此Tokenserver就可快速完成client的身份校验工作。
实现分配Token的服务也有多种这是TokenProvider所负责的目前支持SimpleToken和JWT两种。
通过认证后在访问MVCC模块之前还需要通过授权流程。授权的目的是检查client是否有权限操作你请求的数据路径etcd实现了RBAC机制支持为每个用户分配一个角色为每个角色授予最小化的权限。
![](https://static001.geekbang.org/resource/image/8d/8a/8d18f8877ea7c8fbyybebae236a8688a.png?wh=1920*1125)
好了etcd鉴权体系的整个流程讲完了下面我们就以[第三节课](https://time.geekbang.org/column/article/336766)中提到的put hello命令为例给你深入分析以上鉴权体系是如何进行身份认证来防止匿名访问的又是如何实现细粒度的权限控制以防止越权访问的。
## 认证
首先我们来看第一个问题如何防止匿名用户访问你的etcd数据呢
解决方案当然是认证用户身份。那etcd提供了哪些机制来验证client身份呢?
正如我整体架构中给你介绍的etcd目前实现了两种机制分别是用户密码认证和证书认证下面我分别给你介绍这两种机制在etcd中如何实现以及这两种机制各自的优缺点。
### 密码认证
首先我们来讲讲用户密码认证。etcd支持为每个用户分配一个账号名称、密码。密码认证在我们生活中无处不在从银行卡取款到微信、微博app登录再到核武器发射密码认证应用及其广泛是最基础的鉴权的方式。
但密码认证存在两大难点,它们分别是如何保障密码安全性和提升密码认证性能。
#### 如何保障密码安全性
我们首先来看第一个难点:如何保障密码安全性。
也许刚刚毕业的你会说直接明文存储,收到用户鉴权请求的时候,检查用户请求中密码与存储中是否一样,不就可以了吗? 这种方案的确够简单,但你是否想过,若存储密码的文件万一被黑客脱库了,那么所有用户的密码都将被泄露,进而可能会导致重大数据泄露事故。
也许你又会说自己可以奇思妙想构建一个加密算法然后将密码翻译下比如将密码中的每个字符按照字母表序替换成字母后的第XX个字母。然而这种加密算法它是可逆的一旦被黑客识别到规律还原出你的密码后脱库后也将导致全部账号数据泄密。
那么是否我们用一种不可逆的加密算法就行了呢比如常见的MD5SHA-1这方案听起来似乎有点道理然而还是不严谨因为它们的计算速度非常快黑客可以通过暴力枚举、字典、彩虹表等手段快速将你的密码全部破解。
LinkedIn在2012年的时候650万用户密码被泄露黑客3天就暴力破解出90%用户的密码原因就是LinkedIn仅仅使用了SHA-1加密算法。
**那应该如何进一步增强不可逆hash算法的破解难度**
一方面我们可以使用安全性更高的hash算法比如SHA-256它输出位数更多、计算更加复杂且耗CPU。
另一方面我们可以在每个用户密码hash值的计算过程中引入一个随机、较长的加盐(salt)参数,它可以让相同的密码输出不同的结果,这让彩虹表破解直接失效。
彩虹表是黑客破解密码的一种方法之一它预加载了常用密码使用MD5/SHA-1计算的hash值可通过hash值匹配快速破解你的密码。
最后我们还可以增加密码hash值计算过程中的开销比如循环迭代更多次增加破解的时间成本。
**etcd的鉴权模块如何安全存储用户密码**
etcd的用户密码存储正是融合了以上讨论的高安全性hash函数Blowfish encryption algorithm、随机的加盐salt、可自定义的hash值计算迭代次数cost。
下面我将通过几个简单etcd鉴权API为你介绍密码认证的原理。
首先你可以通过如下的auth enable命令开启鉴权注意etcd会先要求你创建一个root账号它拥有集群的最高读写权限。
```
$ etcdctl user add root:root
User root created
$ etcdctl auth enable
Authentication Enabled
```
启用鉴权后这时client发起如下put hello操作时 etcd server会返回"user name is empty"错误给client就初步达到了防止匿名用户访问你的etcd数据目的。 那么etcd server是在哪里做的鉴权的呢?
```
$ etcdctl put hello world
Error: etcdserver: user name is empty
```
etcd server收到put hello请求的时候在提交到Raft模块前它会从你请求的上下文中获取你的用户身份信息。如果你未通过认证那么在状态机应用put命令的时候检查身份权限的时候发现是空就会返回此错误给client。
下面我通过鉴权模块的user命令给etcd增加一个alice账号。我们一起来看看etcd鉴权模块是如何基于我上面介绍的技术方案来安全存储alice账号信息。
```
$ etcdctl user add alice:alice --user root:root
User alice created
```
鉴权模块收到此命令后它会使用bcrpt库的blowfish算法基于明文密码、随机分配的salt、自定义的cost、迭代多次计算得到一个hash值并将加密算法版本、salt值、cost、hash值组成一个字符串作为加密后的密码。
最后鉴权模块将用户名alice作为key用户名、加密后的密码作为value存储到boltdb的authUsers bucket里面完成一个账号创建。
当你使用alice账号访问etcd的时候你需要先调用鉴权模块的Authenticate接口它会验证你的身份合法性。
那么etcd如何验证你密码正确性的呢
鉴权模块首先会根据你请求的用户名alice从boltdb获取加密后的密码因此hash值包含了算法版本、salt、cost等信息因此可以根据你请求中的明文密码计算出最终的hash值若计算结果与存储一致那么身份校验通过。
#### 如何提升密码认证性能
通过以上的鉴权安全性的深入分析,我们知道身份验证这个过程开销极其昂贵,那么问题来了,如何避免频繁、昂贵的密码计算匹配,提升密码认证的性能呢?
这就是密码认证的第二个难点,如何保证性能。
想想我们办理港澳通行证的时候,流程特别复杂,需要各种身份证明、照片、指纹信息,办理成功后,下发通信证,每次过关你只需要刷下通信证即可,高效而便捷。
那么在软件系统领域如果身份验证通过了后我们是否也可以返回一个类似通信证的凭据给client后续请求携带通信证只要通行证合法且在有效期内就无需再次鉴权了呢
是的etcd也有类似这样的凭据。当etcd server验证用户密码成功后它就会返回一个Token字符串给client用于表示用户的身份。后续请求携带此Token就无需再次进行密码校验实现了通信证的效果。
etcd目前支持两种Token分别为Simple Token和JWT Token。
**Simple Token**
Simple Token实现正如名字所言简单。
Simple Token的核心原理是当一个用户身份验证通过后生成一个随机的字符串值Token返回给client并在内存中使用map存储用户和Token映射关系。当收到用户的请求时 etcd会从请求中获取Token值转换成对应的用户名信息返回给下层模块使用。
Token是你身份的象征若此Token泄露了那你的数据就可能存在泄露的风险。etcd是如何应对这种潜在的安全风险呢
etcd生成的每个Token都有一个过期时间TTL属性Token过期后client需再次验证身份因此可显著缩小数据泄露的时间窗口在性能上、安全性上实现平衡。
在etcd v3.4.9版本中Token默认有效期是5分钟etcd server会定时检查你的Token是否过期若过期则从map数据结构中删除此Token。
不过你要注意的是Simple Token字符串本身并未含任何有价值信息因此client无法及时、准确获取到Token过期时间。所以client不容易提前去规避因Token失效导致的请求报错。
从以上介绍中你觉得Simple Token有哪些不足之处为什么etcd社区仅建议在开发、测试环境中使用Simple Token呢
首先它是有状态的etcd server需要使用内存存储Token和用户名的映射关系。
其次它的可描述性很弱client无法通过Token获取到过期时间、用户名、签发者等信息。
etcd鉴权模块实现的另外一个Token Provider方案JWT正是为了解决这些不足之处而生。
**JWT Token**
JWT是Json Web Token缩写 它是一个基于JSON的开放标准RFC 7519定义的一种紧凑、独立的格式可用于在身份提供者和服务提供者间传递被认证的用户身份信息。它由Header、Payload、Signature三个对象组成 每个对象都是一个JSON结构体。
第一个对象是Header它包含alg和typ两个字段alg表示签名的算法etcd支持RSA、ESA、PS系列typ表示类型就是JWT。
```
{
"alg": "RS256"
"typ": "JWT"
}
```
第二对象是Payload它表示载荷包含用户名、过期时间等信息可以自定义添加字段。
```
{
"username": username
"revision": revision
"exp": time.Now().Add(t.ttl).Unix()
}
```
第三个对象是签名首先它将header、payload使用base64 url编码然后将编码后的
字符串用"."连接在一起最后用我们选择的签名算法比如RSA系列的私钥对其计算签名输出结果即是Signature。
```
signature=RSA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload)
key)
```
JWT就是由base64UrlEncode(header).base64UrlEncode(payload).signature组成。
为什么说JWT是独立、紧凑的格式呢
从以上原理介绍中我们知道它是无状态的。JWT Token自带用户名、版本号、过期时间等描述信息etcd server不需要保存它client可方便、高效的获取到Token的过期时间、用户名等信息。它解决了Simple Token的若干不足之处安全性更高etcd社区建议大家在生产环境若使用了密码认证应使用JWT Token( --auth-token 'jwt')而不是默认的Simple Token。
在给你介绍完密码认证实现过程中的两个核心挑战,密码存储安全和性能的解决方案之后,你是否对密码认证的安全性、性能还有所担忧呢?
接下来我给你介绍etcd的另外一种高性能、更安全的鉴权方案x509证书认证。
### 证书认证
密码认证一般使用在client和server基于HTTP协议通信的内网场景中。当对安全有更高要求的时候你需要使用HTTPS协议加密通信数据防止中间人攻击和数据被篡改等安全风险。
HTTPS是利用非对称加密实现身份认证和密钥协商因此使用HTTPS协议的时候你需要使用CA证书给client生成证书才能访问。
那么一个client证书包含哪些信息呢使用证书认证的时候etcd server如何知道你发送的请求对应的用户名称
我们可以使用下面的openssl命令查看client证书的内容下图是一个x509 client证书的内容它含有证书版本、序列号、签名算法、签发者、有效期、主体名等信息我们重点要关注的是主体名中的CN字段。
在etcd中如果你使用了HTTPS协议并启用了client证书认证(--client-cert-auth)它会取CN字段作为用户名在我们的案例中alice就是client发送请求的用户名。
```
openssl x509 -noout -text -in client.pem
```
![](https://static001.geekbang.org/resource/image/55/94/55e03b4353c9a467493a3922cf68b294.png?wh=1144*854)
证书认证在稳定性、性能上都优于密码认证。
稳定性上它不存在Token过期、使用更加方便、会让你少踩坑避免了不少Token失效而触发的Bug。性能上证书认证无需像密码认证一样调用昂贵的密码认证操作(Authenticate请求),此接口支持的性能极低,后面实践篇会和你深入讨论。
## 授权
当我们使用如上创建的alice账号执行put hello操作的时候etcd却会返回如下的"etcdserver: permission denied"无权限错误,这是为什么呢?
```
$ etcdctl put hello world --user alice:alice
Error: etcdserver: permission denied
```
这是因为开启鉴权后put请求命令在应用到状态机前etcd还会对发出此请求的用户进行权限检查 判断其是否有权限操作请求的数据。常用的权限控制方法有ACL(Access Control List)、ABAC(Attribute-based access control)、RBAC(Role-based access control)etcd实现的是RBAC机制。
### RBAC
什么是基于角色权限的控制系统(RBAC)呢?
它由下图中的三部分组成User、Role、Permission。User表示用户如alice。Role表示角色它是权限的赋予对象。Permission表示具体权限明细比如赋予Role对key范围在\[keyKeyEnd\]数据拥有什么权限。目前支持三种权限分别是READ、WRITE、READWRITE。
![](https://static001.geekbang.org/resource/image/ee/60/ee6e0a9a63aeaa2d3505ab1a37360760.png?wh=1786*1134)
下面我们通过etcd的RBAC机制给alice用户赋予一个可读写\[hello,helly\]数据范围的读写权限, 如何操作呢?
按照上面介绍的RBAC原理首先你需要创建一个role这里我们命名为admin然后新增了一个可读写\[hello,helly\]数据范围的权限给admin角色并将admin的角色的权限授予了用户alice。详细如下
```
$ #创建一个admin role
etcdctl role add admin --user root:root
Role admin created
# #分配一个可读写[hellohelly]范围数据的权限给admin role
$ etcdctl role grant-permission admin readwrite hello helly --user root:root
Role admin updated
# 将用户alice和admin role关联起来赋予admin权限给user
$ etcdctl user grant-role alice admin --user root:root
Role admin is granted to user alice
```
然后当你再次使用etcdctl执行put hello命令时鉴权模块会从boltdb查询alice用户对应的权限列表。
因为有可能一个用户拥有成百上千个权限列表etcd为了提升权限检查的性能引入了区间树检查用户操作的key是否在已授权的区间时间复杂度仅为O(logN)。
在我们的这个案例中很明显hello在admin角色可读写的\[hellohelly)数据范围内因此它有权限更新key hello执行成功。你也可以尝试更新key hey因为此key未在鉴权的数据区间内因此etcd server会返回"etcdserver: permission denied"错误给client如下所示。
```
$ etcdctl put hello world --user alice:alice
OK
$ etcdctl put hey hey --user alice:alice
Error: etcdserver: permission denied
```
## 小结
最后我和你总结下今天的内容从etcd鉴权模块核心原理分析过程中你会发现设计实现一个鉴权模块最关键的目标和挑战应该是安全、性能以及一致性。
首先鉴权目的是为了保证安全必须防止恶意用户绕过鉴权系统、伪造、篡改、越权等行为同时设计上要有前瞻性做到即使被拖库也影响可控。etcd的解决方案是通过密码安全加密存储、证书认证、RBAC等机制保证其安全性。
然后鉴权作为了一个核心的前置模块性能上不能拖后腿不能成为影响业务性能的一个核心瓶颈。etcd的解决方案是通过Token降低频繁、昂贵的密码验证开销可应用在内网、小规模业务场景同时支持使用证书认证不存在Token过期巧妙的取CN字段作为用户名可满足较大规模的业务场景鉴权诉求。
接着鉴权系统面临的业务场景是复杂的因此权限控制系统应当具备良好的扩展性业务可根据自己实际场景选择合适的鉴权方法。etcd的Token Provider和RBAC扩展机制都具备较好的扩展性、灵活性。尤其是RBAC机制让你可以精细化的控制每个用户权限实现权限最小化分配。
最后鉴权系统元数据的存储应当是可靠的各个节点鉴权数据应确保一致确保鉴权行为一致性。早期etcd v2版本时因鉴权命令未经过Raft模块存在数据不一致的问题在etcd v3中通过Raft模块同步鉴权指令日志指令实现鉴权数据一致性。
## 思考题
最后我给你留了一个思考题。你在使用etcd鉴权特性过程中遇到了哪些问题又是如何解决的呢
感谢你的阅读,欢迎你把思考和观点写在留言区,也欢迎你把这篇文章分享给更多的朋友一起阅读。
## 04思考题参考答案
04讲的思考题mckee同学给出了精彩回答下面是他的回答。
1.哪些场景会出现 Follower 日志与 Leader 冲突?
leader崩溃的情况下可能(如老的leader可能还没有完全复制所有的日志条目)如果leader和follower出现持续崩溃会加剧这个现象。follower可能会丢失一些在新的leader中有的日志条目他也可能拥有一些leader没有的日志条目或者两者都发生。
2.follower如何删除无效日志
leader处理不一致是通过强制follower直接复制自己的日志来解决。因此在follower中的冲突的日志条目会被leader的日志覆盖。leader会记录follower的日志复制进度nextIndex如果follower在追加日志时一致性检查失败就会拒绝请求此时leader就会减小 nextIndex 值并进行重试最终在某个位置让follower跟leader一致。
这里我补充下为什么WAL日志模块只通过追加也能删除已持久化冲突的日志条目呢 其实这里etcd在实现上采用了一些比较有技巧的方法在WAL日志中的确没删除废弃的日志条目你可以在其中搜索到冲突的日志条目。只是etcd加载WAL日志时发现一个raft log index位置上有多个日志条目的时候会通过覆盖的方式将最后写入的日志条目追加到raft log中实现了删除冲突日志条目效果你如果感兴趣可以参考下我和Google ptabor[关于这个问题的讨论](https://github.com/etcd-io/etcd/issues/12589)。