概要
Keystone のトークンは、2つ以上の技術の層で構成される階層構造となっている。
ペイロードは、いくつかのトランスポート形式でラップされており、一意性、ID、および認証コンテキストなどの属性を提供する。
トランスポートフォーマットでは、伝送と検証に必要なパッケージを提供する。
Keystone で使用可能な認証ドライバとして、UUID、PKI、PKIZ、Fernet がある。(2018年11月20日 時点)。
それぞれのメリット・デメリットについて以下に示す。
Keystone 認証ドライバ | メリット | デメリット | 備考 |
---|---|---|---|
UUID | - シンプルで最小のトークン形式 シンプルな OpenStack の環境をつくる場合には良い |
- 永続的にトークンを保持する必要がある - トークンテーブルが大きくなるとパフォーマンスに影響が出る - トークン検証にはメタデータを保持した keystone 等の認証サービスへの接続が必須 - マルチ OpenStack 環境には不向き |
Newton 以降、Keystone のデフォルトのトークンドライバが “UUID” から “fernet” に変更となる - Rocky で削除予定 |
PKI | トークン検証として keystone への問い合わせは不要 | 標準的な HTTP ヘッダーサイズより大きい - 複雑な設定 - マルチ OpenStack 環境への適用は不可 |
Ocata 以降、非推奨となる |
PKIZ | トークン検証として keystone への問い合わせは不要 - 圧縮により PKI のトークンサイズよりは小さくなる |
標準的な HTTP ヘッダーサイズより大きい - 複雑な設定 - マルチ OpenStack 環境への適用は不可 |
|
Fernet | 永続的にトークンを保持しないため、定期的にトークンを削除する cron ジョブは不要 - トークンサイズサイズが小さい - マルチ DC 環境に最適 |
失効イベント数に比例してトークン検証のレスポンス時間が増加 | Ocata 以降、デフォルトで使用される |
UUID
ペイロード: UUID4
Keystone がサポートする最もシンプルなトークン形式で、UUID トークンのデータは、一意のランダムに生成された UUID4 値のみで構成される。
In [1]: import uuid
In [2]: payload = uuid.uuid4()
In [3]: payload
Out[3]: UUID('0b1e0002-31c8-482a-837f-e2842b2c3d92')
In [4]: print(payload)
0b1e0002-31c8-482a-837f-e2842b2c3d92
書式: 16進数
これらのトークンは、その16進表記でパッケージ化される。
In [5]: token = payload.hex
In [6]: token
Out[6]: '0b1e000231c8482a837fe2842b2c3d92'
In [7]: len(token)
Out[7]: 32
トークンは、0b1e000231c8482a837fe2842b2c3d92
といった形式となる。
トークン自体に ID や認証コンテキストは含まないため、オンラインで Keystone を使用した検証を行う必要がある。
Pyystoneでは、発行された UUID トークンを ID および認証コンテキストとマッピングを行う。
トークン生成フロー
TBD.
トークン検証フロー
トークン失効フロー
PKI/PKIZ
PKIトークンには、ユーザのカタログ情報が含まれており、クラウドの規模に比例して大きくなる可能性がある。
PKI と PKIZトークンはほぼ同じだが、PKIZ トークンは圧縮を行う点が大きく異なる。
ペイロード: JSON
PKI/PKIZ トークンは、トークンのペイロードとして、オンラインでのトークン検証結果として生成される JSON 形式のデータが使用される。
{
"token": {
"audit_ids": [
"YyobSaHcTNCu7seusdTtpQ"
],
"catalog": [
{
"endpoints": [
{
"id": "9a29eaf20f7942b6b9c96cfb0aa02a3e",
"interface": "admin",
"region": null,
"region_id": null,
"url": "http://104.239.163.215:35357/v3"
},
{
"id": "d3233afd2b6041d4a39f8ac1233757fd",
"interface": "public",
"region": null,
"region_id": null,
"url": "http://104.239.163.215:35357/v3"
}
],
"id": "1b796e214f8140118108a7e4e4ca6e16",
"name": "Keystone",
"type": "identity"
}
],
"expires_at": "2015-02-26T05:48:26.094098Z",
"extras": {},
"issued_at": "2015-02-26T05:33:26.094127Z",
"methods": [
"password"
],
"project": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "59002ce739f143bb8b2cc33caf98fcf9",
"name": "admin"
},
"roles": [
{
"id": "360b177d8c2347ff95e0ac1615ba8fb6",
"name": "admin"
}
],
"user": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "85a9af145ddb4d19a9544dfbeac5d1f0",
"name": "admin"
}
}
}
書式: CMS + [zlib] + base64
JSON ペイロードは、最初に非対称キーを使用して署名され、暗号メッセージ構文でラップされる。
PKIZ トークンの場合、署名されたペイロードは zlib を使用して圧縮され、その後、PKI トークンは base64でエンコードされ、次に任意の置換方式を使用して URL セーフにされる。
代わりに、PDKZ トークンは、従来の置換スキームを使用して base64-URL エンコードされる。
最後に、PKI トークンには固定プレフィックスがありませんが、PKIZ トークンには明示的に PKIZ_ というプレフィックスが付く。
最小の PKI トークンは以下のようになる。
MIIE-gYJKoZIhvcNAQcCoIIE7zCCBOsCAQExDTALBglghkgBZQMEAgEwggNMBgkqhkiG9w0BBwGgggM9BIIDOXsidG9rZW4iOnsibWV0aG9kcyI6WyJwYXNzd29yZCJd
LCJyb2xlcyI6W3siaWQiOiIzNjBiMTc3ZDhjMjM0N2ZmOTVlMGFjMTYxNWJhOGZiNiIsIm5hbWUiOiJhZG1pbiJ9XSwiZXhwaXJlc19hdCI6IjIwMTUtMDItMjZUMDU6
NDg6MjYuMDk0MDk4WiIsInByb2plY3QiOnsiZG9tYWluIjp7ImlkIjoiZGVmYXVsdCIsIm5hbWUiOiJEZWZhdWx0In0sImlkIjoiNTkwMDJjZTczOWYxNDNiYjhiMmNj
MzNjYWY5OGZjZjkiLCJuYW1lIjoiYWRtaW4ifSwiY2F0YWxvZyI6W3siZW5kcG9pbnRzIjpbeyJyZWdpb25faWQiOm51bGwsInVybCI6Imh0dHA6Ly8xMDQuMjM5LjE2
My4yMTU6MzUzNTcvdjMiLCJyZWdpb24iOm51bGwsImludGVyZmFjZSI6ImFkbWluIiwiaWQiOiI5YTI5ZWFmMjBmNzk0MmI2YjljOTZjZmIwYWEwMmEzZSJ9LHsicmVn
aW9uX2lkIjpudWxsLCJ1cmwiOiJodHRwOi8vMTA0LjIzOS4xNjMuMjE1OjM1MzU3L3YzIiwicmVnaW9uIjpudWxsLCJpbnRlcmZhY2UiOiJwdWJsaWMiLCJpZCI6ImQz
MjMzYWZkMmI2MDQxZDRhMzlmOGFjMTIzMzc1N2ZkIn1dLCJ0eXBlIjoiaWRlbnRpdHkiLCJpZCI6IjFiNzk2ZTIxNGY4MTQwMTE4MTA4YTdlNGU0Y2E2ZTE2IiwibmFt
ZSI6IktleXN0b25lIn1dLCJleHRyYXMiOnt9LCJ1c2VyIjp7ImRvbWFpbiI6eyJpZCI6ImRlZmF1bHQiLCJuYW1lIjoiRGVmYXVsdCJ9LCJpZCI6Ijg1YTlhZjE0NWRk
YjRkMTlhOTU0NGRmYmVhYzVkMWYwIiwibmFtZSI6ImFkbWluIn0sImF1ZGl0X2lkcyI6WyJZeW9iU2FIY1ROQ3U3c2V1c2RUdHBRIl0sImlzc3VlZF9hdCI6IjIwMTUt
MDItMjZUMDU6MzM6MjYuMDk0MTI3WiJ9fTGCAYUwggGBAgEBMFwwVzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVVuc2V0MQ4wDAYDVQQHDAVVbnNldDEOMAwGA1UECgwF
VW5zZXQxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbQIBATALBglghkgBZQMEAgEwDQYJKoZIhvcNAQEBBQAEggEAYJR+ETbjA4RpgToeRm0qh-zxRWyBL4RdN99hLHV6
foIpcr6uXMN-DaUJvGygPDi1wi-HAbpErJAe9iRHk4+8BUnX--jQRTaYhkg237eyjpYHU8Hgt8Ydn7Wdnn0hriXKt+RZBG-ZEnnP-MZ9V9GGJz-BoAMHx42uF5j6mlfV
vUxtJGSaZ2wPROkLIHAjrX-8zEo8YhtGQHi-rFvXOoP+w8TVb907R2WNsGs3LbFKRmDv-yev6pMnz+gQu8uImf2idd18hyEYdw8M9bgZc2YsGBiPSeIm-VhzH9qTX0e7
fK-chhAE+saIEbl5Mw0PzybhTyKHRzqtsW4HWFOlbE0yOA==
今回のケースでは 1,712 バイトとなっている。
一方、最小の PKIZ トークンは次のようになる。
PKIZ_eJxtVcmSozgUvPMVc6_oKMBgm0Mf2IzBCIpVlm4sNiAEtssLy9eP7K6Jqo4YboCUysyX7-nXL_ZopmV7_-gger784oBtm-8VcnYnbNePwlODQj-xb6tZ1zX_qqu
BORqx6moVreq20nAATLUyh6rygFa1F65uG0sZeE0brKqqgKLZtuHvr01pKZ8YSo3fX5scpnxmKW0x2Us4OQPae3MpKhPWnZJzdWfKxZG-fi6uTQaDxm9s2TPAgEgwe10
i-9DkPWLOfkwpIJWMYq32LId4c7LgfN2-2p1c5zBhG50aW8I5bxxlHw0N3tdDtndoISh1qdtLm9gDiJMbMOwbIDgBBlpyIEZLQII7mNuJnTrDhgH2GmN1pmgRvCRgS7k
hSO82Oa_sjrY2ObFvaYf26ZUr_2ZgYojrEo683fPX78WmhOaw82MgITHtPCvhgWjzvpW2HLBwh4nX-kYgYENtmCd3BAX63IhgeMuYkUcmB4kbHsHxgb-8wlBuC0s5c3k
fzoxafpicCcPynIvy8WVkJwu5NTA56ZQ_9Xc1X27VpTutR2AwyQTILjFFDkzSxIxZgjmZvbh4lAQ8WXyBSd9AHb2XVjrhbkNw9ATctDnzhbOb4at0Tu2RkIC4HX3DHDF
BPIYhRXG1AHNKEUEy6hAPIJhw5Cju9toUXdpzGVTue_Fp1vnOzLuy04WiG56Ap3IbDn6zfoBY5V1iz34kjR4BjL4p-AQI4JkDd4HmJ4sn2hPsB9CZ-UOLDtdIfFVoKKF
zzeBL4hm_fAELDhgVQy07TwwpjkMmg9a-0cqsTIJnPdPXDqBDC7sXSraRP-y1V4UyJo8dcObKbfuNSBIex7YErISFqlpgI-CxUdYotmcQOy0mxeiJKYuwR5-s825z416
Otjd62Hs8KyH9OoketuGE9oAl8aa8fBHT6U8Sw0cONyzu9pKV_sz90cLodxsh3wZ_BSn8imupO8o3S6_GsSkxhjyaW55jNAVECtm37AUmlQQgK6eFJCAC-T-aP-v-J-I
bAVuUf1aP--rxNklGMekrIRM290g8NxnFt6yjJOmd3qavvpiLRUrx5u_O5H62JjDMH52JJMja-hhbuooSNoEsjU0iDWyGIZ1NF6itpQqJyWk10NMUjAZR2YjyUrYKaGl
6Z6bxIJAGQ0VGGgRbQ03TvPdoaZg-UIfXZr0aNlwK5Rnvg9EyVPgHAABjUS7KSaYHa3MrrJG6nffIA1tT_2c2ckbwc6CamhaoZlWZ6s5fHiM7FSN_F4LPwIZ62eK-Ck7
bCCpG5gpWk55VZuJb-wZ30-Uwfh6c4_0Srgp12Ak0si9usTwdmuUcuHlIuqUjXarRXcN-_THIn6tdAN-nPSg57PGwD4Wt2Avm6qpmghnW1w0ZrGUX7cQ3MprKmr7nWFm
kufamysNiZfWSqNPDabMl54Q7ykPw2Gzxx1G8gzcNvGvRvTCjTLAqtQ1dZ7xM-zxbbam8Vha3SgGNhxL8-bESItc8SiF3PhHSXD4Mfztp16N2Em_F8CYqviBlaj917zP
Uwf2h-1nsiVSIpWGKeu-Gdtc6rtfD2eRWEbn5VNhNU-wivHb8i14U1yo6RNH7qf0Y4ValpVTG9nR4NMHv39zrQjM94_ty-xc2_Erg
PKI と比較して、1,637 バイトと若干小さくなっている。
今回の場合、トークンには最小限のカタログ情報だけがエンコードされているが、カタログ情報の大きい PKIZ トークンでは、PKI と比較して圧縮率がはるかに高くなる。
トークン生成フロー
TBD.
トークン検証フロー
トークン失効フロー
Fernet
Fernet トークンはトークン内にアカウント情報(ユーザID、プロジェクトID)や有効期限を保持し、共通鍵で暗号化して格納している。
そのため Keystone のデータベース問い合わせすることなくトークンの検証が可能となっており、データベース負荷を大幅に低減することが可能となる。
Fornet トークンでは、memcached の使用が推奨されている。
トークンを定期的に削除する cron ジョブは不要となる。
Newton 以降、Keystone のデフォルトのトークンドライバが “UUID” から “fernet” に変更となる。
ペイロード: Fernet トークン
Fernet トークンは、最小の識別情報と動的な認証コンテキストを優先する非常に軽いトークン。
書式: Fernet
Keystone の Fernet トークン形式は、暗号化認証の機構である Fernet がベースとなっている。
Fernet は、もともと Heroku が API トークンで使用するために作成された安全なメッセージング形式で、共通鍵暗号化を実装したもの。
共通鍵暗号とは、暗号用と復号用に同じ鍵を用いる方式で、ファイル暗号などによく使われ、処理が早いのが特徴。 共通鍵暗号では、暗号化されたデータを復号化する前に鍵を渡す必要がある。
Fernet 認証方式では、共通鍵の一覧を取得し、一覧内にある最初の鍵を使用してすべての暗号化を実行し、その一覧からすべての鍵を使用して復号化を試みるといった複数の鍵にも対応している。
Fernet 鍵は base64 エンコーディング(base64.urlsafe_b64encode)で、次の項目の組み合わせで構成されている。
項目 | データ長 | 値 | 備考 |
---|---|---|---|
Fernet 形式のバージョン番号 (Version) | 8ビット | 0x80 | |
現在のタイムスタンプ (Timestamp) | 64ビット | int(time.time()) | |
初期化ベクトル (IV) | 128ビット | os.urandom(16) | - 署名鍵はfernet鍵の最初の16バイト - 暗号化キーは fernet キーの最後の16バイト |
暗号文 (Ciphertext) | 可変長(128 ビットの倍数) | 以下のデータにより構成されている。 - バージョン: 0(スコープなしのペイロード) 1(ドメインスコープのペイロード) 2(プロジェクトスコープのペイロード) 3(トラストスコープのペイロード) - ユーザID: バイト形式 - メソッド: 整数形式 - プロジェクトID: バイト形式 - 有効期限: タイムスタンプの整数形式 - 監査ID: バイト形式 |
|
HMAC | 256 ビット |
Fernet は、トークンの作成時間をトークンから把握することもできる。
>>> token = b'gAAAAABb93HyEo0JIFZlTfKHlyRFTiJPqlBK75MEt_858fnATWN3mRNomlNQr-ZjHwnmlzcXKKZYpuGSmc8UgMwwEhCvWk5PsCiAxV-GsVDhpYcduZVK6ugtLTVkGgZZiEBC3-77Jkpi8VA2qouzyWzDbBgjMO98YuQkjEH6kPAKApGYrSGnFEw='
>>>
>>> import struct, base64, datetime
>>> timestamp = struct.unpack('>Q', base64.urlsafe_b64decode(token)[1:9])[0]
>>>
>>> timestamp
1542943218
>>> datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
'2018-11-23 12:20:18'
トークン生成フロー
Fernet トークンのバージョン
Fernet 形式のバージョン。現在は “0x80” で固定。
>>> version=b'\x80'
現在時刻のタイムスタンプ
現在時刻を取得する。
>>> import time
>>> time.time()
1542943218.944182
現在時刻を int 型へ変換する。
>>> current_time=int(time.time())
>>> current_time
1542943218
IV
初期化ベクトルを作成する。
>>> import os
>>> iv = os.urandom(16)
>>> iv
b'\x12\x8d\t VeM\xf2\x87\x97$EN"O\xaa'
暗号文
>>> from cryptography.hazmat.primitives import hashes, padding
>>> from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
>>> from cryptography.hazmat.backends import default_backend
>>>
>>> padder = padding.PKCS7(algorithms.AES.block_size).padder()
padder にブロックサイズ 128 が設定される。
>>> padder.__dict__
{'_buffer': b'', 'block_size': 128}
プロジェクトスコープ付きペイロードを定義する。
>>> project_scoped_payload = b'\x96\x02\xb0\x134\xf3\xed~\xb2H;\x91\xb8\x19+\xa0C\xb5\x80\x02\xb0B=E\xcd\xde\xc8Ap\xbe6^\x0b1\xa1\xb1_\xcbA\xd5\x87P\x02\xb4C\xd9\x91\xb0}oA\xd3fCu\x95z\\\xbd\xd8{\x89\xbc'
ブロックサイズに合わせてパディングを適用する。
>>> padded_data = padder.update(project_scoped_payload) + padder.finalize()
>>> padded_data
b'\x96\x02\xb0\x134\xf3\xed~\xb2H;\x91\xb8\x19+\xa0C\xb5\x80\x02\xb0B=E\xcd\xde\xc8Ap\xbe6^\x0b1\xa1\xb1_\xcbA\xd5\x87P\x02\xb4C\xd9\x91\xb0}oA\xd3fCu\x95z\\\xbd\xd8{\x89\xbc\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10'
32バイトのランダム文字列バイナリ形式の fernet 鍵を定義する。
>>> b_key = os.urandom(32)
暗号化キーは fernet キーの最後の16バイトとなる。
>>> backend = default_backend()
>>> e_key = b_key[16:]
CBC モードで ABC 暗号化を行う。
>>> encryptor = Cipher(algorithms.AES(e_key), modes.CBC(iv), backend).encryptor()
パディングされた Keystone ペイロードを暗号化する。
>>> ciphertext = encryptor.update(padded_data) + encryptor.finalize()
>>> ciphertext
b'PJ\xef\x93\x04\xb7\xff9\xf1\xf9\xc0Mcw\x99\x13h\x9aSP\xaf\xe6c\x1f\t\xe6\x977\x17(\xa6X\xa6\xe1\x92\x99\xcf\x14\x80\xcc0\x12\x10\xafZNO\xb0(\x80\xc5_\x86\xb1P\xe1\xa5\x87\x1d\xb9\x95J\xea\xe8--5d\x1a\x06Y\x88@B\xdf\xee\xfbJb'
HMAC
署名鍵付き 256 ビット SHA256 HMAC。 上記のすべての項目(バージョン、タイムスタンプ、IV、暗号文)を結合したもの。
署名鍵はfernet鍵の最初の16バイトとなる。
>>> s_key = b_key[:16]
HMAC の作成では SHA256 ハッシュを使用する。
>>> from cryptography.hazmat.primitives.hmac import HMAC
>>> h = HMAC(s_key, hashes.SHA256(), backend)
HMAC は Fernet Version、Timestamp、IV、Ciphertext の組み合わせで構成される。
>>> import struct
>>>
>>> basic_parts = (version+ struct.pack(">Q", current_time) + iv + ciphertext)
>>> basic_parts
b'\x80\x00\x00\x00\x00[\xf7q\xf2\x12\x8d\t VeM\xf2\x87\x97$EN"O\xaaPJ\xef\x93\x04\xb7\xff9\xf1\xf9\xc0Mcw\x99\x13h\x9aSP\xaf\xe6c\x1f\t\xe6\x977\x17(\xa6X\xa6\xe1\x92\x99\xcf\x14\x80\xcc0\x12\x10\xafZNO\xb0(\x80\xc5_\x86\xb1P\xe1\xa5\x87\x1d\xb9\x95J\xea\xe8--5d\x1a\x06Y\x88@B\xdf\xee\xfbJb'
“>” はビッグエンディアン、“Q” は符号なし long long 型を表す。
Fernet Version、Timestamp、IV、Ciphertext から HMAC を生成する。
>>> h.update(basic_parts)
>>> hmac = h.finalize()
>>> hmac
b'\xf1P6\xaa\x8b\xb3\xc9l\xc3l\x18#0\xef|b\xe4$\x8cA\xfa\x90\xf0\n\x02\x91\x98\xad!\xa7\x14L'
Fernet トークンは、Version、Timestamp、IV、Ciphertext、HMAC の Base64 URL-Safe エンコーディングとなる。
>>> import base64
>>>
>>> fernet = base64.urlsafe_b64encode(basic_parts + hmac).decode('utf-8')
>>> fernet
'gAAAAABb93HyEo0JIFZlTfKHlyRFTiJPqlBK75MEt_858fnATWN3mRNomlNQr-ZjHwnmlzcXKKZYpuGSmc8UgMwwEhCvWk5PsCiAxV-GsVDhpYcduZVK6ugtLTVkGgZZiEBC3-77Jkpi8VA2qouzyWzDbBgjMO98YuQkjEH6kPAKApGYrSGnFEw='
トークン検証フロー
トークン失効フロー
Keystone における Fernet トークン設定
Fernet トークンを使用するには、keystone.conf の [fernet_token] セクション項目、および [token] セクションの provider 項目を設定する。
[token]
provider = keystone.token.providers.fernet.Provider
[fernet_tokens]
# Fernet 鍵を保存する鍵リポジトリの場所
key_repository = /etc/keystone/fernet-keys/
# 鍵リポジトリ上の最大の鍵の数(初期値: 3)
# - コントローラノードの台数以下を設定する。
max_active_keys = 5
Fernet 鍵は、デフォルトで “/etc/keystone/fernet-keys/” にある鍵リポジトリに保存されている。
Fernet 鍵ファイルは 0 から始まる integer 形式の名前となる。
$ ls /etc/keystone/fernet-keys
0 1 2 3 4
以下の 3 つのタイプの鍵ファイルが存在する。
- プライマリ鍵 - プライマリ鍵は、fernet トークンの暗号化および復号化に使用される。最も大きいインデックス名となっているものがプライマリ鍵ファイルとなる。
- セカンダリ鍵 - セカンダリ鍵は、fernet トークンの復号にのみ使用される。 セカンダリ鍵ファイルの名前は、最大のインデックスより小さく、最小のインデックス(0)よりも大きい値となる。
- ステージ鍵 - ステージング鍵は、fernet トークンの復号にのみ使用されるセカンダリ鍵と似ているが、最小のインデックス番号(0)となっているものがステージング鍵となる。
Fernet 鍵は、以下のフローで遷移する。
- 新規作成 → ステージ鍵 → プライマリ鍵 → セカンダリ鍵 → 削除
Fernet 鍵 | インデックス番号 (max_active_keys=3 の場合) |
暗号化 | 複合化 |
---|---|---|---|
プライマリ鍵 | 2 | ○ | ○ |
セカンダリ鍵 | 1 | - | ○ |
ステージ鍵 | 0 | - | ○ |
要約すると、fernet トークンはプライマリ鍵を使用して暗号化され、鍵リポジトリからの fernet キーの一覧を使用して復号化される。
Fernet 鍵のローテーション手順
keystone-manage コマンドを使用し、鍵リポジトリに2つの鍵を新規作成する fernet-setup を行う。
セカンダリ鍵はなく、プライマリ鍵 1 と、ステージ鍵 0 とで構成される。
$ keystone-manage fernet_setup
2507 INFO keystone.token.providers.fernet.utils [-] [fernet_tokens] key_repository does not appear to exist; attempting to create it
2507 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2507 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 1 key files: ['/etc/keystone/fernet-keys/0']
2507 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 0
2507 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 1
2507 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 1
2507 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2507 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: []
$ ls /etc/keystone/fernet-keys/
0 1
ローテーションの状態を確認しながら、ローテーションを行う。
$ keystone-manage fernet_rotate
2528 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 2 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1']
2528 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 1
2528 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 2
2528 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 2
2528 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2528 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: []
$ ls /etc/keystone/fernet-keys/
0 1 2
- ローテーションした後、2 がプライマリ鍵となり、0 および 1 は復号化で使用される。
- 前のプライマリ鍵 1 は、同じ名前のままセカンダリ鍵となる。
- 新しいステージ鍵 0 が導入される。
再度 Fernet 鍵のローテーションを行う。
$ keystone-manage fernet_rotate
2698 INFO keystone.token.providers.fernet.utils [-] Starting key rotation with 3 key files: ['/etc/keystone/fernet-keys/0', '/etc/keystone/fernet-keys/1', '/etc/keystone/fernet-keys/2']
2698 INFO keystone.token.providers.fernet.utils [-] Current primary key is: 2
2698 INFO keystone.token.providers.fernet.utils [-] Next primary key will be: 3
2698 INFO keystone.token.providers.fernet.utils [-] Promoted key 0 to be the primary: 3
2698 INFO keystone.token.providers.fernet.utils [-] Created a new key: /etc/keystone/fernet-keys/0
2698 INFO keystone.token.providers.fernet.utils [-] Excess keys to purge: [1]
$ ls /etc/keystone/fernet-keys/
0 2 3
今度は、最大のインデックスは 3 となり、ステージ鍵 0 は、鍵 3 のプライマリ鍵となる。
前のプライマリ鍵 2 は、同じファイル名のままセカンダリ鍵となり、前のセカンダリ鍵は、同じファイル名のままセカンダリ鍵として残る。そして、新たにステージ鍵 0 が導入される。
max_active_keys を 3 に設定した場合、セカンダリ鍵 1 は今回の鍵のローテーション中に削除される。
想定通り、プライマリ鍵が 3 となり、鍵 1 が削除されている。
今回の場合、max_active_keys=3 で設定しているため、有効な鍵は 3 つとなっており、最も小さいインデックスの鍵が削除される。
鍵 2 と鍵 3 がプライマリ鍵のときに暗号化されたものは、引き続き検証可能な状態だが、削除済みである鍵 1 で暗号化されたものは検証することができない。
鍵のローテーションを頻繁に行ったり、max_active_keys の値が小さすぎたりした場合、有効なトークンが早期に消滅することがあり得るため、注意が必要となる。
たとえば、プライマリ鍵を 30 分ごとにローテーションするようにし、Keystone トークンの有効期間を 6 時間に設定している場合、max_active_keys には 12 以上の値を設定する必要がある。
max_active_keys 設定値 |
性能比較
トークンの作成速度は、PKI/PKIZ が一番遅く、次に UUID が続き、Fernet が一番早い。(2015年6月時点)
UUID は、Keystone で使用可能な最もシンプルなトークンフォーマットで、ランダムに生成された 32 文字の文字列を使用して Keystone に接続し、トークン検証を行う。
この時、Keystone でトークンが有効か無効化かを判断するために、トークンとユーザー ID、認証メタデータのマッピング情報を保持している必要がある。
PKI トークンは、所有者の ID とトークン認証コンテキストをトークンと共にパックし、CMS を使用してパッケージ全体をラップする。
PKIZ トークンは、端的に言うと PKI トークンを圧縮を追加したもの。
近年では、AES-CBC を使用した暗号化、および SHA256 を使用した署名を行う Fernet トークンを使用することができるようになっている。
Fernet トークンには、所有者 ID や認証コンテキストに関するメタデータも含まれている。
Keystone で行う処理の内、性能が大きく影響するものとしては、主にトークン作成およびトークン検証が挙げられる。
その他の処理については、頻繁に行われるものでもなく、性能はさほど問題にはならない。
Keystone トークンを複数の地域にまたがった同期機能は、機能要求としてあるのが一般的となっている。
これは、2 つ以上の地域にそれぞれ Keystone インスタンスを立て、ある地域で生成されたトークンを他の地域で検証し、正常に使用できるようにするといったケースである。
UUID トークンを使用する場合には、可能な限り迅速にすべての地域にトークンを同期させる必要があり、複雑となる傾向がある。
この場合、クライアントからのトークンをすべての場所に複製するまで保留するか、ローカルで作成して非同期で複製するかの 2 つの方法が考えられる。
しかし、1つ目の方法では、トークンの作成時に応答時間が長くなり、2つ目の方法では、トークン発行が競合状態になる可能性がある。
つまり、どちらの方法を採択するにせよ応答時間の短縮が必須となってくる。
一方、Fernet トークンは永続化する必要がないため、token テーブルは空となり、すべての地域と同期を取る必要がない。
> SELECT * FROM `token`;
Empty set (0.00 sec)
世界中に点在するう5つのデータセンター(ワシントンDC、シカゴ [イリノイ州]、ダラス [テキサス州]、香港、シドニー [オーストラリア])で Fernet 検証結果では、応答時間は1つの地域で行なった場合と変わらないにも関わらず、すべての地域で即座にトークンが使用可能となる。
待機時間や同期にかかる時間は一切なく、応答時間には同期レプリケーションのオーバーヘッドが発生しないため、すべての地域でトークンの暗号鍵をすぐに確認することができるようになる。
※ 今回、検証ケースを絞るため Fernet トークンは常に255バイト未満で設定。
Keystone + グローバルネットワークでクラスタ化された Galera Cluster 構成
これらのメトリックは、公開されたインターネット上でレプリケーションを実行するグローバルに分散された5台のGalera クラスタノードの場合の検証結果を示している。
トークン作成の性能
トークン | 応答時間 | リクエスト数/秒 |
---|---|---|
UUID | 342.4 | 166.9 |
PKI | 351.4 | 110.2 |
PKIZ | 339.7 | 120.7 |
Fernet | 50.8 | 237.1 |
トークン検証の性能
トークン | 応答時間 | リクエスト数/秒 |
---|---|---|
UUID | 6.02 | 1715.7 |
PKI | 6.25 | 1717.2 |
PKIZ | 6.15 | 1676.4 |
Fernet | 5.55 | 1957.8 |
※1 ApacheBench には最大要求サイズがあり、PKI または PKIZ トークンの ( X-Auth-Token + X-Subject-Token) がこれを超える。そのため、Stack Overflow での提案に沿って ApacheBench 最大要求サイズを広げた後、再コンパイルすることでこの制限を回避している。