JWTの要塞化:API本番環境での6ヶ月間から得た実践的レッスン

Security tutorial - IT technology blog
Security tutorial - IT technology blog

現場でのJWT:なぜ基本的な実装では不十分なのか

昨年、私のチームはコアAPIインフラをステートレスなマイクロサービスアーキテクチャに移行しました。JSON Web Token (JWT) を選んだのは、容易にスケールでき、リクエストごとの高コストなデータベース検索を排除できるという一般的な理由からです。しかし、6ヶ月間の本番環境での運用を経て、「標準的」なセットアップは攻撃者にとって格好の標的になりやすいことが分かりました。

私の個人サーバーが深夜にSSHブルートフォース攻撃を受け、1時間足らずで1,200回以上のログイン失敗が記録されたとき、セキュリティは私にとって切実な問題となりました。その一件以来、私は単なる機能性から、あらゆるレイヤーでの積極的な要塞化(Hardening)へと注力するようになりました。JWTに関して、多くの開発者は設定を「一度設定すれば終わり」のものと考えがちです。しかし現実には、要塞化されていないJWTは、マットの下に鍵を置いているようなものではありません。デバッガーを持つすべての人に対して、攻撃面を公開しているのと同義なのです。

強固な基盤の構築

信頼できるセキュリティは、検証済みの依存関係から始まります。私たちはPythonサービスには PyJWT を、Node.jsには jsonwebtoken を使用しています。パースロジックを「自作」しようとする誘惑に負けてはいけません。暗号技術は、わずかな見落としが致命的な情報漏洩につながる分野です。

Python環境では、仮想環境内にセットアップを隔離し、暗号化がサポートされたバージョンがインストールされていることを確認します。:

bash
pip install "PyJWT[crypto]"

Node.jsのマイクロサービスでは、標準的なインストールを行います。:

bash
npm install jsonwebtoken

ライブラリをインストールするのは、あくまで第一歩です。私たちは pip-auditnpm audit をCI/CDパイプラインに直接組み込んでいます。これにより、脆弱性(CVE)がコンテナに到達する前に検知できます。コアライブラリに脆弱性があれば、どんなにエレガントなコードを書いてもデータは守れません。

一般的な設定ミスの排除

多くのJWTの侵害は、本番環境への移行時にうっかり残ってしまった「一時的」な設定選択が原因で起こります。私たちは、デフォルトで現実世界の脅威に対応できるよう、設定を再構築しました。

1. 「None」アルゴリズムの排除

悪名高い「none」アルゴリズムの脆弱性を突くと、攻撃者はヘッダーを {"alg": "none"} に設定することで署名をバイパスできてしまいます。バックエンドでこれを明示的に制限していない場合、署名のないトークンを有効なものとして扱ってしまう可能性があります。私たちは現在、このすり替えを防ぐために、検証ロジックで許可するアルゴリズムを直接ハードコードしています。

python
import jwt

# 'none' や 'HS256' へのダウングレード攻撃を防ぐため、明示的に RS256 を定義する
try:
    payload = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])
except jwt.InvalidTokenError:
    # ログを記録し、リクエストを破棄する
    pass

2. 非対称アルゴリズム RS256 への移行

当初は対称鍵のHS256を使用していましたが、これではすべてのマイクロサービスが同じシークレットキーを共有する必要がありました。これは大きなリスク(爆発半径)を生みます。もし一つのサービスが侵害されれば、攻撃者はエコシステム全体のトークンを偽造できてしまうからです。そこで私たちは、署名には秘密鍵(認証サービスのみに保存)を使い、検証には他の12のサービスで公開鍵を使うRS256へ移行しました。

bash
# 2048ビットの秘密鍵を生成
openssl genrsa -out private.pem 2048

# 各マイクロサービスに配布するための公開鍵を抽出
openssl rsa -in private.pem -pubout -out public.pem

3. ペイロードクレームの最適化

本番環境のトークンでは、iss (発行者)、exp (有効期限)、iat (発行時刻)、jti (JWT ID) の4つの必須クレームを使用しています。有効期限は厳格に15分に設定しています。有効期限の長いトークンは大きなリスクです。ユーザーのログイン状態を維持するために、これらの短寿命のJWTと併せて、新しいアクセストークンが要求されるたびにローテーションされる、セキュアなHTTP-onlyのリフレッシュトークンを使用しています。

能動的防御:モニタリングと失効処理

検証はコードをデプロイして終わりではありません。私たちはログを活用し、本格的な侵害に発展する前に悪意のあるパターンを特定しています。最近の監査では、特定の異常値を監視することで、クレデンシャルスタッフィング攻撃を数分以内にブロックすることができました。

署名の異常を検知する

ダッシュボードで Signature Verification Failed(署名検証失敗)エラーを追跡しています。少数の失敗は正常(タブの期限切れやクライアント側のバグ)ですが、単一のIPから1分間に50回以上の失敗があるような急増は、自動的に一時的なブロックをトリガーします。これは通常、誰かがアルゴリズムのすり替えをテストしているか、HS256のシークレットをブルートフォースで特定しようとしていることを示しています。

Redisによるブラックリスト管理

JWTはステートレスであるため、即時のログアウト処理が厄介です。これを解決するために、私たちは jti (JWT ID) クレームとRedisベースの失効リストを使用しています。ユーザーが「ログアウト」をクリックすると、その jti を、トークンの残りの有効寿命に合わせた有効期限(TTL)付きでRedisに保存します。これにより、リクエストごとのデータベース確認を強制することなく、トークンを即座に無効化できます。

python
def is_token_revoked(payload):
    jti = payload.get("jti")
    # Redis での O(1) ルックアップにより、認証のオーバーヘッドを 2ms 未満に抑える
    return redis_client.exists(f"revoked_token:{jti}")

結論

JWTセキュリティとは、単一の「魔法の」設定ではありません。それは多層的な防御です.RS256、厳格なアルゴリズムのホワイトリスト、そして積極的なRedisモニタリングを組み合わせることで、私たちは半年間、認証の侵害を一度も受けることなく本番APIを運用してきました。トークンを、自分のSSH秘密鍵と同じくらいの警戒心を持って扱うようにしてください。そうすれば、あなたのAPIは攻撃者にとって格段に困難なターゲットになるはずです。

Share: