标签:
通俗的讲,token 是用户的一种凭证,需拿正确的用户名/密码向 Keystone 申请才能得到。如果用户每次都采用用户名/密码访问 OpenStack API,容易泄露用户信息,带来安全隐患。所以 OpenStack 要求用户访问其 API 前,必须先获取 token,然后用 token 作为用户凭据访问 OpenStack API。
D 版本时,仅有 UUID 类型的 Token,UUID token 简单易用,却容易给 Keystone 带来性能问题,从图一的步骤 4 可看出,每当 OpenStack API 收到用户请求,都需要向 Keystone 验证该 token 是否有效。随着集群规模的扩大,Keystone 需处理大量验证 token 的请求,在高并发下容易出现性能问题。
于是 PKI(Public Key Infrastructrue) token 在 G 版本运用而生,和 UUID 相比,PKI token 携带更多用户信息的同时还附上了数字签名,以支持本地认证,从而避免了步骤 4。因为 PKI token 携带了更多的信息,这些信息就包括 service catalog,随着 OpenStack 的 Region 数增多,service catalog 携带的 endpoint 数量越多,PKI token 也相应增大,很容易超出 HTTP Server 允许的最大 HTTP Header(默认为 8 KB),导致 HTTP 请求失败。
顾名思义,PKIZ token 就是 PKI token 的压缩版,但压缩效果有限,无法良好的处理 token size 过大问题。
前三种 token 都会持久性存于数据库,与日俱增积累的大量 token 引起数据库性能下降,所以用户需经常清理数据库的 token。为了避免该问题,社区提出了 Fernet token,它携带了少量的用户信息,大小约为 255 Byte,采用了对称加密,无需存于数据库中。
UUID token 是长度固定为 32 Byte 的随机字符串,由 uuid.uuid4().hex 生成。
def _get_token_id(self, token_data):
return uuid.uuid4().hex
但是因 UUID token 不携带其它信息,OpenStack API 收到该 token 后,既不能判断该 token 是否有效,更无法得知该 token 携带的用户信息,所以需经图一步骤 4 向 Keystone 校验 token,并获用户相关的信息。其样例如下:
144d8a99a42447379ac37f78bf0ef608
UUID token 简单美观,不携带其它信息,因此 Keystone 必须实现 token 的存储和认证,随着集群的规模增大,Keystone 将成为性能瓶颈。
在阐述 PKI(Public Key Infrastruction) token 前,让我们简单的回顾公开密钥加密(public-key cryptography)和数字签名。公开密钥加密,也称为非对称加密(asymmetric cryptography,加密密钥和解密密钥不相同),在这种密码学方法中,需要一对密钥,分别为公钥(Public Key)和私钥(Private Key),公钥是公开的,私钥是非公开的,需用户妥善保管。如果把加密和解密的流程当做函数 C(x) 和 D(x),P 和 S 分别代表公钥和私钥,对明文 A 和密文 B 而言,数学的角度上有以下公式:
B = C(A, S)
A = D(B, P)
其中加密函数 C(x), 解密函数 D(x) 以及公钥 P 均是公开的。采用公钥加密的密文只能用私钥解密,采用私钥加密的密文只能用公钥解密。非对称加密广泛运用在安全领域,诸如常见的 HTTPS,SSH 登录等。
数字签名又称为公钥数字签名,首先采用 Hash 函数对消息生成摘要,摘要经私钥加密后称为数字签名。接收方用公钥解密该数字签名,并与接收消息生成的摘要做对比,如果二者一致,便可以确认该消息的完整性和真实性。
PKI 的本质就是基于数字签名,Keystone 用私钥对 token 进行数字签名,各个 API server 用公钥在本地验证该 token。相关代码简化如下:
def _get_token_id(self, token_data): try: token_json = jsonutils.dumps(token_data, cls=utils.PKIEncoder) token_id = str(cms.cms_sign_token(token_json, CONF.signing.certfile, CONF.signing.keyfile)) return token_id
其中 cms.cms_sign_token 调用 openssl cms --sign 对 token_data 进行签名,token_data 的样式如下:
{ "token": { "methods": [ "password" ], "roles": [{"id": "5642056d336b4c2a894882425ce22a86", "name": "admin"}], "expires_at": "2015-12-25T09:57:28.404275Z", "project": { "domain": { "id": "default", "name": "Default"}, "id": "144d8a99a42447379ac37f78bf0ef608", "name": "admin"}, "catalog": [ { "endpoints": [ { "region_id": "RegionOne", "url": "http://controller:5000/v2.0", "region": "RegionOne", "interface": "public", "id": "3837de623efd4af799e050d4d8d1f307" }, ...... ]}], "extras": {}, "user": { "domain": {"id": "default", "name": "Default"}, "id": "1552d60a042e4a2caa07ea7ae6aa2f09", "name": "admin"}, "audit_ids": ["ZCvZW2TtTgiaAsVA8qmc3A"], "issued_at": "2015-12-25T08:57:28.404304Z" } }
token_data 经 cms.cms_sign_token 签名生成的 token_id 如下,共 1932 Byte:
MIIKoZIhvcNAQcCoIIFljCCBZICAQExDTALBglghkgBZQMEAgEwggPzBgkqhkiG9w0B
......
rhr0acV3bMKzmqvViHf-fPVnLDMJajOWSuhimqfLZHRdr+ck0WVQosB6+M6iAvrEF7v
PKIZ 在 PKI 的基础上做了压缩处理,但是压缩的效果极其有限,一般情况下,压缩后的大小为 PKI token 的 90 % 左右,所以 PKIZ 不能友好的解决 token size 太大问题。
def _get_token_id(self, token_data): try: token_json = jsonutils.dumps(token_data, cls=utils.PKIEncoder) token_id = str(cms.pkiz_sign(token_json, CONF.signing.certfile, CONF.signing.keyfile)) return token_id
其中 cms.pkiz_sign() 中的以下代码调用 zlib 对签名后的消息进行压缩级别为 6 的压缩。
compressed = zlib.compress(token_id, compression_level=6)
PKIZ token 样例如下,共 1645 Byte,比 PKI token 减小 14.86 %:
PKIZ_eJytVcuOozgU3fMVs49aTXhUN0vAQEHFJiRg8IVHgn5OnA149JVaunNS3NYjoSU
......
W4fRaxrbNtinojheVICXYrEk0oPX6TSnP71IYj2e3nm4MLy7S84PtIPDz4_03IsOb2Q=
用户可能会碰上这么一个问题,当集群运行较长一段时间后,访问其 API 会变得奇慢无比,究其原因在于 Keystone 数据库存储了大量的 token 导致性能太差,解决的办法是经常清理 token。为了避免上述问题,社区提出了Fernet token,它采用 cryptography 对称加密库(symmetric cryptography,加密密钥和解密密钥相同) 加密 token,具体由 AES-CBC 加密和散列函数 SHA256 签名。Fernet 是专为 API token 设计的一种轻量级安全消息格式,不需要存储于数据库,减少了磁盘的 IO,带来了一定的性能提升。为了提高安全性,需要采用 Key Rotation 更换密钥。
def create_token(self, user_id, expires_at, audit_ids, methods=None, domain_id=None, project_id=None, trust_id=None, federated_info=None): """Given a set of payload attributes, generate a Fernet token.""" if trust_id: version = TrustScopedPayload.version payload = TrustScopedPayload.assemble( user_id, methods, project_id, expires_at, audit_ids, trust_id) ... versioned_payload = (version,) + payload serialized_payload = msgpack.packb(versioned_payload) token = self.pack(serialized_payload) return token
以上代码表明,token 包含了 user_id,project_id,domain_id,methods,expires_at 等信息,重要的是,它没有 service_catalog,所以 region 的数量并不影响它的大小。self.pack() 最终调用如下代码对上述信息加密:
def crypto(self): keys = utils.load_keys() if not keys: raise exception.KeysNotFound() fernet_instances = [fernet.Fernet(key) for key in utils.load_keys()] return fernet.MultiFernet(fernet_instances)
该 token 的大小一般在 200 多 Byte 左右,本例样式如下,大小为 186 Byte:
gAAAAABWfX8riU57aj0tkWdoIL6UdbViV-632pv0rw4zk9igCZXgC-sKwhVuVb-wyMVC9e5TFc
7uPfKwNlT6cnzLalb3Hj0K3bc1X9ZXhde9C2ghsSfVuudMhfR8rThNBnh55RzOB8YTyBnl9MoQ
XBO5UIFvC7wLTh_2klihb6hKuUqB6Sj3i_8
Token 类型 | UUID | PKI | PKIZ | Fernet |
---|---|---|---|---|
大小 | 32 Byte | KB 级别 | KB 级别 | 约 255 Byte |
支持本地认证 | 不支持 | 支持 | 支持 | 不支持 |
Keystone 负载 | 大 | 小 | 小 | 大 |
存储于数据库 | 是 | 是 | 是 | 否 |
携带信息 | 无 | user, catalog 等 | user, catalog 等 | user 等 |
涉及加密方式 | 无 | 非对称加密 | 非对称加密 | 对称加密(AES) |
是否压缩 | 否 | 否 | 是 | 否 |
版本支持 | D | G | J | K |
Token 类型的选择涉及多个因素,包括 Keystone server 的负载、region 数量、安全因素、维护成本以及 token 本身的成熟度。region 的数量影响 PKI/PKIZ token 的大小,从安全的角度上看,UUID 无需维护密钥,PKI 需要妥善保管 Keystone server 上的私钥,Fernet 需要周期性的更换密钥,因此从安全、维护成本和成熟度上看,UUID > PKI/PKIZ > Fernet 如果:
http://wsfdl.com/openstack/2015/12/26/%E7%90%86%E8%A7%A3Keystone%E7%9A%84%E5%9B%9B%E7%A7%8DToken.html
2个问题:
PKI既然支持本地验证为何还有存到DB中?
uuid既然有有效时间,为何不自动在DB中删除?
标签:
原文地址:http://www.cnblogs.com/allcloud/p/5120414.html