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」の必要性を思い出してほしい。それが君のシステムを守る最後の砦になる。
コメント