Emissary を利用した Envoy での JWT SVID ベースのアクセス制御

## はじめに

数ヶ月前に SPIFFE Slack で GitHub の方が [Emissary](https://github.com/github/emissary) という OSS を公開したと [アナウンス](https://spiffe.slack.com/archives/C2F07SCD6/p1595523299053500) されていて、Emissary を利用することで何ができるのかが気になったので調べてみました。なお、この記事では SPIFFE/SPIRE についての説明は割愛します。

## Emissary とは

![image](https://user-images.githubusercontent.com/6594187/94105529-6dcc7a00-fe74-11ea-94af-0549f65088b3.png)

Emissary は Envoy へのリクエストに含まれる JWT SVID を元にしたアクセス制御機能と、Envoy から別サービスへのリクエストに JWT SVID を追加する機能を持つソフトウェアとなっており、Envoy の認可処理を別システムに任せる仕組みである [External Authorization](https://www.envoyproxy.io/docs/envoy/v1.15.0/intro/arch_overview/security/ext_authz_filter) の Server として機能します。Server は TCP または UDS での起動がサポートされています。

External Authorizaion の Client としては Network Filter（L4）と HTTP Filter（L7）の ext_authz フィルターが存在しますが、Emissary は HTTP Filter の Server として機能するものになっています。基本的には External Authorization での利用となりますが Lua スクリプトで利用する方法もサンプルコードでは紹介されています。

Emissary には Ingress モードと Egress モードの2つのモードがあり、Envoy の ext_authz フィルターから Emissary へのリクエストに含まれる `x-emissary-mode` ヘッダーによりモードが決定されます。モードは [サンプルの Envoy 設定](https://github.com/github/emissary/blob/19b6972c6bb64a6626e0c13c176f6c96241f1a8a/examples/kubernetes/configmap.yaml) のように [extensions.filters.http.ext_authz.v3.AuthorizationRequest](https://www.envoyproxy.io/docs/envoy/v1.15.0/api-v3/extensions/filters/http/ext_authz/v3/ext_authz.proto#extensions-filters-http-ext-authz-v3-authorizationrequest) の `headers_to_add` で固定して設定するのが良いかと思います。

### Ingress モード

Ingress モードは HTTP リクエストの `x-emissary-auth` ヘッダーで渡ってきた JWT SVID のデジタル署名を検証して、`aud` に自身の SPIFFE ID がセットされているかをチェックした後で、HTTP メソッドや HTTP パスなどのリクエスト情報を元にアクセス制御（認可）を行う機能になっています。

アクセス制御の定義は Emissary 起動時に `EMISSARY_INGRESS_MAP` という環境変数に JSON 形式で設定する必要があります。以下の定義例は `spiffe://domain.test/app` という SPIFFE ID を持つ通信相手に対して特定のエンドポイントへのアクセスを許可するものとなります。

```bash
$ EMISSARY_INGRESS_MAP='{"spiffe://domain.test/app": [{"path":"/put","methods":["PUT"]},{"path":"/p","methods":["PATCH"]},{"path":"/g","methods":["GET"]}]}'

$ echo $EMISSARY_INGRESS_MAP | jq .
{
  "spiffe://domain.test/app": [
    {
      "path": "/put",
      "methods": [
        "PUT"
      ]
    },
    {
      "path": "/p",
      "methods": [
        "PATCH"
      ]
    },
    {
      "path": "/g",
      "methods": [
        "GET"
      ]
    }
  ]
}
```

### Egress モード

Egress モードは SPIRE Agent の Workload API を介して JWT SVID を取得して HTTP リクエストの `x-emissary-auth` ヘッダーに JWT SVID をセットする機能になっています。

JWT SVID の `aud`（JWT を受け取るシステム、つまり通信相手）に含める SPIFFE ID もしくは任意の文字列は、Emissary 起動時に `EMISSARY_EGRESS_MAP` という環境変数に JSON 形式で設定する必要があります。以下の定義例はホスト名が `app.domain.test` へのリクエストに `aud` に `spiffe://domain.test/app` が定義された JWT SVID をセットするものとなります。

```bash
$ EMISSARY_EGRESS_MAP='{"app.domain.test": "spiffe://domain.test/app"}'

$ echo $EMISSARY_EGRESS_MAP | jq .
{
  "app.domain.test": "spiffe://domain.test/app"
}
```

## 全体像を整理する

サービス間の通信は Emissary が設定された Envoy を介して行われることを前提としています。

### リクエストを受けたとき

1. リクエストを受けた Envoy が `x-emissary-mode:ingress` というヘッダーをセットして HTTP Filter の `ext_authz` フィルターを介して Emissary に問い合わせを行う
2. Emissary はリクエストの `x-emissary-auth` ヘッダーで定義された JWT SVID を検証した後で `EMISSARY_INGRESS_MAP` で許可されているリクエストであるかを判定して Envoy にレスポンスする
3. Envoy は判断結果に応じてレスポンスを制御する

### リクエストを送るとき

1. Envoy と同居しているサービスから自身の Envoy にリクエストが送られる
2. リクエストを受けた Envoy は `x-emissary-mode:egress` というヘッダーをセットして HTTP Filter の `ext_authz` フィルターを介して Emissary にリクエストする
3. Emmisary は同居しているサービスからのリクエストに含まれる Host ヘッダーと `EMISSARY_EGRESS_MAP` で定義された情報を照合して JWT SVID の `aud` に含める SPIFFE ID を判定した上で JWT SVID を Workload API から発行する
4. 元リクエストの `x-emissary-auth` ヘッダーに JWT SVID を乗せて別サービスにリクエストを投げる

## 手元で動かすには？

Kubernetes で Emissary を動かす [Example](https://github.com/github/emissary/tree/master/examples/kubernetes) が提供されているので、[spire-examples](https://github.com/spiffe/spire-examples) を参考に SPIRE がインストールされた Kubernetes クラスタを用意すれば手元でも試すことができます。

## 所感

Envoy で JWT SVID を利用してアクセス制御をしたいユースケースで非常に役立ちそうな印象を受けましたが、サービス数が多い環境では `EMISSARY_INGRESS_MAP` と `EMISSARY_EGRESS_MAP` を動的に変更できるような仕組みがないと厳しいかな思ったので、本番導入を検討する際には機能改善をするかアイデアを拝借して新しいものを作るのが良いかなと思いました。

あとは、External Authorization の Server で Egress リクエストにヘッダーを追加するという使い方が斬新だなと思いました。リクエストに応じて都度 JWT SVID を発行してセットするということをやろうとするとこのようなアプローチしかなかったのか、これが一般的なのかはナレッジ不足なので詳しい方いらっしゃったらコッソリ教えてもらいたいです。

最後に小ネタですが [HiNative での投稿](https://hinative.com/ja/questions/42841) によると Envoy と Emissary は同義語みたいです。