代表的な面接トピック

バックエンド面接:認証の停止を引き起こさずにJWT署名鍵をローテーションするにはどうすればよいか?

バックエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

ある認証サービスはRS256でアクセストークンに署名しています。トークンは最大15分間受け入れられる可能性があり、200のバックエンドサービスはJWKSを10分間キャッシュしてローカルで検証します。認証のダウンタイムを発生させない計画的な鍵ローテーションを設計し、未知のkid、JWKSの障害、および鍵の漏洩に対応してください。

プロンプトと適用されるコンテキスト

ある認証サービスは RS256 を使用してアクセストークンに署名します。トークンは、1分のクロックスキューを含めて最大15分間受け入れられる可能性があります。200のバックエンドサービスが、IssuerのOpenID Discoveryドキュメントから固定の jwks_uri を読み取り、JWKSを10分間キャッシュして、トークンをローカルで検証します。

有効なリクエストに対して 401 レスポンスの急増を引き起こさない署名鍵のローテーションを設計してください。以下のケースを網羅してください:

  • 通常のローテーション中に新旧両方のトークンが到着する;
  • トークンヘッダーにローカルキャッシュに存在しない kid が含まれている;
  • 多数のランダムな kid 値がJWKSリフレッシュストームを引き起こそうとする;
  • ローテーション中にJWKSエンドポイントがタイムアウトまたは失敗する;
  • 現在の秘密鍵が漏洩した可能性があり、緊急対応が必要である;
  • オペレーターは、古い鍵を廃止する前にすべてのVerifierが新しい鍵を受け入れていることを証明しなければならない。

15分の有効期間、10分のキャッシュ、1分のスキュー、および200のサービスはシナリオ上の制約であり、普遍的な推奨事項ではありません。中核となるスキルは、バックエンド認証プロトコル設計、キャッシュ整合性、障害セマンティクス、およびセキュリティ運用であるため、カテゴリは backend です。

面接官が評価するポイント

第一に、候補者が安全な順序を提示できるか:新しい公開鍵を公開し、Verifierがそれを認識できるようにし、新しい秘密鍵での署名を開始し、古いトークンが期限切れになるのを待ち、その後でのみ古い公開鍵を削除するという手順です。最初に署名者を切り替えると、古いJWKSキャッシュを持つサービスが新しいトークンを拒否することが確実になります。

第二に、JWKSが公開鍵のセットであることを理解しているか? RFC 7517は keys 配列を定義しており、kid は信頼できるセットから鍵を選択するだけにすぎません。秘密鍵をJWKSで公開してはなりません。また、kid 自体も信頼の証明にはならず、信頼できるIssuer、固定されたアルゴリズム、および署名検証の成功にバインドされている必要があります。

第三に、キャッシュとセキュリティのバランスを取ることができるか? リクエストごとにJWKSを取得するとIssuerに過大な負荷がかかり、無期限にキャッシュすると新しい鍵の受け入れや古い鍵の廃止が遅れます。未知の kid は1回の制御されたリフレッシュをトリガーできますが、同時リフレッシュは合流(coalesce)させ、レート制限をかけ、識別子が存在しないことが確認された場合は短いネガティブキャッシュを適用する必要があります。

第四に、計画的なローテーションと秘密鍵の漏洩を区別しているか? 計画的なローテーションでは、可用性のために新旧の公開鍵を重複させます。漏洩発生時に古い鍵を信頼し続けると、攻撃者がトークンを発行できてしまいます。緊急パスには、明示的なキャッシュ無効化、失効制御、およびより強固なセキュリティ優先度が必要です。

第五に、署名チェックを完全な検証に拡張しているか? Verifierは許可されたアルゴリズムを固定し、署名、issaudexp、および nbf を検証します。トークンからの信頼できない jku や任意の鍵URLには従いません。これらに従うと、アルゴリズム混乱攻撃やSSRFを許す可能性があります。

回答前に確認すべき明確化のための質問

  • トークンの最大受け入れ時間はどれくらいか? 名目上設定されたTTLだけでなく、受け入れられる可能性のあるすべての古いトークンの有効期間を使用し、クロックスキューを含めます。
  • JWKSの最大陳腐化期間(staleness)はどれくらいか? ブラウザ、CDN、プロキシ、インプロセスキャッシュがそれぞれ層を追加する可能性があるため、ローテーションには実際の上限値が必要です。
  • すべてのVerifierをプロアクティブにリフレッシュできるか? 設定のプッシュ、バージョン確認応答、またはカナリアプローブにより、自然な期限切れを待つだけの状態を置き換えることができます。
  • JWKSが利用できない場合はどうなるか? 既知の鍵が境界付きの古いキャッシュ(bounded stale cache)を使用できるかどうか、および未知の鍵がフェイルクローズするかどうかを定義します。
  • 発行済みのトークンを直ちに失効させる必要があるか? 自己完結型のJWTは、ローテーションだけではトークン単位の正確な失効をサポートできません。拒否リスト、トークンバージョン、またはイントロスペクションが必要になる場合があります。
  • IssuerとVerifierを管理しているのは誰か? 1つの組織であればリフレッシュの確認応答を収集できますが、サードパーティのVerifierは通常、文書化された互換性ウィンドウに依存します。
  • 秘密鍵はどこに保持されているか? KMS、HSM、または制限された署名サービス内で生成および使用します。公開プレーンに属するのは公開データのみです。
  • 成功の定義は何か? 計画的ローテーションは、有効な新旧トークンの動作を維持します。緊急ローテーションでは、漏洩した鍵の信頼を迅速に停止するために、制御された再認証を受け入れる場合があります。

30秒の回答フレームワーク

「私はローテーションを『公開(publish)』、『ウォーム(warm)』、『切り替え(switch)』、『オーバーラップ(overlap)』、『廃止(retire)』に分割します。新しい kid を持つ鍵を生成し、JWKSに新旧両方の公開鍵を公開します。最大10分のキャッシュウィンドウを待つか、200すべてのVerifierをプロアクティブにリフレッシュして新しいJWKSバージョンを収集します。新しい公開鍵が検証可能になった後にのみ、Issuerは新しい秘密鍵での署名を開始します。

古い公開鍵は、最後の古いトークンが発行されてから15分に1分のクロックスキューを加えた時間まで保持します。Verifierは信頼できるIssuerのセット内でのみ kid を選択し、RS256 を固定し、署名、issaudexp、および nbf を検証します。未知の kid は、合流およびレート制限された1回のリフレッシュをトリガーします。それでも存在しない場合は拒否します。JWKSの障害中、既知の鍵は明示的に制限された古いキャッシュを使用できますが、未知の鍵はフェイルクローズします。

秘密鍵が漏洩した場合、古い鍵での発行を停止し、新しい鍵を公開して切り替え、キャッシュ無効化をブロードキャストし、古い鍵で署名されたトークンを失効させます。通常のオーバーラップ期間は使用しません。完了の証明は、kid 別の結果、キャッシュの経過時間、JWKS取得ボリューム、および古い kid の最終観測によって行います。」

ステップバイステップの詳細解説

ステップ 1:信頼のルートと検証不変条件を固定する

Verifierは、設定された信頼できるIssuerから開始し、そのOpenID Discoveryドキュメントを読み取り、そのドキュメントのHTTPS jwks_uri を使用します。トークンヘッダーの kid は、この信頼できるJWKSから候補となる公開鍵を選択するだけです。トークンが jku を介してVerifierをリダイレクトすることはできず、アプリケーションは kid をファイル、データベース、またはURLの検索に直接連結してはなりません。

すべての検証において、以下の不変条件を維持します:

  1. 設定された RS256 のみを受け入れる(トークンがアルゴリズムを交渉することはできない);
  2. kid が信頼できるIssuerのJWKS内の1つの署名鍵と一意に一致することを要求する;
  3. 署名検証後、期待される iss、このAPIの audexp、および nbf を要求する;
  4. いずれかのチェックが失敗した場合はトークン全体を拒否する;
  5. 公開鍵データのみを公開し、秘密鍵は管理された署名境界内に留める。

ローテーション中の最小限の鍵セットは次のようになります:

json
{
  "keys": [
    { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "2026-07-a", "n": "...", "e": "AQAB" },
    { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "2026-07-b", "n": "...", "e": "AQAB" }
  ]
}

配列の順序は優先順位ではありません。Verifierは完全一致する kid を選択します。キャッシュは同じ識別子の背後に隠された変更された鍵データを区別できないため、新しい世代には再利用されない新しい kid が割り当てられます。

ステップ 2:署名前の公開により計画的ローテーションを実行する

計画的ローテーションを明示的な状態としてモデル化します:

text
GENERATED
  -> PUBLISHED(old + new)
  -> VERIFIER_READY
  -> SIGNING_WITH_NEW
  -> OLD_TOKEN_DRAINED
  -> OLD_KEY_RETIRED

プロトコルは以下の通りです:

  1. 管理された鍵システムで 2026-07-b を生成するが、まだそれを使用して署名しない;
  2. 2026-07-a2026-07-b を含むJWKSを、新しいETagまたはセットバージョンとともに公開する;
  3. 最大10分のキャッシュ陳腐化ウィンドウを待つか、200すべてのVerifierをリフレッシュして新しい kid を認識したという確認応答を収集する;
  4. 各環境でカナリアトークンを使用して署名、Issuer、Audience、および時間のクレームを検証する;
  5. Verifierが新しい公開鍵を受け入れた後にのみ、2026-07-b での署名を開始する;
  6. 古い鍵による最後のトークンの発行時刻である T_last_old を記録する;
  7. T_last_old + 15 minutes + 1 minute より前ではなく、古い kid のトラフィックが予想通りに減少した後、古い公開鍵をJWKSから削除する;
  8. 異常な古い kid トラフィックの監視を継続し、その後古い秘密データを無効化して破棄する。

「署名前の公開(publish before sign)」は新しいトークンを保護します。「古い署名の停止、古いトークンの排出、その後の削除」は古いトークンを保護します。キャッシュの伝播が署名切り替えの最短タイミングを制御し、古いトークンの受け入れ有効期間とクロックスキューが公開鍵削除の最短タイミングを制御します。

ステップ 3:1回の制御されたリフレッシュで未知のkidを処理する

未知の kid は、正当な新世代の鍵である場合もあれば、攻撃者が提供したノイズである場合もあります。Verifierは、初確認のたびに即座に拒否することはできず、また確認のたびに無制限のアップストリームトラフィックを発生させることもできません。適切なフローは以下の通りです:

text
verify(token):
  header = parse_bounded_header(token)
  require header.alg == "RS256"
  key = trusted_cache.find(header.kid)
  if key is missing:
    refresh trusted_issuer_jwks once through single-flight
    key = trusted_cache.find(header.kid)
  if key is missing:
    short_negative_cache.add(header.kid)
    reject "unknown kid"
  verify signature and require iss, aud, exp, nbf

1つのIssuerに対する同時ミスは、シングルフライト(single-flight)リフレッシュを共有します。リフレッシュにはグローバルなクールダウンとタイムアウトが設定されています。最新のセットによって kid が存在しないことが確認された後、短いネガティブキャッシュにより、繰り返されるランダムな識別子がIssuerに到達するのを防ぎます。ネガティブTTLは、実際のローテーションをブロックするほど長くすることはできません。また、Verifierは高カーディナリティのメモリアタックを防ぐために kid の長さとフォーマットを制限する必要があります。

リフレッシュは、設定された jwks_uri のみにTLS経由でアクセスし、レスポンスサイズ、接続、および読み取りの制限を設けます。ETag条件付きリクエストを使用してもよいでしょう。攻撃者が提供した jkux5u、または同様の場所が信頼のルートに取って代わることは決してありません。

ステップ 4:JWKS障害時の可用性境界を定義する

通常のリクエストはローカルキャッシュを使用します。JWKSエンドポイントは、各認証リクエストの同期パスには配置されません。リフレッシュが失敗した場合:

  • 一致する既知の kid は、事前定義された制限付き陳腐化ウィンドウ(bounded stale window)内であれば使用を継続できる;
  • 未知の kid は拒否されなければならず、署名なしで検証されたり無関係な鍵に対して検証されたりしてはならない;
  • 制限付き陳腐化ウィンドウの期限が切れた後、リフレッシュの失敗はアラートを発行し、セキュリティポリシー(通常は拒否)に従う;
  • Issuerは、Verifierが新しい公開鍵を持っていることを証明できない場合、新しい署名鍵に切り替えてはならない。

短い stale-if-error はコントロールプレーンの一時的な不調を吸収しますが、現在のセットから削除された鍵に対するローカルの信頼も延長してしまいます。その期間はセキュリティモデルに含まれるべきであり、漏洩時には明示的な無効化によってオーバーライドされなければなりません。可用性を無期限に古い鍵に依存させることはできません。

ステップ 5:秘密鍵の漏洩には個別の緊急パスを使用する

漏洩が発生すると、攻撃者が作成したトークンの受け入れを可能な限り迅速に停止することが目標に変わります。古い鍵での発行を停止し、新しい公開鍵を生成して公開し、Verifierに強制的にリフレッシュさせ、署名者を切り替え、古い鍵を失効済みとしてマークします。シームレスな体験のためだけに通常の16分のオーバーラップを維持してはなりません。

Verifierが10分間のキャッシュを保持し続ける可能性があるため、中央のJWKSから古い公開鍵を削除するだけでは不十分です。テスト済みのコントロールプレーンブロードキャスト、キャッシュバージョンのプッシュ、サービスの再起動、またはその他の無効化チャネルを使用します。サードパーティのVerifierに到達できない場合、Issuerはそのキャッシュの上限によって制限されるため、そのリスク露出を明示的に言及する必要があります。

また、ローテーションはすでに発行された自己完結型トークンを正確に回収することはできません。即時失効のためには、古い kid または iat のカットオフによる一時的なルールを適用するか、アクセストークンの有効期間を短縮するか、高リスクなAPIに対してイントロスペクション/セッション状態を使用します。緊急対応によってユーザーに再認証を強制する場合がありますが、セキュリティが優先される場合は許容されるビジネス影響です。

ステップ 6:バージョン、メトリクス、監査で完了を証明する

JWKSレスポンスは、観察可能なセットバージョンまたはETagを公開する必要があります。Verifierは、現在のバージョン、キャッシュの経過時間、リフレッシュ結果、および認識された kid 値を報告します。Issuerは、トークン本文や秘密データをログに記録することなく、アクティブな署名 kid を記録します。

主要なメトリクスには、Issuer、kid、およびエラー別の検証結果、未知の kid のカーディナリティ、JWKSの取得回数、レイテンシ、および失敗率、シングルフライトの合流状況、キャッシュの経過時間、新しい鍵のカナリア成功率、ならびに古い kid の最終正当使用が含まれます。切り替え後に新しい kid に対する未知の鍵エラーが増加した場合、ユーザーが問題を報告する前に署名の変更を停止またはロールバックする必要があります。

監査ログは、誰が各鍵を生成、公開、アクティブ化、および廃止したか、どのJWKSバージョンが最新であったか、どのVerifierが準備完了を確認したか、そして廃止を正当化する古いトークンの最終発行時刻は何であったかを回答します。二重承認と最小権限の原則により、鍵のライフサイクル運用の安全性が高まります。

ステップ 7:遷移と敵対的入力をテストする

単一の静的な有効なトークンのみをテストしてはなりません。少なくとも以下を網羅してください:

  1. 古い公開鍵のみが公開されている場合、古いトークンは合格し、新しい鍵のトークンは失敗する;
  2. オーバーラップ中、両方の世代のトークンが合格し、繰り返しのチェックによってJWKSが再取得されない;
  3. 古い鍵のみがキャッシュされている場合、新しい kid は正確に1回の制御されたリフレッシュの後に合格する;
  4. 多数の同一またはランダムな未知の kid 値が、制限されたリフレッシュを引き起こし拒否される;
  5. JWKSのタイムアウト、500エラー、過大なレスポンス、および無効なJSONが発生しても既知の鍵のポリシーは維持され、未知の鍵は失敗する;
  6. 誤ったアルゴリズム、issaud、期限切れトークン、およびまだ有効でないトークンがすべて失敗する;
  7. 攻撃者が提供した jku によってVerifierが異なる場所に接続することはない;
  8. 古い鍵の削除後、新しいプロセスは古いトークンを拒否し、古いキャッシュは指定されたウィンドウ内でのみ受け入れる;
  9. 漏洩訓練により、管理下にあるすべてのVerifierで古い kid が無効化される;
  10. 署名ゲートにより、Verifierの準備が整っていない間は新しい鍵のアクティブ化が防止される。

受け入れテストでは、認可結果とJWKSリクエスト数の両方をチェックします。すべてのリクエストがIssuerに到達している状態で合格するトークンは、正しい実装ではありません。有効な新しいトークンが拒否されている状態での通常のリクエストボリュームも同様に正しくありません。

高品質な回答例

「私は kid を、信頼の源ではなく、信頼できるIssuerのJWKS内のセレクターとして定義します。各Verifierは設定された RS256 のみを受け入れ、固定されたDiscovery jwks_uri から公開鍵を取得し、kid で選択し、署名、issaudexp、および nbf を検証します。JWKSには公開鍵のみが含まれ、秘密鍵はKMSまたはHSMの署名境界内に留まります。

計画的ローテーションの場合、新しい kid を持つ鍵を生成し、新旧両方の公開鍵を公開してETagを更新します。その後、シナリオの最大キャッシュウィンドウである10分間待つか、200すべてのVerifierをプロアクティブにリフレッシュして準備完了状態を収集します。署名者が切り替わる前に、カナリアトークンによって新しい鍵が機能することを証明します。

最後の古いトークンの発行時刻を記録します。古い公開鍵は、少なくとも15分の受け入れ有効期間に1分のクロックスキューを加えた時間保持され、古い kid のトラフィックが完全に排出された後にのみ削除します。両方の遷移ゲートは明示的です。新しい公開鍵が伝播するまで新しいトークンに署名せず、古いトークンが無効になるまで古い公開鍵を削除しません。

キャッシュミスが発生した場合、各Issuerはタイムアウト、クールダウン、およびETagを備えた1回のシングルフライトリフレッシュを実行します。鍵が存在しないままである場合は、拒否して短時間ネガティブキャッシュします。ランダムな kid 値がアップストリームへのフラッドに増幅されることはありません。一時的なJWKS障害の間、既知の鍵は明示的に設定された制限付き陳腐化ウィンドウ内でのみ継続できますが、未知の鍵は常にフェイルクローズします。

古い秘密鍵が漏洩した場合、古い署名を停止し、新しい鍵を公開して切り替え、キャッシュ無効化をブロードキャストし、古い kid または古い iat の範囲のトークンを失効させます。攻撃者が署名を行っている可能性があるため、通常のオーバーラップは維持しません。最後に、JWKSバージョン、キャッシュの経過時間、kid 別の検証エラー、新しい鍵のカナリア、および古い kid の最終観測によって完了を証明し、遷移とIssuerの障害を定期的にリハーサルします。」

よくある間違い

  • 公開鍵を公開する前に署名者を切り替える → 古いJWKSを持つサービスが新しいトークンを拒否する → 公開し、伝播を証明してから新しい署名を開始する。
  • 新しい鍵のみを公開する → 有効な古いトークンが直ちに失敗する → 受け入れウィンドウ中は両方の世代を公開する。
  • 任意の時間を待つ → 実際のキャッシュやトークンの有効期間よりも短い可能性がある → 最大陳腐化期間、最後の古い発行、トークン有効期間、およびクロックスキューからゲートを導出する。
  • リクエストごとにJWKSをダウンロードする → Issuerの障害によってすべての認証が停止し、負荷が増幅する → ローカルキャッシュ、条件付きリフレッシュ、および制限付き陳腐化ポリシーを使用する。
  • 未知のkidごとにリフレッシュする → ランダムな識別子によってリフレッシュストームが発生する → シングルフライト、レート制限、ネガティブキャッシュ、および入力制限を使用する。
  • 未知のkidに対してすべての鍵を試す → 鍵の選択が曖昧になり、攻撃対象領域が広がる → 1回リフレッシュし、完全一致しない場合は拒否する。
  • トークンからのalgやjkuを信頼する → アルゴリズム混乱攻撃やSSRFにつながる可能性がある → Verifierの設定でアルゴリズムとJWKSの場所を固定する。
  • 署名のみをチェックする → 別のIssuer、Audience、または時間のトークンが受け入れられる可能性がある → issaudexp、および nbf も検証する。
  • 新しい鍵データでkidを再利用する → キャッシュは1つの識別子が変更されたことを認識できない → すべての鍵世代に新しい識別子を割り当てる。
  • 中央のJWKS削除を即時失効として扱う → Verifierが古いキャッシュを保持している可能性がある → 明示的なキャッシュ無効化とトークン失効を提供する。
  • 漏洩後も通常のオーバーラップを維持する → 攻撃者はそのウィンドウ全体を通じてトークンを発行できる → 緊急パスを使用し、必要な再認証を受け入れる。

フォローアップの質問と回答

フォローアップ 1:古い公開鍵は正確にどれくらいの期間保持すべきか?

最後の古い鍵によるトークンの発行時刻から起算して、最低でも「最大受け入れ有効期間 + クロックスキュー」の間保持します。このシナリオでは 15 + 1 = 16 分です。キューの遅延、オフライン発行、またはより長い暗黙の受け入れウィンドウが存在する場合は、それを加算します。設定とランタイムの実態を一致させるため、削除前に古い kid のトラフィックを観察してください。

フォローアップ 2:未知のkidに対してすぐに401を返してはいけない理由は?

正当なローテーションの後、そのプロセスの自然なキャッシュリフレッシュの前に新しいトークンが到着する可能性があります。1回の制御されたリフレッシュによってそのギャップを埋めることができます。これは合流させ、レート制限をかけ、信頼できるURLに制限する必要があります。最新のセットに依然として kid が欠落している場合にのみ拒否することで、互換性と増幅攻撃への耐性のバランスを取ります。

フォローアップ 3:JWKSがダウンしている場合、検証はフェイルオープンすべきか?

署名検証を決してスキップしてはなりません。キャッシュ内で既に信頼されている一致する鍵は、明示的な制限付き陳腐化ウィンドウ内であれば完全な検証を完了できます。未知の鍵やそのウィンドウ後のリクエストは拒否します。これにより、測定可能な信頼限界を維持しながら、データプレーンの個々のリクエストからコントロールプレーンの一時的な停止を切り離すことができます。

フォローアップ 4:漏洩した鍵を削除した後でも古いトークンが機能してしまうのはなぜか?

Verifierがまだ10分前の古いキャッシュを持っている可能性があり、自己完結型JWTは中央機関に問い合わせないためです。緊急パスでは、プロアクティブにキャッシュを無効化し、古い kid、発行時刻、またはトークンバージョンによる一時的な拒否ルールを適用する必要があります。高リスクなシステムでは、より迅速な失効のためにイントロスペクションまたはサーバーサイドセッションを使用できます。

フォローアップ 5:ローテーション中のリフレッシュストームをどのように回避するか?

事前公開により、ほとんどのプロセスが自然なリフレッシュ中に鍵を取得できるようになります。また、プロアクティブなリフレッシュはジッター(ゆらぎ)を持たせて分散させることができます。実際のミスに対しては、Issuerごとに1つのシングルフライトを使用し、クールダウン、ETag、タイムアウト、および短いネガティブキャッシュを組み合わせます。JWKSのリクエスト量と未知の kid のカーディナリティを監視し、やみくもにリトライするのではなく異常に対してレート制限をかけます。

フォローアップ 6:署名ゲートをどのように自動化できるか?

各JWKSにバージョンを割り当てます。Verifierは、読み込まれたバージョンと認識された kid を定期的に報告します。リリースコントローラーは、秘密鍵をアクティブ化する前に、目標とする準備完了レベルと重要なパスでのカナリアトークンの成功を要求します。新しい kid の検証失敗が増加した場合は、署名を一時停止またはロールバックします。新しい公開鍵を公開したままにしておいても、古いトークンに害はありません。

フォローアップ 7:サードパーティのVerifierが準備完了を確認応答できない場合はどうするか?

安定したオーバーラップ契約を公開します。新しい公開鍵を少なくとも最大キャッシュ期間の1回分前に公開し、すべての古いトークンが期限切れになるまで古い鍵を保持し、適切なキャッシングヘッダーを送信します。Issuerはサードパーティにリフレッシュを強制できないため、互換性ウィンドウ、変更通知、およびカナリアチェックが内部的な確認応答の代わりとなります。緊急の漏洩時には、依然として避けられないリスク露出期間が残る可能性があります。

公開情報ソース

関連する質問