コンテンツにスキップ

シークレットに 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 restarts
10: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_KEY
templates/app/app.env
SESSION_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 を偽造することもできなくなります。

アプリケーションは 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 B
10:05 every server uses B
tegata prune releases_read → tegata deletes token A on Cloudflare

Cloudflare は 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 key
tegata prune data_key

ここで早すぎる settle をしても、誰かがログアウトされるわけではありません。古い鍵で暗号化したままのデータが、読めなくなります。暗号化し直しが終わるまで待ってください。

ほかのサービスが、あなたのアプリケーションへのリクエストに署名するとき(Webhook のシークレット)、署名を確かめるのはアプリケーションなので、最初の場合と同じです。previous を付け、入れ替え、新しいシークレットを送る側に渡し、送る側がそれを使うようになったら settle します。

アプリケーションがその値ですること 確かめるのは previous 手順
何かに署名や暗号化をし、あとでそれを確かめたり復号したりする(session cookie、CSRF のトークン、暗号化したデータ) アプリケーション 要る rotate → render → 待つ → settle → render → prune
ほかの誰かがそれで署名したものを確かめる(Webhook のシークレット) アプリケーション 要る rotate → render → 送る側が切り替える → settle → render → prune
ほかのサービスに差し出す(API トークン、データベースのパスワード、SMTP の認証情報) ほかのサービス 要らない rotate → render → prune

値の種類ごとの、コマンドつきの手順:シークレットを入れ替える。キーそのもの:previous。

このページの問題を報告する(GitHub。英語でも日本語でも)