【実務・中級編】 JWTの秘密鍵漏洩と鍵ローテーション戦略 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

JWTの秘密鍵が漏洩したその瞬間、システムは「乗っ取られた」も同然だ

現場で数多のインシデントを見てきたが、開発者が最も過小評価しているのが「JWTの秘密鍵の重要性」だ。

JWT(JSON Web Token)は便利だ。ステートレスでスケーラブル。だが、ひとたび署名鍵が漏洩すれば、攻撃者はサーバーの許可なく「管理者権限」を捏造し、あらゆるユーザーになりすますことができる。しかも、そのトークンは「正当な署名」を持っているため、バックエンドは疑いようがない。

今日は、そんな悪夢を現実のものにしないための「鍵のローテーション」と「JTI(JWT ID)による無効化」の実践的な話をしよう。

—

1. 鍵漏洩が引き起こす「絶望的シナリオ」

攻撃者はどうやって鍵を盗むか? GitHubにプッシュされた .env ファイル、設定管理ツール(AWS Parameter Store等)のIAM権限設定ミス、あるいはログ出力への混入だ。

一度秘密鍵が盗まれると、攻撃者は以下のペイロードを署名して送り込む。

{
  "sub": "1234567890",
  "name": "admin_user",
  "role": "admin",
  "iat": 1516239022
}

このトークンは完璧だ。有効期限内であれば、あなたのシステムはこれを「信頼できる管理者」として受け入れてしまう。鍵が変わらない限り、攻撃を止める術はない。

—

2. 鍵ローテーションの実装戦略:鍵ID (kid) の活用

鍵を更新した途端に全ユーザーがログアウトされる…そんな運用ではビジネスが止まる。ここで使うのが kid (Key ID) ヘッダーだ。複数の鍵を保持し、トークンが「どの鍵で署名されたか」を識別させる。

Pythonによる実装サンプル (PyJWT)

鍵をローテーションさせるためには、アプリケーション側で「現在の鍵」と「一つ前の鍵」の両方を保持するロジックが必要だ。

import jwt

# 鍵管理のイメージ:DBや環境変数から取得する
# 本来はAWS KMSやVault等で管理すべき
KEYS = {
    "key-2023-10": "secret_key_v1",
    "key-2023-11": "secret_key_v2" # 最新の鍵
}

def verify_token(token):
    # ヘッダーからkidを抽出
    header = jwt.get_unverified_header(token)
    kid = header.get("kid")
    
    if kid not in KEYS:
        raise Exception("無効な鍵IDです")
    
    # 該当する鍵で検証
    return jwt.decode(token, KEYS[kid], algorithms=["HS256"])

—

3. JTIによる動的な無効化(ブラックリスト)

鍵のローテーションだけでは、「漏洩した鍵で作られたトークン」を即座に破棄できない。ここで jti (JWT ID) を使う。各トークンに一意なIDを付与し、Redis等の高速なKVSで「使用済み、または無効化対象のID」を管理する。

Redisを使ったJTIチェック(Node.js / Expressの例)

const redis = require('redis');
const client = redis.createClient();

// ミドルウェア:トークンのJTIがブラックリストにないか確認
async function checkJti(req, res, next) {
    const jti = req.user.jti; // デコード済みトークンから取得
    
    const isBlacklisted = await client.get(`blacklist:${jti}`);
    if (isBlacklisted) {
        return res.status(401).send("このトークンは無効化されています");
    }
    next();
}

—

4. 現場で守るべき「鉄の掟」

1. 署名鍵はコードに書くな: どんなに忙しくても、ハードコーディングは厳禁だ。環境変数にすら置きたくない。AWS Secrets ManagerやHashiCorp Vaultなど、動的なシークレット管理ツールを導入しろ。
2. 鍵の寿命を短くせよ: 鍵のローテーションは「年1回」ではなく、「四半期ごと」あるいは「CI/CDパイプラインのデプロイごと」に自動化しろ。
3. JTIは必須: ログアウト機能や、怪しい挙動を検知した際の「特定のユーザーの強制ログアウト」には、JTIの管理が不可欠だ。
4. アルゴリズムの固定: 鍵漏洩時に「Noneアルゴリズム攻撃」を受けないよう、バックエンドでは algorithms=['HS256'] のように、使用するアルゴリズムを厳格に指定(固定)すること。

—

最後に:セキュリティは「動的」なものだ

セキュリティ担当者として、完璧な防御など存在しないと知っている。だが、「漏洩したときのリカバリ速度」こそが、そのシステムの信頼性を決める。

鍵が漏れたら、即座に新しい鍵で署名を開始し、古い鍵を検証対象から外す。そして必要であれば、Redisに記録されたJTIを使って、怪しいトークンを瞬時に無効化する。この「自動化されたリカバリフロー」を構築しているチームだけが、真の安全を手にできる。

次にコードを書くとき、jwt.sign の横に、この「ローテーションとJTI」の必要性を思い出してほしい。それが君のシステムを守る最後の砦になる。

コメント

タイトルとURLをコピーしました