TT Lab
はじめる
学ぶ 学習パス コース

セッションとトークン — ブラウザからメッシュまで

mTLS があるのになぜ JWT も検証するのか

TT Labで続きを見る

一言でいうと

サービスメッシュで「誰が要求したのか」には2つの答えがあります。どのワークロードが接続を張ったかはmTLS証明書が、どの人の代わりに要求しているかはJWTが証明します。サイドカープロキシは両方を検証できますが、2つは互いの代わりにはなりません。

なぜ必要なのか

前のモジュールまで、トークンの検証はアプリケーションコードの役目でした。サービスが20個あれば、20か所に同じ検証コードがあり、そのうち1つがaudの検査を抜かしたり、ライブラリの更新が1拍遅れたりします。メッシュはこの検証をすべてのサービスの前に立つプロキシへ移します。検証ルールが1か所の設定になり、不正なトークンはアプリケーションコードに届く前に遮断されます。

ところがメッシュを導入すると、よくこんな勘違いが生まれます。「サービス間にはすでにmTLSがかかっているから、認証は終わっている」。mTLSが証明するのは接続を張ったワークロードです。注文サービスにつながったのがフロントエンドのPodだという事実です。その要求がアリスのものかボブのものかは、証明書にはありません。Istioのセキュリティ概念のドキュメントが、認証をpeer authentication(サービス対サービス)とrequest authentication(エンドユーザー)に分けて説明している理由です。

どう動くのか

Envoyのjwt_authnフィルターは、署名・発行者・オーディエンスを検証します。公開鍵はJWK Setで受け取りますが、リモートURLの代わりに、ファイルやインライン文字列(local_jwks)で渡すこともできます。トークンヘッダーのkidで、どの鍵を使うかを選びます。鍵を入れ替えるとき、古い鍵と新しい鍵を1つのJWKSに一緒に置けるのは、このおかげです。

設定リファレンスで、このモジュールが使うフィールドは次のとおりです。

forward                 기본값 false. 검증에 성공하면 원래 토큰을 요청에서 지운다.
forward_payload_header  검증된 페이로드를 base64url(JSON) 으로 이 헤더에 실어 보낸다.
payload_in_metadata     헤더 대신 동적 메타데이터에 넣는다(RBAC 같은 다음 필터용).
rules                   경로별 요구. 처음 맞는 규칙이 이기고, requires 가 비면 검증하지 않는다.
allow_missing_or_failed 없든 틀리든 통과시킨다 — 잘못 쓰면 필터가 장식이 된다.

forwardがデフォルトでオフになっている理由が重要です。アップストリームは検証済みのクレームだけを受け取れば足り、元のトークンを受け取ったアップストリームは、そのトークンを別のサービスで再利用できてしまいます。トークンが流れる距離が短いほど、漏れる場所は少なくなります。

拒否のコードも区別されます。トークンがない、署名が違う、期限切れなら401、署名は合っているがaudがこのサービスでなければ403です。RFC 7519は、処理する側がaudに自分が含まれていなければ、そのJWTを拒否しなければならない(MUST)と書いています。

mTLS側は、リスナーのTLS設定です。require_client_certificateをオンにすると、有効なクライアント証明書のない接続を拒否します。検証済みの証明書の情報は、x-forwarded-client-cert(XFCC)ヘッダーでアップストリームに渡ります。URIキーには、証明書のURI SANが入り、メッシュではspiffe://신뢰도메인/...の形(プレースホルダーは信頼ドメインです)のSPIFFE IDが入ります。HCMのforward_client_cert_detailsはデフォルトがSANITIZEで、受け取ったXFCCを捨て、SANITIZE_SETは、mTLS接続のときに受け取ったものを捨てて自分が検証した値で新しく埋め直します。

現場での姿

最も多い事故は「ヘッダーを信じる」アップストリームです。アップストリームがx-jwt-payloadを身元として信じているのに、プロキシが検証を飛ばす公開パスで、クライアントがそのヘッダーを直接付けて送ってくると、アップストリームは偽の身元をそのまま信じてしまいます。検証するパスではEnvoyが検証済みの値で上書きしますが、公開パスは誰も手を触れません。そのため公開パスでは、そのヘッダーを明示的に削除します。

2つ目は、トークンなしのリクエストです。IstioのドキュメントはRequestAuthenticationだけを置くと、トークンなしのリクエストはデフォルトで通過すると書いており、拒否するには認可ルールを別に置くよう述べています。「トークンがあれば検査する」と「トークンがなければならない」は別の設定です。Envoyで直接組むなら、requiresを置いたルールがその違いです。

次のラボですること

cryptographyでRS256の鍵とJWKSを作り、トークンを手動で署名します。Envoyにjwt_authnをかけて、有効なトークンだけがアップストリームに届き、なし・偽造・期限切れは401、別のオーディエンスは403で、アップストリームの手前で遮断されることを確認します。検証済みのクレームをヘッダーで渡し、公開パスでは偽装されたヘッダーを削除します。最後にmTLSのチェーンを加えて、ワークロードの身元とユーザーの身元が別々に届くのを見ます。