Envoy の External Authorization について

この記事は [Z Lab Advent Calendar 2020](https://qiita.com/advent-calendar/2020/zlab) の7日目の記事となります。

## はじめに

この記事では Envoy でサポートされている External Authorization を紹介していきます。ここからの情報は v1.16.0 時点での v3 API の仕様を元にまとめたものとなっています。

## External Authorization とは

[External Authorization](https://www.envoyproxy.io/docs/envoy/v1.16.0/intro/arch_overview/security/ext_authz_filter) は Envoy が受信したリクエストが認可されているかを gRPC または HTTP で外部のシステムに問い合わせて判定する機能です。これを利用することで Envoy での認可処理を外部のシステム（以下、認可サービス）に委譲することが可能になります。

External Authorization は Network Filter（L3/L4）や HTTP Filter（L7）として設定する仕様になっていて、Network Filter でリクエストが許可されていない判定がされた場合はコネクションがクローズされ、HTTP Filter でリクエストが許可されていない判定がされた場合は 403 Forbidden がレスポンスされます。

なお、External Authorization で使用する `ext_authz` フィルターはフィルターチェーンの最初のフィルターとして設定することが推奨されています。これはリクエストが許可されているかを判定した後で他のフィルターによるリクエスト処理を行うほうが効率が良いためです。

## 認可サービスが受け取る情報

以下のように Network Filter では gRPC サーバーとして稼働する認可サービスを設定でき、HTTP Filter では gRPC または HTTP サーバーとして稼働する認可サービスのどちらかを設定できる仕様になっており、認可サービスがどのように稼働しているかで受け取る情報のフォーマットが異なります。

|種別|gRPC な認可サービス|HTTP な認可サービス|
|:-:|:-:|:-:|
|Network Filter|○|✕|
|HTTP Filter|○|○|

gRPC な認可サービスが受け取る情報は、Protocol Buffers で定義されたメッセージである [service.auth.v3.CheckRequest](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/service/auth/v3/external_auth.proto#envoy-v3-api-msg-service-auth-v3-checkrequest) として定義されます。CheckRequest には、送信元の IP や X.509証明書、HTTPメソッドやHTTPヘッダーなど、受信リクエストのコンテキストが含まれています。そして、HTTP な認可サービスでは HTTP Filter での受信リクエストをそのまま受け取る仕様になっています。

認可サービスではこれらの情報を元に認可判定の処理を実装することになります。

## ext_authz フィルターの設定例

ここからの設定例は、以下のように `127.0.0.1:10003` で稼働させている gRPC な認可サービスが Cluster として定義されている前提となります。

```yaml
clusters:
  - name: ext-authz
    type: static
    http2_protocol_options: {}
    load_assignment:
      cluster_name: ext-authz
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 10003
```

### [Network Filter](https://www.envoyproxy.io/docs/envoy/v1.16.0/configuration/listeners/network_filters/ext_authz_filter)

以下が設定例です。フィルターの設定（[extensions.filters.network.ext_authz.v3.ExtAuthz](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/extensions/filters/network/ext_authz/v3/ext_authz.proto)）で認可サービスが停止している場合に受信リクエストを許可するか否かや、認可サービスへのリクエストに受信リクエストのX.509証明書を含めるかどうかなどの制御が可能です。

```yaml
filters:
  - name: envoy.filters.network.ext_authz
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.ext_authz.v3.ExtAuthz
      stat_prefix: ext_authz
      grpc_service:
        envoy_grpc:
          cluster_name: ext-authz
      failure_mode_allow: false
      include_peer_certificate: true
```

### [HTTP Filter](https://www.envoyproxy.io/docs/envoy/v1.16.0/configuration/http/http_filters/ext_authz_filter)

以下が設定例です。フィルターの設定（[extensions.filters.http.ext_authz.v3.ExtAuthz](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto)）で Network Filter と同様の制御に加えて、前述の通り gRPC ではなく HTTP な認可サービスを指定できたり、認可サービスに受信リクエストのHTTPリクエストボディを送信するかどうかなど HTTP に関する制御が可能です。

```yaml
http_filters:
  - name: envoy.filters.http.ext_authz
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
      stat_prefix: ext_authz
      grpc_service:
        envoy_grpc:
          cluster_name: ext-authz
      # Only one of grpc_service, http_service may be set.
      # http_service:
        # ...
      failure_mode_allow: false
      include_peer_certificate: true
      with_request_body:
        max_request_bytes: 8192
        allow_partial_message: true
```

HTTP Filter では [HTTP connection manager](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto) のルーティング設定で、特定パスへのリクエストにおいて External Authorization を無効にすることもできます。以下は `/static` プレフィックスを持つパスへのリクエストに対して External Authorization を無効にする設定例です。

```yaml
route_config:
  name: local_route
  virtual_hosts:
  - name: local_service
    domains: ["*"]
    typed_per_filter_config:
      envoy.filters.http.ext_authz:
        "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
        check_settings:
          context_extensions:
            virtual_host: local_service
    routes:
    - match: { prefix: "/static" }
      route: { cluster: some_service }
      typed_per_filter_config:
        envoy.filters.http.ext_authz:
          "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute
          disabled: true
    - match: { prefix: "/" }
      route: { cluster: some_service }
```

## 認可サービスの開発方法

### gRPC サービスとして実装する場合

[Authorization サービス](https://github.com/envoyproxy/envoy/blob/v1.16.0/api/envoy/service/auth/v3/external_auth.proto#L27-L34) が実装された gRPC サーバーを開発すれば完了です。[^1]

```protobuf
// A generic interface for performing authorization check on incoming
// requests to a networked service.
service Authorization {
  // Performs authorization check based on the attributes associated with the
  // incoming request, and returns status `OK` or not `OK`.
  rpc Check(CheckRequest) returns (CheckResponse) {
  }
}
```

Check メソッド内で、[service.auth.v3.CheckRequest](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/service/auth/v3/external_auth.proto#envoy-v3-api-msg-service-auth-v3-checkrequest) に含まれる受信リクエストのコンテキストを元に認可処理を実行して、[service.auth.v3.CheckResponse](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/service/auth/v3/external_auth.proto#service-auth-v3-checkresponse) で判定結果をフィルターに返す必要があります。

CheckResponse では単純に判定結果を返すだけではなく、HTTP Filter での利用で NG 判定をした際の HTTP レスポンスをデフォルトの 403 Forbidden ではなく任意のものに変更することや、OK 判定をした際に後続のフィルターチェーンでの利用を想定して、受信リクエストの HTTP ヘッダーを 追加/更新/削除 することも可能になっています。

GitHub で gRPC な実装が OSS で公開してくれている方がいるので、開発時に参考にすると良いかと思います。

- https://github.com/salrashid123/envoy_external_authz
- https://github.com/nokamoto/envoy-external-authz

### HTTP サービスとして実装する場合

[extensions.filters.http.ext_authz.v3.HttpService](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto#extensions-filters-http-ext-authz-v3-httpservice) に記載されているとおり、認可判定が OK なら 200 レスポンスを返し、NG なら 200 以外のレスポンスを返す HTTP サーバーを開発すれば完了です。

gRPC サービスとして実装する際には OK 判定の場合に CheckResponse で受信リクエストを加工することができましたが、HTTP サービスとして実装する場合には、先述の HttpService の設定オプションである [extensions.filters.http.ext_authz.v3.AuthorizationResponse](https://www.envoyproxy.io/docs/envoy/v1.16.0/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto#envoy-v3-api-msg-extensions-filters-http-ext-authz-v3-authorizationresponse) を利用することで HTTP ヘッダーを加工すること（追加/更新のみ）が可能です。

GitHub で HTTP な実装が OSS で公開してくれている方がいるので、開発時に参考にすると良いかと思います。

- https://github.com/rickcodetalk/envoy-ext-authz

## おわりに

今回は、Envoy での認可処理を外部のシステムに委譲することが可能になる External Authorization という機能を紹介しました。[RBAC Filter](https://www.envoyproxy.io/docs/envoy/v1.16.0/intro/arch_overview/security/rbac_filter) など Envoy にビルトインされている認可機構ではなく、独自の認可機構を利用したい場合には External Authorization を活用すると良いでしょう。[^2] [^3]

[^1]: Open Policy Agent（OPA）を gRPC な認可サービスとして利用することが可能です。詳細は  Z Lab Advent Calendar 2019 で公開した [Envoy + SPIRE + OPA で Service Mesh を構築する](https://qiita.com/ryysud/items/bbfc730e17f53be65ce0) を参照ください。
[^2]: 一例として Consul Connect で構築された Service Mesh では External Authorization を利用して独自の認可機構が提供されている。
[^3]: Istio や Kuma のように xDS サーバーを配置して RBAC Filter を動的に設定することもアプローチとしては有用。