理解 Keystone 的四種 Token

Token 是什麼

通俗的講,token 是用戶的一種憑證,需拿正確的用戶名/密碼向 Keystone 申請才能得到。如果用戶每次都採用用戶名/密碼訪問 OpenStack API,容易泄露用戶信息,帶來安全隱患。所以 OpenStack 要求用戶訪問其 API 前,必須先獲取 token,然後用 token 作爲用戶憑據訪問 OpenStack API。
這裏寫圖片描述

四種 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
這裏寫圖片描述

在闡述 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
這裏寫圖片描述

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
這裏寫圖片描述

用戶可能會碰上這麼一個問題,當集羣運行較長一段時間後,訪問其 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 類型 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 如果:

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?utm_source=tuicool&utm_medium=referral

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章