XEY LABS — MYTHIC / 002 EN/ ID/ 中文/ 日本語/ 한국어 ← BUILD LOG プロジェクト REPO ↗ GITHUB ↗

Mythicのハードニング。

1週間、私たちは Mythic の構築を止めて、壊すことに専念した —— 稼働中のサーバーに対する ブラックボックス、ブラックボックスで足りないところはホワイトボックスで。 12件の発見が生まれた。すべて修正し、回帰テストで固定し、完了と呼ぶ前に 元の再現手順で再検証した。本エントリでは、同じパスで着地したM1 ロードマップの2項目 —— 鍵ローテーションとRedisストア —— も記録する。

ダウンタイムなしの鍵ローテーション

今週までMythicはすべての決定トークンを単一のEd25519鍵で署名して いた。ローテーションは再起動を意味し、生きている全トークンを 無効化した。サーバーは現在、アクティブな署名者を正確に1つ持つ キーリングを保持する (ADR-0006)。 ローテーションは時間ベースでプロセス内で動く (MYTHIC_KEY_MAX_AGE、既定720h);引退した鍵は MYTHIC_KEY_RETENTION(既定24h)の間JWKSに残る —— その鍵が署名したトークンの寿命より意図的に長く、飛行中のトークンが ローテーション途中で壊れることは決してない:

キーストアはアトミックに書き込まれるJSONファイルである;旧来の 32バイトシードファイルも読み込み可能で、最初の保存時に移行する。 オリジンは引き続きトークンをローカルで検証し、 kid クレームで鍵を選択する —— 調整のための呼び出しは不要。

単一ノードを超えて生き残るストア

インメモリストアはmythicdを単一ノードに縛り付けていた (ADR-0009)。 MYTHIC_STORE=redisでRedisバックエンドを選択する: 使い捨てチャレンジはGETDEL(アトミックな取得と削除)、 プレッシャーカウンタは1つのLuaスクリプトで刈り込むソート済みセット。 両バックエンドはスライディングウィンドウカウンタを実装し、 固定ウィンドウの境界バーストを塞ぐ。 Storeインターフェースはエラーを返すようになった: 到達不能なRedisは起動を拒否し、実行時の障害は/v1で 503 store_unavailableとして表出する一方、 /healthzはロードバランサ向けに開いたままになる。 共有の契約スイートが両バックエンド(memoryとminiredis)に対して走り、 セマンティクスが乖離できないようにする。

明記しておくべき注意点が1つ:共有されるのはストアであって、 署名鍵ではない。ノードAのトークンはノードBのJWKSでは検証できない。 マルチノードのトークン検証には共有キーストア(KMS/HSM)が必要 —— M5であり、この変更では解決していない。

12件の発見、グループ別

各行:攻撃が示したこと、次に変えたこと。すべての修正に回帰テストがあり、 すべての修正は元の再現手順で再検証した。

領域原因修正
トランスポートと制限
レート制限バジェット バジェットは検証の後で消費された —— 不正JSONや未知のサイトキーの洪水がスロットルなしで通った(600リクエスト、225 req/s、429ゼロ)。 バジェットは検証の前にカウントされるようになった。
IPv6プレッシャーキー リミッターはIPv6を/128でキー化 —— 1つの/64が2⁶⁴のIDを支配した(単一ブロックから5,000リクエスト、ヒットゼロ)。 カウンタはIPv6を/64に集約する(ADR-0007)。
接続タイムアウト body/read/idle タイムアウトがなかった —— 遅いクライアントが接続を無期限に保持した。 ReadTimeout、WriteTimeout、IdleTimeout を設定した。
チャレンジの経済性
ソルブ時間フロア フロアは50k H/sのワーカーをモデル化;実測の crypto.subtle ワーカーは約400k H/s —— 誠実なクライアントが非現実的と判定された。 フロアは誠実なソルブ時間の低い分位数になった(2^d / (2M × 32)):最悪でも誤検出 ≤ ~6%、GPU級の回答は約96%で引っかかる(ADR-0008)。
検証の遅延 クライアントは検証を壁時計フロアの後に遅らせるだけでよい —— フロアは忍耐を捕まえられない。 脅威モデルに行動層(M2)の仕事として文書化した。フロアの仕事ではない。
トークンと設定
トークンTTL “トークンはチャレンジより長生きしない”という文書化された不変条件は強制されていなかった —— 5秒のチャレンジから1時間のトークンが発行された。 TokenTTLはChallengeTTLにクランプされるようになった。
スコアクランプ 敵意あるチューニング(MYTHIC_FAST_SOLVE_PENALTY=500)が署名済みトークンにリスクスコア500を漏らした。 スコアはエンジン境界で0–100にクランプ;100超のペナルティは起動を拒否する。
設定の検証 負の MYTHIC_BASE_DIFFICULTY は任意のnonceで通るチャレンジを発行した。 設定は起動時に検証され、範囲外の値は提供を拒否する。
Base64正規形 Go の base64 デコーダは末尾文字の未使用下位ビットを黙って捨てるため、異なるトークン文字列が同じバイト列にデコードされて検証を通過した —— 文字列レベルのトークン同一性が壊れていた。改ざん耐性ミューテーションスイートの初回実行で検出。 VerifyToken は正規 base64 を強制する:デコードしたバイト列の再エンコードが入力と完全一致しなければトークンを拒否する。
状態管理
キーストアの並行性 1つのキーストアを共有する2プロセスは、同じテンポラリファイルで衝突するか、署名鍵を黙ってスプリットブレインした。 テンポラリ名はプロセスごとに一意;保存はロード後に変更されたファイルへの上書きを拒否する。
JSON末尾バイト JSON値の後の末尾バイトは黙って無視された。 末尾バイトは拒否されるようになった。
カウンタの飽和 スライディングウィンドウカウンタはヒットごと(レート制限済みも含む)に1エントリ保存した —— 1つのIDからの百万ヒットで約16.4 MBのヒープと595秒のCPUを消費した。 カウンタは IPLimit+1 で飽和する:同じ洪水でも +0.00 MB、714 ms しかかからない。

スコアクランプの修正は見せる価値がある。信頼境界に位置するからだ —— トークンの中に署名されて入る値:

// The score is a 0-100 contract (token claim `risk`,
          // dashboards, tuning docs). Hostile tuning must clamp
          // here, not leak 500 into tokens (red-team H3).
          if score > 100 {
              score = 100
          }
          if score < 0 {
              score = 0
          }

検証 —— そして残された課題

すべての発見に再現手順があり、すべての修正に回帰テストがある (go test ./... -race、加えてSDKのタイプチェックとテスト)。 そしてすべての修正は元の再現手順で再検証した。未解決のまま残るものは、 私たちの頭の中ではなく脅威モデルに文書化されている:検証を遅らせる 辛抱強いソルバー(M2、行動シグナル)、新しいIDを使った分散攻撃 (その後M1のネットワークフィンガープリントで部分的に対処 —— 003参照)、ノードごとのキーストア (M5、共有KMS/HSM)。

— XEY LABS