整体架构
鉴权体系架构由控制面和数据面组成。
控制面是指对认证、授权规则进行调整,整体流程如下,
- 客户端通过鉴权 API 发出调整认证、授权规则的请求。
- AuthServer 收到请求后,为确保各节点间鉴权元数据一致性,会先通过 Raft 模块进行数据同步。
- 当对应的 Raft 日志条目被集群半数以上节点确认后,Apply 模块通过鉴权存储 (AuthStore) 模块,执行日志条目的内容,将规则存储到 boltdb 的一系列“鉴权表”里面。
数据面是指在读写 key-value 数据的过程中,判断该读写请求是否合法,整体流程如下,
![](https://img.dawnguo.cn/2024/image-20240403161347005.png)
- etcd server 收到 put hello 的请求后,会从请求的上下文中获取用户身份信息。
- 之后先进行身份认证。目前 etcd 实现了两种认证机制,分别是密码认证和证书认证。
- 如果认证通过,则继续下一步。如果使用了密码认证,这一步则还会给客户端分配一个 Token,在请求返回的时候会把这个 Token 返回给客户端。之后客户端的请求携带此 Token,etcd server 就可以快速完成客户端的身份校验工作。目前 Token 支持 SimpleToken 和 JWT 两种。
- 如果认证不通过,则拒绝请求。
- 认证通过之后,再检查对应角色是否有执行 put 操作的权限。针对授权这块,etcd 采用的是 RBAC。
- 如果权限检查通过,则继续下一步。
- 如果权限检查不通过,则拒绝请求。
- 授权检查也通过了的话,则会按照 Put 数据的流程先通过 Raft 共识,然后将数据写入,最后返回请求的响应。
认证
认证的目的是获取 client 的身份,并检查该身份是否合法,防止匿名或不合法用户访问等。目前 etcd 实现了两种认证机制,分别是密码认证和证书认证。
密码认证
对于密码认证来说,etcd 并不是简单地直接存储明文密码,因为这样安全性太低,一旦数据库泄露,就都知道了。
针对控制面来说,在收到用户添加用户和密码的请求之后,
- 鉴权模块会使用 bcrpt 库的 blowfish 算法,基于明文密码、随机的加盐 salt,并进行多次迭代 hash(迭代次数是自定义的 cost 次),最终得到一个 hash 值。
- 之后将加密算法版本、salt 值、cost、hash 值组成一个字符串,作为加密后的密码。
- 最后,将用户名作为 key,用户名、加密后的密码作为 value,存储到 boltdb 的 AuthUsers bucket 里面。
针对数据面来说,
-
鉴权模块首先根据请求的用户名,从 boltdb 获取加密后的密码。由于加密后的密码包含了算法版本、salt、cost 等信息。因此,鉴权模块会根据请求中的明文密码,计算出最终的 hash 值,若计算结果与存储一致,那么身份校验通过。
-
为了提升密码验证的性能,当验证用户密码成功后,会返回一个 Token 字符串给 client,用于表示用户的身份。后续请求携带此 Token,就无需再次进行密码校验。
etcd 目前支持两种 Token,分别为 Simple Token 和 JWT Token。
-
Simple Token。
核心原理是当一个用户身份验证通过后,生成一个随机的字符串值 Token 返回给 client,并在内存中使用 map 存储用户和 Token 映射关系。当后续收到用户的请求时,etcd 会从请求中获取 Token 值,通过 map 转换成对应的用户名信息。
同时生成的每个 Token,都有一个过期时间 TTL 属性,Token 过期后 client 需再次验证身份。通过调整 TTL,可在性能上、安全性上实现平衡。在 etcd v3.4.9 版本中,Token 默认有效期是 5 分钟。
Simple Token 存在的主要问题是 Simple Token 字符串本身并未含任何有价值信息,对于 client 来说,它无法及时、准确地获取到 Token 过期时间。所以 client 无法提前规避因 Token 失效导致的请求报错。同时需要使用内存存储 Token 和用户名的映射关系。因此 etcd 社区仅建议在开发、测试环境中使用 Simple Token。
-
JWT Token。
Etcd 还提供了 JWT Token,由于 JWT Token 可自带用户名、版本号、过期时间等描述信息,
- 一方面 etcd server 不需要保存它。
- 另一方面 client 可方便、高效的获取到 Token 的过期时间、用户名等信息。
因此它解决了 Simple Token 的若干不足之处,安全性更高。etcd 社区建议大家在生产环境若使用了密码认证,应使用 JWT Token( --auth-token 'jwt'),而不是默认的 Simple Token。
下面介绍下 JWT: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 Token 内容,就是由 base64UrlEncode(header).base64UrlEncode(payload).signature 组成。
-
证书认证
密码认证一般使用在 client 和 server 基于 HTTP 协议通信的内网场景中。当对安全有更高要求的时候,需要使用 HTTPS 协议加密通信数据,防止中间人攻击和数据被篡改等安全风险。HTTPS 是利用非对称加密实现身份认证和密钥协商,在使用 HTTPS 协议的时候,需要使用 CA 证书给 client 生成证书才能访问。
Etcd 支持 x509 证书,Etcd 会将客户端 x509 证书中 Subject 字段的 CN 作为用户名。比如下图中,alice 就是客户端发送请求的用户名。
etcd 通过 --client-cert-auth 参数来启用 client 证书认证。
![](https://img.dawnguo.cn/2024/image-20240403145506859.png)
证书认证在稳定性、性能上都优于密码+Token认证。
- 稳定性上,它不存在 Token 过期、使用更加方便,避免了不少因 Token 失效而触发的 Bug。
- 性能上,证书认证无需像密码认证一样调用昂贵的密码认证操作(Authenticate 请求),该操作的性能极低。
授权
常用的权限控制方法有 ACL(Access Control List)、ABAC(Attribute-based access control)、RBAC(Role-based access control),而 etcd 采用的是 RBAC 机制。
RBAC 机制由三部分组成,User、Role、Permission。
![](https://img.dawnguo.cn/2024/image-20240403150409918.png)
- User 表示用户,如 alice。
- Role 表示角色,它是权限的赋予对象,会包含一系列 Permission。
- Permission 表示具体权限明细,对于 Etcd 来说,就是对 key 范围在 [key,KeyEnd] 的数据是否具有 READ、WRITE、 READWRITE 等相应权限。
因为一个用户可能拥有成百上千个权限列表,因此 etcd 为了提升权限检查的性能,引入了区间树,会快速检查用户操作的 key 是否在已授权的区间,然后对应的权限是什么,时间复杂度为 O(logN)。
相关操作
# 开启鉴权,此时 etcd 会先要求你创建一个 root 账号,它拥有集群的最高读写权限。
$ etcdctl user add root:root
User root created
$ etcdctl auth enable
Authentication Enabled
# 添加用户 alice
$ etcdctl user add alice:alice --user root:root
User alice created
# put hello=world,带上 alice 用户
$ etcdctl put hello world --user alice:alice
# 创建一个 admin role
$ etcdctl role add admin --user root:root
# 分配一个可读写 [hello,helly] 范围数据的权限给 admin role
$ etcdctl role grant-permission admin readwrite hello helly --user root:root
# 将用户 alice 和 admin role 关联起来,赋予 admin 权限给 user
$ etcdctl user grant-role alice admin --user root:root