はじめに
数ヶ月前に SPIFFE Slack で GitHub の方が Emissary という OSS を公開したと アナウンス されていて、Emissary を利用することで何ができるのかが気になったので調べてみました。なお、この記事では SPIFFE/SPIRE についての説明は割愛します。
Emissary とは
Emissary は Envoy へのリクエストに含まれる JWT SVID を元にしたアクセス制御機能と、Envoy から別サービスへのリクエストに JWT SVID を追加する機能を持つソフトウェアとなっており、Envoy の認可処理を別システムに任せる仕組みである External Authorization の 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 設定 のように 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 を持つ通信相手に対して特定のエンドポイントへのアクセスを許可するものとなります。
$ 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 をセットするものとなります。
$ EMISSARY_EGRESS_MAP='{"app.domain.test": "spiffe://domain.test/app"}'
$ echo $EMISSARY_EGRESS_MAP | jq .
{
"app.domain.test": "spiffe://domain.test/app"
}
全体像を整理する
サービス間の通信は Emissary が設定された Envoy を介して行われることを前提としています。
リクエストを受けたとき
- リクエストを受けた Envoy が
x-emissary-mode:ingress
というヘッダーをセットして HTTP Filter のext_authz
フィルターを介して Emissary に問い合わせを行う - Emissary はリクエストの
x-emissary-auth
ヘッダーで定義された JWT SVID を検証した後でEMISSARY_INGRESS_MAP
で許可されているリクエストであるかを判定して Envoy にレスポンスする - Envoy は判断結果に応じてレスポンスを制御する
リクエストを送るとき
- Envoy と同居しているサービスから自身の Envoy にリクエストが送られる
- リクエストを受けた Envoy は
x-emissary-mode:egress
というヘッダーをセットして HTTP Filter のext_authz
フィルターを介して Emissary にリクエストする - Emmisary は同居しているサービスからのリクエストに含まれる Host ヘッダーと
EMISSARY_EGRESS_MAP
で定義された情報を照合して JWT SVID のaud
に含める SPIFFE ID を判定した上で JWT SVID を Workload API から発行する - 元リクエストの
x-emissary-auth
ヘッダーに JWT SVID を乗せて別サービスにリクエストを投げる
手元で動かすには?
Kubernetes で Emissary を動かす Example が提供されているので、spire-examples を参考に SPIRE がインストールされた Kubernetes クラスタを用意すれば手元でも試すことができます。
所感
Envoy で JWT SVID を利用してアクセス制御をしたいユースケースで非常に役立ちそうな印象を受けましたが、サービス数が多い環境では EMISSARY_INGRESS_MAP
と EMISSARY_EGRESS_MAP
を動的に変更できるような仕組みがないと厳しいかな思ったので、本番導入を検討する際には機能改善をするかアイデアを拝借して新しいものを作るのが良いかなと思いました。
あとは、External Authorization の Server で Egress リクエストにヘッダーを追加するという使い方が斬新だなと思いました。リクエストに応じて都度 JWT SVID を発行してセットするということをやろうとするとこのようなアプローチしかなかったのか、これが一般的なのかはナレッジ不足なので詳しい方いらっしゃったらコッソリ教えてもらいたいです。
最後に小ネタですが HiNative での投稿 によると Envoy と Emissary は同義語みたいです。