05 鉴权:如何保护你的数据安全? 你好,我是唐聪。

不知道你有没有过这样的困惑,当你使用etcd存储业务敏感数据、多租户共享使用同etcd集群的时候,应该如何防止匿名用户访问你的etcd数据呢?多租户场景又如何最小化用户权限分配,防止越权访问的?

etcd鉴权模块就是为了解决以上痛点而生。

那么etcd是如何实现多种鉴权机制和细粒度的权限控制的?在实现鉴权模块的过程中最核心的挑战是什么?又该如何确保鉴权的安全性以及提升鉴权性能呢?

今天这节课,我将为你介绍etcd的鉴权模块,深入剖析etcd如何解决上面的这些痛点和挑战。希望通过这节课,帮助你掌握etcd鉴权模块的设计、实现精要,了解各种鉴权方案的优缺点。你能在实际应用中,根据自己的业务场景、安全诉求,选择合适的方案保护你的etcd数据安全。同时,你也可以参考其设计、实现思想应用到自己业务的鉴权系统上。

整体架构

在详细介绍etcd的认证、鉴权实现细节之前,我先给你从整体上介绍下etcd鉴权体系。

etcd鉴权体系架构由控制面和数据面组成。

上图是是etcd鉴权体系控制面,你可以通过客户端工具etcdctl和鉴权API动态调整认证、鉴权规则,AuthServer收到请求后,为了确保各节点间鉴权元数据一致性,会通过Raft模块进行数据同步。

当对应的Raft日志条目被集群半数以上节点确认后,Apply模块通过鉴权存储(AuthStore)模块,执行日志条目的内容,将规则存储到boltdb的一系列“鉴权表”里面。

下图是数据面鉴权流程,由认证和授权流程组成。认证的目的是检查client的身份是否合法、防止匿名用户访问等。目前etcd实现了两种认证机制,分别是密码认证和证书认证。

认证通过后,为了提高密码认证性能,会分配一个Token(类似我们生活中的门票、通信证)给client,client后续其他请求携带此Token,server就可快速完成client的身份校验工作。

实现分配Token的服务也有多种,这是TokenProvider所负责的,目前支持SimpleToken和JWT两种。

通过认证后,在访问MVCC模块之前,还需要通过授权流程。授权的目的是检查client是否有权限操作你请求的数据路径,etcd实现了RBAC机制,支持为每个用户分配一个角色,为每个角色授予最小化的权限。

好了,etcd鉴权体系的整个流程讲完了,下面我们就以第三节课中提到的put hello命令为例,给你深入分析以上鉴权体系是如何进行身份认证来防止匿名访问的,又是如何实现细粒度的权限控制以防止越权访问的。

认证

首先我们来看第一个问题,如何防止匿名用户访问你的etcd数据呢?

解决方案当然是认证用户身份。那etcd提供了哪些机制来验证client身份呢?

正如我整体架构中给你介绍的,etcd目前实现了两种机制,分别是用户密码认证和证书认证,下面我分别给你介绍这两种机制在etcd中如何实现,以及这两种机制各自的优缺点。

密码认证

首先我们来讲讲用户密码认证。etcd支持为每个用户分配一个账号名称、密码。密码认证在我们生活中无处不在,从银行卡取款到微信、微博app登录,再到核武器发射,密码认证应用及其广泛,是最基础的鉴权的方式。

但密码认证存在两大难点,它们分别是如何保障密码安全性和提升密码认证性能。

如何保障密码安全性

我们首先来看第一个难点:如何保障密码安全性。

也许刚刚毕业的你会说直接明文存储,收到用户鉴权请求的时候,检查用户请求中密码与存储中是否一样,不就可以了吗? 这种方案的确够简单,但你是否想过,若存储密码的文件万一被黑客脱库了,那么所有用户的密码都将被泄露,进而可能会导致重大数据泄露事故。

也许你又会说,自己可以奇思妙想构建一个加密算法,然后将密码翻译下,比如将密码中的每个字符按照字母表序替换成字母后的第XX个字母。然而这种加密算法,它是可逆的,一旦被黑客识别到规律,还原出你的密码后,脱库后也将导致全部账号数据泄密。

那么是否我们用一种不可逆的加密算法就行了呢?比如常见的MD5,SHA-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

证书认证在稳定性、性能上都优于密码认证。

稳定性上,它不存在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范围在[key,KeyEnd]数据拥有什么权限。目前支持三种权限,分别是READ、WRITE、READWRITE。

下面我们通过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 /# /#分配一个可读写[hello,helly]范围数据的权限给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角色可读写的[hello,helly)数据范围内,因此它有权限更新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://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/etcd%e5%ae%9e%e6%88%98%e8%af%be/05%20%e9%89%b4%e6%9d%83%ef%bc%9a%e5%a6%82%e4%bd%95%e4%bf%9d%e6%8a%a4%e4%bd%a0%e7%9a%84%e6%95%b0%e6%8d%ae%e5%ae%89%e5%85%a8%ef%bc%9f.md