認可サーバーが落ちたら止めるか通すか
一言でいうと
ext_authzは、リクエストごとに、プロキシの外にある認可サーバーへ問い合わせて、「このリクエストを通してよいか」を尋ねるHTTPフィルターです。尋ね方は2種類(HTTP・gRPC)で、認可サーバーが答えられないときに遮断するか通すかは、設定1行(failure_mode_allow)が決めます。
なぜ必要なのか
ローカルレート制限や障害注入は、Envoyが設定だけを見て判断できます。認可はそうではありません。「このトークンの持ち主がこの注文を見られるか」は、ユーザーDB・権限表・組織ポリシーを知っている側だけが答えられ、そのような判断をサービスごとにコードに入れると、サービスの数だけ別々の認可が生まれます。1つのサービスだけ規則の修正が遅れても、そのサービスが穴になります。
そこで、判断を1か所(認可サーバー)に集め、プロキシがリクエストを保持したまま、そのサーバーに尋ねる構造が生まれました。OPAをEnvoyの隣に付ける方式も、社内の認可サービスを置く方式も、すべてこのフィルターを使います。IstioのAuthorizationPolicyでaction: CUSTOMとproviderを使うと、istiodはメッシュ設定(extensionProvidersのenvoyExtAuthzHttp・envoyExtAuthzGrpc)を見て、サイドカーにまさにこのフィルターを入れます。
どう動くのか
HTTP方式: Envoyが、元のリクエストのメソッド・パスで認可サーバーにリクエストをもう1回送ります。本文は空にして(Content-Length: 0)、ヘッダーは選んで載せます。デフォルトで載るのはHost・Method・Path・Content-Length・Authorizationだけで、残りはallowed_headersに書かないと渡りません。認可サーバーが2xxを返せば許可で、それ以外なら、そのステータスコードと本文がそのままクライアントへ行きます。
gRPC方式: リクエストを送り直さず、リクエストの属性(送信元・宛先・ヘッダー・パス・TLS情報)をCheckRequestというprotobufで渡します。サービス名はenvoy.service.auth.v3.Authorization、メソッドはCheckです。こちらはallowed_headersを空にしておくと、すべてのヘッダーが渡ります。同じフィルターなのに、2つの方式のデフォルトが逆なのは、古い動作を守るための互換性のためだと、ドキュメントが明記しています。認可サーバーを変えながら方式を変えると、ポリシーが頼っていたヘッダーが、静かに消えることがあります。
ヘッダーは両方向に流れます。
| 設定 | いつ | どこへ |
|---|---|---|
allowed_upstream_headers |
許可 | アップストリームのリクエストに付ける(同じ名前は上書きします) |
allowed_client_headers |
拒否 | クライアントへの応答に付ける |
gRPC OkHttpResponse.headers |
許可 | アップストリームのリクエストに付ける |
上書きするという性質が重要です。認可サーバーがx-user: aliceを決めて渡すと、クライアントが同じヘッダーにadminと書いて送っても、アップストリームにはaliceが届きます。アップストリームがそのヘッダーを信じて権限を判断するなら、この性質が偽造を防ぐ最後の壁です。
認可サーバーが答えられないとき。接続の失敗、タイムアウト、5xxがここに入ります。
failure_mode_allow: false status_on_error(기본 403)를 돌려준다 ← 막는다(fail closed)
failure_mode_allow: true 요청을 그대로 보낸다 ← 흘린다(fail open)
+ failure_mode_allow_header_add: true 업스트림에 x-envoy-auth-failure-mode-allowed: true
統計は、http.<HCM stat_prefix>.ext_authz.<필터 stat_prefix>.(プレースホルダーはHCMのstat_prefixとフィルターのstat_prefixです)の下に、ok・denied・error・failure_mode_allowedとして積まれます。通す側を選んだなら、failure_mode_allowedが上がる瞬間が、そのまま認可なしでリクエストが通過した瞬間です。
パスごとにオフにできます。ルートのtyped_per_filter_configにExtAuthzPerRouteを入れてdisabled: trueにすると、そのパスは認可サーバーにまったく尋ねません。ヘルスチェックや静的ファイルのように、トークンのないリクエストに使います。
現場での姿
「認可サーバーをデプロイしている間、全サービスが403でした」。遮断する側の代償です。認可サーバーがすべてのリクエストの経路上にあるので、その可用性がそのままサービスの可用性になります。認可サーバーをプロキシの隣にサイドカーとして付けたり(OPAの一般的な配置)、複数台置いて短いtimeoutを設定したりする理由です。
「セキュリティ点検で、認可がオフだった時間が見つかりました」。通す側の代償です。認可サーバーが死んだその数分間、すべてのリクエストが通過したのに、目印のヘッダーも統計のアラートもなければ、その事実は後でログを調べて初めて明らかになります。
「gRPCに変えたら、すべてのリクエストが通ります」。認可クラスターでHTTP/2を有効にしておらず、毎回接続が失敗しているのに、通す側の設定なので静かに通過した場合です。統計のerrorが、リクエスト数だけ上がっているはずです。
公式ドキュメント: External Authorization filter・ext_authz API・Authorization service API・Istio External Authorization
次のラボですること
認可サーバーをHTTP版とgRPC版で1つずつ自分で書き、それぞれに尋ねるEnvoyを2台起動します。偽造した身元ヘッダーがアップストリームでどう変わるか、2つの方式が認可サーバーに渡すヘッダーがどう違うかを、サーバーのログで確認します。ヘルスチェックのパスだけ認可をオフにし、最後に2つの認可サーバーを止めて、遮断する側と通す側が、実際にどんな応答を返すかを測ります。