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

Envoyの内部構造

認可サーバーが落ちたら止めるか通すか

TT Labで続きを見る

一言でいうと

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つの認可サーバーを止めて、遮断する側と通す側が、実際にどんな応答を返すかを測ります。