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

Envoyの内部構造

認可サーバー二つと Envoy 二台

TT Labで続きを見る

目標

HTTP・gRPCの2つの方式の認可サーバーを自分でPythonで書き、それぞれに尋ねるEnvoyを起動して、ヘッダーがどこへ流れるかと、認可サーバーが死んだときに何が起きるかを測ります。

なぜ重要なのか

認可をサービスごとにコードに入れると、サービスの数だけ別々の認可が生まれます。判断を1か所に集めると、今度はその1か所がすべてのリクエストの経路上に置かれます。そのサーバーが死んだときに遮断するか通すかを決めておかないと、障害のときに可用性とセキュリティのどちらかを、知らないうちに失います。IstioのCUSTOM認可が裏で使うのもこのフィルターなので、ここで見たヘッダーの規則と失敗時の動作が、メッシュでもそのまま現れます。

ステップ

  1. /root/envd-authz/authz_http.pyに、127.0.0.1:9191で待ち受ける認可サーバーを書き、バックグラウンドで起動してください。Authorization: Bearer alice-tokenならユーザーalice、bob-tokenならユーザーbobと見なして、200と、応答ヘッダーx-auth-user: <사용자>(プレースホルダーはユーザー名です)を返し、トークンがないか知らない値なら、403と、応答ヘッダーx-deny-reason: missing-or-unknown-tokenを返します。リクエストごとに/root/envd-authz/authz-http.logに、JSONを1行(path・user・tenant(x-tenantヘッダーの値)・allowed)追記します。
  2. アップストリームpython3 /opt/lab/envoy/echo.py 8091(受け取ったリクエストをJSONで返す)を起動し、/root/envd-authz/envoy-http.yamlに管理ポート9981、リスナー127.0.0.1:10081の設定を書いて、起動してください(--base-id 11)。HTTPフィルターチェーンの一番前にenvoy.filters.http.ext_authzを置き、stat_prefix: http_authz、http_serviceでクラスターauthz_http(127.0.0.1:9191)に尋ねるようにします。認可サーバーが返したx-auth-userはアップストリームへ、x-deny-reasonは拒否応答と一緒にクライアントへ渡し、failure_mode_allowはfalseです。
  3. リスナー10081に3つのリクエストを送り、結果を/root/envd-authz/03-http.txtに4行で書いてください。トークンなしで送ったリクエストのステータスコードno_token=、その応答のx-deny-reasonの値deny_reason=、bob-tokenと、偽造したヘッダーx-auth-user: malloryを一緒に送ったときに、アップストリームが受け取ったx-auth-userの値spoof_upstream=、そしてalice-tokenにx-tenant: acmeを付けたリクエストが認可サーバーのログに残したtenantの値http_saw_tenant=(値がなければnone)です。
  4. /root/envd-authz/authz_grpc.pyに、envoy.service.auth.v3.Authorization/Checkを実装して、127.0.0.1:9192で起動してください(/opt/xds/bin/python3で実行します。grpcioとEnvoyのprotobufが入った仮想環境です)。判断はHTTP版と同じですが、パスが/publicで始まるなら、トークンなしで許可し、このときのユーザーはanonymousです。許可ならOkHttpResponseにヘッダーx-auth-userを、拒否ならDeniedHttpResponseにステータス403・ヘッダーx-deny-reason: grpc-missing-or-unknown-token・本文を入れます。リクエストごとに/root/envd-authz/authz-grpc.logに、同じ形のJSONを1行残します。
  5. /root/envd-authz/envoy-grpc.yamlに、管理ポート9982、リスナー127.0.0.1:10082の2台目のEnvoyを書いて、起動してください(--base-id 12)。ext_authzはstat_prefix: grpc_authz、grpc_serviceのenvoy_grpcでクラスターauthz_grpc(127.0.0.1:9192、HTTP/2)を呼び出し、failure_mode_allow: trueとfailure_mode_allow_header_add: trueを有効にします。1台目のEnvoyはそのままにします。
  6. /root/envd-authz/envoy-grpc.yamlに、パスがちょうど/healthzのルートを一番前に追加して、本文okで200を直接返し(direct_response)、そのルートでだけext_authzをオフにしてください(typed_per_filter_configのExtAuthzPerRouteでdisabled: true)。そのEnvoyを起動し直してから、トークンなしで/healthzを3回呼んでください。
  7. 2つの認可サーバーをどちらも止めたまま、1台目のEnvoyにはalice-tokenを付けたリクエストを、2台目のEnvoyにはトークンなしの/ordersリクエストを送り、/root/envd-authz/07-failure.txtにhttp_on_error=(1台目のEnvoyのステータスコード)、grpc_on_error=(2台目のステータスコード)、grpc_failure_header=(アップストリームが受け取ったx-envoy-auth-failure-mode-allowedの値)の3行を書いてください。書いた後で、2つの認可サーバーを起動し直します。
  8. /root/envd-authz/08-report.mdに、次の6行を書いてください。fail_closed_code=・fail_open_code=(ステップ7の2つのコード)、spoofed_header_reached_upstream=(偽造したmalloryがアップストリームに届いたならyes)、http_authz_saw_tenant=・grpc_authz_saw_tenant=(各認可サーバーのログにx-tenantの値が記録されていたならyes)、healthz_asked_authz=(gRPC認可サーバーのログに/healthzがあればyes)です。その下に学んだことを4行以上書いてください。gRPC側の値を確認するには、2台目のEnvoyにalice-tokenとx-tenant: acmeを付けたリクエストを1回送っておいてください。

参考

HTTP認可サーバーを起動する

/root/envd-authz/authz_http.pyに、127.0.0.1:9191で待ち受ける認可サーバーを書き、バックグラウンドで起動してください。Authorization: Bearer alice-tokenならユーザーalice、bob-tokenならユーザーbobと見なして、200と、応答ヘッダーx-auth-user: <사용자>(プレースホルダーはユーザー名です)を返し、トークンがないか知らない値なら、403と、応答ヘッダーx-deny-reason: missing-or-unknown-tokenを返します。リクエストごとに/root/envd-authz/authz-http.logに、JSONを1行(path・user・tenant(x-tenantヘッダーの値)・allowed)追記します。

ext_authzのHTTP方式で、認可サーバーはごく普通のWebサーバーです。Envoyが元のリクエストのメソッド・パスでもう一度リクエストを送り、2xxなら許可、それ以外なら拒否と読みます。そのため、GETだけでなく、すべてのメソッドに同じ判断をする必要があります。標準ライブラリのhttp.serverで十分で、起動するときは、setsid --fork nohup python3 … > 로그 2>&1 </dev/null(プレースホルダーはログファイルです)でシェルから切り離してください。採点ツールは、このサーバーに直接トークン3つ(alice・bob・なし)を送ってみます。

Envoyがリクエストごとに認可サーバーへ尋ねるようにする

アップストリームpython3 /opt/lab/envoy/echo.py 8091(受け取ったリクエストをJSONで返す)を起動し、/root/envd-authz/envoy-http.yamlに管理ポート9981、リスナー127.0.0.1:10081の設定を書いて、起動してください(--base-id 11)。HTTPフィルターチェーンの一番前にenvoy.filters.http.ext_authzを置き、stat_prefix: http_authz、http_serviceでクラスターauthz_http(127.0.0.1:9191)に尋ねるようにします。認可サーバーが返したx-auth-userはアップストリームへ、x-deny-reasonは拒否応答と一緒にクライアントへ渡し、failure_mode_allowはfalseです。

フィルターの順序が、そのまま処理の順序です。ext_authzがrouterより前にあって初めて、拒否されたリクエストがアップストリームに届きません。認可サーバーの応答ヘッダーをどこへ渡すかは、authorization_responseの2つのリストが決めます。allowed_upstream_headersは許可のときにアップストリームのリクエストに、allowed_client_headersは拒否のときにクライアントへの応答に付きます。このPodではEnvoyを複数起動するので、--base-idを互いに異なる値にし、止めるときはcurl -X POST localhost:9981/quitquitquitを使ってください。

何が認可サーバーへ、何がアップストリームへ行ったか

リスナー10081に3つのリクエストを送り、結果を/root/envd-authz/03-http.txtに4行で書いてください。トークンなしで送ったリクエストのステータスコードno_token=、その応答のx-deny-reasonの値deny_reason=、bob-tokenと、偽造したヘッダーx-auth-user: malloryを一緒に送ったときに、アップストリームが受け取ったx-auth-userの値spoof_upstream=、そしてalice-tokenにx-tenant: acmeを付けたリクエストが認可サーバーのログに残したtenantの値http_saw_tenant=(値がなければnone)です。

アップストリームが何を受け取ったかは、echoが返すJSONのheadersにあります。認可サーバーが何を受け取ったかは、自分で残したログにあります。HTTP方式の認可リクエストには、Host・Method・Path・Content-Length・Authorizationだけがデフォルトで載り、ほかのヘッダーはallowed_headersに書かないと渡りません。反対方向(認可サーバー→アップストリーム)では、allowed_upstream_headersは同じ名前のヘッダーを上書きします。クライアントが身元ヘッダーを偽造しても、アップストリームには認可サーバーが決めた値が届くという意味です。

gRPC認可サーバーを起動する

/root/envd-authz/authz_grpc.pyに、envoy.service.auth.v3.Authorization/Checkを実装して、127.0.0.1:9192で起動してください(/opt/xds/bin/python3で実行します。grpcioとEnvoyのprotobufが入った仮想環境です)。判断はHTTP版と同じですが、パスが/publicで始まるなら、トークンなしで許可し、このときのユーザーはanonymousです。許可ならOkHttpResponseにヘッダーx-auth-userを、拒否ならDeniedHttpResponseにステータス403・ヘッダーx-deny-reason: grpc-missing-or-unknown-token・本文を入れます。リクエストごとに/root/envd-authz/authz-grpc.logに、同じ形のJSONを1行残します。

gRPC方式では、Envoyはリクエストを送り直さず、リクエストの属性をCheckRequestで渡します。ヘッダーはrequest.attributes.request.http.headers(小文字の名前のマップ)、パスは.pathにあります。判定はCheckResponse.status.codeが決めます(google.rpc.code_pb2.OKなら許可)。モジュールのパスはenvoy.service.auth.v3.external_auth_pb2・_pb2_grpcです。採点ツールは、このサーバーに直接Checkを3回送ります。

2台目のEnvoyはgRPCで尋ね、認可サーバーがなければ通す

/root/envd-authz/envoy-grpc.yamlに、管理ポート9982、リスナー127.0.0.1:10082の2台目のEnvoyを書いて、起動してください(--base-id 12)。ext_authzはstat_prefix: grpc_authz、grpc_serviceのenvoy_grpcでクラスターauthz_grpc(127.0.0.1:9192、HTTP/2)を呼び出し、failure_mode_allow: trueとfailure_mode_allow_header_add: trueを有効にします。1台目のEnvoyはそのままにします。

gRPCは、HTTP/2の上でしか動きません。クラスターでHTTP/2を有効にしないと、EnvoyはHTTP/1.1で接続しようとして失敗し、その失敗は「認可サーバーのエラー」として数えられます。このEnvoyは失敗したら通すように設定したので、すべてのリクエストが通過してしまい、設定が間違っていることに気づきにくくなります。クラスターのtyped_extension_protocol_optionsに、HttpProtocolOptionsのexplicit_http_config.http2_protocol_optionsを入れてください。

ヘルスチェックのパスは認可サーバーに尋ねない

/root/envd-authz/envoy-grpc.yamlに、パスがちょうど/healthzのルートを一番前に追加して、本文okで200を直接返し(direct_response)、そのルートでだけext_authzをオフにしてください(typed_per_filter_configのExtAuthzPerRouteでdisabled: true)。そのEnvoyを起動し直してから、トークンなしで/healthzを3回呼んでください。

ロードバランサーのヘルスチェックは、トークンを知りません。認可をすべてのパスに掛けると、ヘルスチェックが403を受け取り、ロードバランサーは正常なプロキシを外してしまいます。フィルターをオフにするのは、フィルターの設定ではなく、ルート側の役割です。同じフィルターがパスごとに違う動作をするようにする仕組みが、typed_per_filter_configです。オフにしたパスは、認可サーバーにまったく行かないので、認可サーバーのログに/healthzが残らないはずです。

認可サーバーが死んだら、遮断する側と通す側

2つの認可サーバーをどちらも止めたまま、1台目のEnvoyにはalice-tokenを付けたリクエストを、2台目のEnvoyにはトークンなしの/ordersリクエストを送り、/root/envd-authz/07-failure.txtにhttp_on_error=(1台目のEnvoyのステータスコード)、grpc_on_error=(2台目のステータスコード)、grpc_failure_header=(アップストリームが受け取ったx-envoy-auth-failure-mode-allowedの値)の3行を書いてください。書いた後で、2つの認可サーバーを起動し直します。

認可サーバーが応答しないとき、Envoyは設定に従って、2つの道のどちらかを選びます。遮断する側(fail closed)はstatus_on_error(デフォルト403)を返し、通す側(fail open)は、リクエストをそのまま送り、望むなら目印のヘッダーを付けます。どちらの場合も、統計のext_authz.<stat_prefix>.errorが上がり、通したならfailure_mode_allowedも上がります。サーバーを止めるときにpkill -f authz_http.pyのように書くと、その文字列を含むシェルも一緒に終了することがあるので、pkill -f '[a]uthz_http.py'のように、最初の文字を角括弧で囲んでください。

どちらへ失敗させるかを決めた根拠を残す

/root/envd-authz/08-report.mdに、次の6行を書いてください。fail_closed_code=・fail_open_code=(ステップ7の2つのコード)、spoofed_header_reached_upstream=(偽造したmalloryがアップストリームに届いたならyes)、http_authz_saw_tenant=・grpc_authz_saw_tenant=(各認可サーバーのログにx-tenantの値が記録されていたならyes)、healthz_asked_authz=(gRPC認可サーバーのログに/healthzがあればyes)です。その下に学んだことを4行以上書いてください。gRPC側の値を確認するには、2台目のEnvoyにalice-tokenとx-tenant: acmeを付けたリクエストを1回送っておいてください。

値は、記憶ではなく、証拠のファイルと2つの認可サーバーのログから移してください。説明の行には、「認可サーバーが死んだとき、遮断する側を選ぶと何を失い、通す側を選ぶと何を失うか」を、自分の言葉で書いておいてください。採点ツールは、同じログとファイルを読んで照合します。