シークレットに previous が要るとき
tegata v0.3.1 の文書
rotate → prune で入れ替えるシークレットもあれば、previous を付けて、rotate → 待つ → settle → prune で入れ替えるシークレットもあります。どちらになるかは、1 つの問いで決まります。その値を確かめるのは誰か?
- ほかのサービスが確かめる(Cloudflare の API トークン、データベースのパスワード):古い値と新しい値は、そのサービスの上でしばらく両方とも有効にでき、
pruneで古い方を終わらせます。previousは要りません。 - 自分のアプリケーションが確かめる(セッションの cookie に署名する鍵、Webhook のシークレット、暗号化の鍵):アプリケーションが、しばらくの間は古い値も受け付けなければならないので、両方を知っている必要があります。そのための
previousで、settleで古い値を終わらせます。
このページの残りでは、例のアプリケーションを使って、両方を順に見ていきます。
アプリケーションが確かめる鍵:署名した session cookie
Section titled “アプリケーションが確かめる鍵:署名した session cookie”「hako」というアプリケーションは、利用者ごとに渡す session cookie に署名します。
session = "user=tanaka" + signature(SESSION_KEY, "user=tanaka")署名は、その cookie を hako が作ったことの証しです。利用者が user=admin に書き換えると、署名が合わなくなり、hako は受け付けません。hako は、利用者がログインしたときに署名を作り、そのあとのリクエストのたびに同じ鍵で署名を確かめます。今の鍵は A です。
previous なしでは:全員がログアウトされる
Section titled “previous なしでは:全員がログアウトされる”10:00 the key becomes B; hako restarts10:01 Tanaka, who logged in yesterday (cookie signed with A), opens a page hako checks with B → no match → "please log in again"鍵が変わった瞬間に、すべてのセッションが終わります。
previous があれば:誰も気づかない
Section titled “previous があれば:誰も気づかない”hako は、署名を SESSION_KEY で作り、SESSION_KEY と PREVIOUS_SESSION_KEY のどちらで作った署名も受け付けるように書いてあります。
resources: session_key: provider: random stores: main: - { name: SESSION_KEY, readers: [app] } previous: { name: PREVIOUS_SESSION_KEY } # read by the readers of SESSION_KEYSESSION_KEY=__SESSION_KEY__PREVIOUS_SESSION_KEY=__PREVIOUS_SESSION_KEY__入れ替えは、次のように進みます。
| いつ | コマンド | SESSION_KEY |
PREVIOUS_SESSION_KEY |
hako が受け付けるもの |
|---|---|---|---|---|
| 前 | A | A | A | |
| 10:00 | tegata rotate session_key、そのあと render |
B | A | A と B |
| 10:01 | B | A | 田中さんの A の cookie は通る。hako は B で署名した新しい cookie を渡す | |
| 10:05 | B | A | 鈴木さんがログインし、B の cookie を受け取る | |
| 7 日後 | tegata settle session_key、そのあと render |
B | B | B だけ |
tegata prune session_key |
B | B | (保存先の A の版は無効になる) |
rotateは、新しい値を作る前に、今の値(A)をpreviousに写します。rotateからsettleまでの間は、A で署名した cookie も使えます。A で署名したもののうち、いちばん長く生きるものが生きている間だけ待ちます。ここでは、cookie の有効期間の 7 日です。settleは、今の値(B)をpreviousに写します。これで両方のシークレットが B になり、A は受け付けられなくなります。pruneは、保存先の A の版を無効にします。
誰もログアウトされず、settle のあとは、盗まれた A で cookie を偽造することもできなくなります。
アプリケーションがすること
Section titled “アプリケーションがすること”アプリケーションは 2 つの鍵を持ち、1 つ目で作り、両方で確かめます。
import hashlib, hmac, os
KEYS = [os.environ["SESSION_KEY"], os.environ["PREVIOUS_SESSION_KEY"]]
def sign(data: bytes) -> str: return hmac.new(KEYS[0].encode(), data, hashlib.sha256).hexdigest()
def verify(data: bytes, signature: str) -> bool: return any( hmac.compare_digest(hmac.new(k.encode(), data, hashlib.sha256).hexdigest(), signature) for k in KEYS )多くのフレームワークは、すでにこれをしています。古い鍵の設定に PREVIOUS_SESSION_KEY を向けてください。
| フレームワーク | 今の鍵 | 古い鍵 |
|---|---|---|
| Django 4.1 以上 | SECRET_KEY |
SECRET_KEY_FALLBACKS |
| Flask 3.1.1 以上 | SECRET_KEY |
SECRET_KEY_FALLBACKS(3.1.0 は、今の鍵ではなく最後の fallback で署名していた:CVE-2025-47278) |
Express、cookie-session |
keys の 1 つ目 |
keys の残り |
ほかのサービスが確かめる値:API トークン
Section titled “ほかのサービスが確かめる値:API トークン”hako は、トークンを使って Cloudflare の API を呼びます。hako はトークンを差し出すだけで、確かめるのは Cloudflare です。
10:00 tegata rotate releases_read → tegata makes token B on Cloudflare; token A still works there render on the servers → hako restarts and uses B10:05 every server uses B tegata prune releases_read → tegata deletes token A on CloudflareCloudflare は A と B を同時に受け付けるので、hako が両方を知っている必要はありません。previous も settle も要らず、古いトークンは prune で消されたときに終わります。
データベースのパスワードも似ていますが、データベースのユーザーにはパスワードが 1 つしかないので、新しく作るとすぐに古い方が使えなくなります。tegata はその前に聞き、サーバーはすぐあとに render するべきです(すぐに置き換わる値を見てください)。それでも、パスワードを確かめるのはアプリケーションではなくデータベースなので、previous は要りません。
保存するデータを暗号化するアプリケーションは、あとでそれを復号するのも自分なので、やはり previous が要ります。手順が 1 つ増えます。古い鍵をやめる前に、古い鍵で暗号化したデータを、新しい鍵で暗号化し直さなければなりません。
tegata rotate data_key # DATA_KEY = new, PREVIOUS_DATA_KEY = old# render; the application encrypts with the new key, and decrypts with either# re-encrypt everything still encrypted with the old key (the application's own job)tegata settle data_key # only once nothing needs the old keytegata prune data_keyここで早すぎる settle をしても、誰かがログアウトされるわけではありません。古い鍵で暗号化したままのデータが、読めなくなります。暗号化し直しが終わるまで待ってください。
Webhook
Section titled “Webhook”ほかのサービスが、あなたのアプリケーションへのリクエストに署名するとき(Webhook のシークレット)、署名を確かめるのはアプリケーションなので、最初の場合と同じです。previous を付け、入れ替え、新しいシークレットを送る側に渡し、送る側がそれを使うようになったら settle します。
| アプリケーションがその値ですること | 確かめるのは | previous |
手順 |
|---|---|---|---|
| 何かに署名や暗号化をし、あとでそれを確かめたり復号したりする(session cookie、CSRF のトークン、暗号化したデータ) | アプリケーション | 要る | rotate → render → 待つ → settle → render → prune |
| ほかの誰かがそれで署名したものを確かめる(Webhook のシークレット) | アプリケーション | 要る | rotate → render → 送る側が切り替える → settle → render → prune |
| ほかのサービスに差し出す(API トークン、データベースのパスワード、SMTP の認証情報) | ほかのサービス | 要らない | rotate → render → prune |
値の種類ごとの、コマンドつきの手順:シークレットを入れ替える。キーそのもの:previous。