zoukankan      html  css  js  c++  java
  • (转)理解Keystone的四种Token

    Token 是什么

    通俗的讲,token 是用户的一种凭证,需拿正确的用户名/密码向 Keystone 申请才能得到。如果用户每次都采用用户名/密码访问 OpenStack API,容易泄露用户信息,带来安全隐患。所以 OpenStack 要求用户访问其 API 前,必须先获取 token,然后用 token 作为用户凭据访问 OpenStack API。 P1


    四种 Token 的由来

    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

    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

    P2在阐述 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

    P3

    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=
    

    Fernet

    P4

    用户可能会碰上这么一个问题,当集群运行较长一段时间后,访问其 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

    Token 类型UUIDPKIPKIZFernet
    大小 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 如果:

      • Keystone server 负载低,region 少于 3 个,采用 UUID token。
      • Keystone server 负载高,region 少于 3 个,采用 PKI/PKIZ token。
      • Keystone server 负载低,region 大与或等于 3 个,采用 UUID token。
      • Keystone server 负载高,region 大于或等于 3 个,K 版本及以上可考虑采用 Fernet token。

    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中删除?

  • 相关阅读:
    管理页面的类 PageHelper
    接下来打算写一下visual stuido 2013使用git进行远端管理。
    转一下网上找来的tortoise git不用每次都输入邮箱和密码的方法。备查看
    tortoise git使用 git版本库的rsa key来进行ssh连接
    2015年4月1日 14:36:56 EF 主从表更新
    Oop分析方法
    Knative 实战:基于 Knative Serverless 技术实现天气服务-下篇
    超大规模商用 K8s 场景下,阿里巴巴如何动态解决容器资源的按需分配问题?
    从零开始入门 K8s | 可观测性:你的应用健康吗?
    Knative 暂时不会捐给任何基金会 | 云原生生态周报 Vol. 22
  • 原文地址:https://www.cnblogs.com/allcloud/p/5120414.html
Copyright © 2011-2022 走看看