3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

proxy-wasm を使って JWT 検証を go 言語で実装できるのか?

Posted at

proxy-wasm での JWT 検証の実装を通して proxy-wasm でどこまでできるのかを調べた。
proxy-wasm は将来性に期待できる技術なものの、WebAssembly(wasm) のメインの用途はブラウザ上と思われる。
そのため proxy-wasm は発展途上で、現時点では色々な制約・将来性がある中でその魅力・使いやすさなどを知るために触ってみた。

要約

  • proxy-wasm で JWT 検証を go 言語で実装するのは下記理由で困難だった。おそらく他言語でも同様だと思われる。
  • リクエストの内容についての簡単なチェックやメトリクス・ログなどをどこかに送るなどの用途であれば現状でも可能。
    例えば protobuf で送られてきたメッセージを解析してメトリクス・ログを送る用途であれば
  • proxy 拡張のコードをなれた言語で書け、ユニットテストできるのは魅力的。proxy-wasm 構築用のツール(wasme)もあり、実装しやすい。

proxy-wasm とは

proxy-wasm は L4/L7 プロキシにおける拡張を wasm によって実現するための機能である。制約事項や性能懸念等まだまだあるものの、 wasm コンパイル可能な好きな言語を使って既存のライブラリも活用しながら拡張機能を追加できる点で魅力的に思える。

proxy 拡張でほかの実現方法といえば、例えば Envoy Lua Filter が考えられる。
こちらの場合は Lua 言語で実装し、 Envoy を拡張する Filter を追加する形で適用される。
Lua 言語も個人的には扱いやすい言語ではあるものの、環境によっては Lua パッケージ管理ツールが使えないなどデメリットもあると思われる。

モチベーション

今回は proxy-wasm を使って istio における JWT 検証を go 言語で実装できるかトライしたい。
istio の JWT 検証であれば JWTRule リソースを使えば簡単に実現できるが、header からの値の取得・解析・JWKS URL へのアクセスを介した署名検証などの多くの機能が必要で、 proxy-wasm がどこまでやれるか把握するためにちょうど良いターゲットかなと思う。

具体的には、下記のようなフローの実装をトライする。JWTは GCP の ServiceAccount を使って生成されたものを利用し、検証用に istio install 手順 にあるサンプルアプリを採用する。
proxy-wasm-arch.drawio.png

調査

go 言語で proxy-wasm を実装するために、wasme, proxy-wasm-go-sdkを使った。
wasme は proxy-wasm を実装・ビルド・デプロイするための便利なツールである。これを使うと初期ファイルの作成からやってくれるが、go 言語で実装したい場合は tinygo を選択することになる。

調査結果

前述のとおり、下記理由から proxy-wasm で JWT 検証を go 言語で実装するのは困難だった。

具体的には、一般的な JWT 検証フローは下記のようになるが、4. までしか実現できない。そのため、JWT が不正に書き換えられているかのチェックが実施できないことになる。

  1. HTTP Header から Bearer トークンを取得する
  2. Bearer トークンを base64 デコードする
  3. JWT ヘッダーのアルゴリズム(alg)に問題ないか検証する
  4. JWT ペイロードの有効期限(exp)が切れてないか検証する
  5. JWT ペイロードの issuer(iss) をもとに JWKS url にアクセスし、 JWT の署名検証を行う

以下はその調査内容の詳細である。

tinygo で利用可能な package の制約

主に下記3つの package がサポートされていないため、既存の JWT 検証 OSS をほぼ利用できない。また、特に crypto/rsa が使えないことで RSA 署名された JWT 検証がかなり困難となる。

- crypto/rsa
- encoding/json
- net/http

encoding/json が使えない理由は reflect package が利用できないことが原因なので、ほかの reflect を使わない json encode/decode する OSS を利用すれば JWT の json decode 自体は問題なく実施できる。
net/http は利用できないが、 HTTP アクセスはそもそも proxy-wasm で定義されたインターフェースを使う必要があるので、 tinygo でサポートされたとしても proxy-wasm では使えない。ただ、これを利用できないことによって活用できる OSS は制限されてしまいそう。

proxy-wasm における http アクセスの制約

proxy-wasm では HTTP アクセスのために DispatchHttpCall を通して呼び出す必要がある。

この際、 HTTP 呼び出しを行ってその response を受け取って処理を行う callback を渡す必要があるが、この callback 処理が非同期で行われ、upstream へのアクセスを待ってくれない。
そのため、 HTTP GET を行った結果を元に何らか処理をし、それによって upstream への呼び出しをする/しないを切り替えることはできない。

調査手順

下記環境で proxy-wasm の実装・デプロイを行う。

  • proxy-wasm の実装
    • wasme: 0.0.33
    • proxy-wasm-go-sdk v0.1.1
  • proxy-wasm のデプロイ
    • Ubuntu 20.04
    • Docker 20.10.18
    • minikube v1.28.0
    • Istio 1.16.1

また、JWT 検証実装の調査手順としては下記になる。

  • proxy-wasm の実装
    • wasme ツールのインストールと初期化
    • JWT 検証処理の実装
  • proxy-wasm のデプロイ
    • istio 環境の準備
    • 実装したものを OCI として push
    • proxy-wasm を istio WasmPlugin を通してデプロイ
    • curl で JWT を設定してアクセスし、その結果・ログを確認

最終的な実装されたコードの全体像はこちらに置いた。

wasme ツールのインストールと初期化

wasme のインストール手順通り、下記を実施する。

curl -sL https://run.solo.io/wasme/install | sh
export PATH=$HOME/.wasme/bin:$PATH

wasme init で下記を選択すると、 go 言語で proxy-wasm を実装するための初期テンプレートが生成される。

# wasme init custom_jwt_filter
✔ tinygo
✔ istio:1.7.x, gloo:1.6.x, istio:1.8.x, istio:1.9.x

JWT 検証処理の実装

JWT 検証処理を実装するには、生成された main.go の中にある OnHttpRequestHeaders 関数に処理を追加する。
Header は [][2]string 型で取得できる。また、key はすべて小文字で格納されているので、下記のように authorization Header から Bearer トークンを取得する処理を挟む。
types.Action は2種類存在しており、 upsteam に処理を送りたいなら types.ActionContinue を、処理を中断して response を返したいなら types.ActionPause を設定する。

func (ctx *httpHeaders) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
hs, err := proxywasm.GetHttpRequestHeaders()
if err != nil {
    proxywasm.LogCriticalf("failed to get request headers: %v", err)
}
proxywasm.LogDebugf("on request header: %v", hs)

for _, h := range hs {
    // jwt header access
    if h[0] == "authorization" {
        token := strings.TrimPrefix(h[1], "Bearer ")
        // ここに JWT 検証処理を実装
    }
}
return types.ActionPause
}

JWT の中身を json を decode して検証する

https://github.com/buger/jsonparser のような OSS を使うと tinygo でも json を処理できる。他にもあると思うが、今回の実装ではこちらを採用した。

例えば JWT ヘッダーのアルゴリズム(alg)の値を取得したい場合は下記のような記述になる。

parts := strings.Split(token, ".")
header, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
    panic(err)
}
alg, err := jsonparser.GetString(header, "alg")
if err != nil {
    panic(err)
}

JWKS URL にアクセスする


※ 検証結果で記載した通り実際には機能しないが、トライした実装を記載する。
DispatchHttpCall には envoy cluster 名しか渡せないので、 URL path は Header に設定する。
JWT は GCP ServiceAccount から生成したものを想定しているため、 JWKS URL は https://www.googleapis.com/service_accounts/v1/metadata/jwk/{issuer} となる。
本来 callback にはこの JWKS URL にアクセスした結果を処理して署名検証するコードを記述することになるが、 tinygo はまだ crypto/rsa をサポートしていない。上記 JWT は alg=RS256 なので現状は実装できず、 wasme build が通らなくなる。

func callJwksUrl(host, iss string, callback callback) error {
    reqHeaders := [][2]string{
        {":authority", "www.googleapis.com"},
        {":method", "GET"},
        {":path", "/service_accounts/v1/metadata/jwk/" + iss},
    }
    if _, err := proxywasm.DispatchHttpCall(host, reqHeaders, "", nil, 5000, callback); err != nil {
        return err
    }
}

istio 環境の構築

下記は minikube 環境が設定済みの前提で手順を記載する。

istio 構築チュートリアルのとおりに istio をインストールし、

curl -L https://istio.io/downloadIstio | sh -
cd istio-1.16.1
istioctl install --set profile=demo -y

bookinfo のサンプルアプリをデプロイする。

minikube kubectl -- label namespace bookinfo istio-injection=enabled
minikube kubectl -- apply -n bookinfo -f https://raw.githubusercontent.com/istio/istio/release-1.16/samples/bookinfo/platform/kube/bookinfo.yaml
minikube kubectl -- apply -n bookinfo -f https://raw.githubusercontent.com/istio/istio/release-1.16/samples/bookinfo/networking/bookinfo-gateway.yaml

実装したものを OCI として push

istio の WasmPlugin を追加するためにはいくつか方法があるが、ここでは oci image を作成してそれを実行する方法を採用する。
そのために、webassemblyhub に実装した proxy-wasm の image を push する。まずユーザ登録を行い、 wasme login しておく。

下記コマンドを proxy-wasm を実装した main.go があるディレクトリで実行し、ビルド・tag 打ち・push する。

wasme build tinygo ./ -t webassemblyhub.io/hiroyoshii/sample:dev
wasme tag webassemblyhub.io/hiroyoshii/sample:dev webassemblyhub.io/hiroyoshii/sample:v1.0
wasme push webassemblyhub.io/hiroyoshii/sample:v1.0

proxy-wasm を istio WasmPlugin を通してデプロイ

あとは WasmPlugin リソースをデプロイするだけだが、そのために下記2つの設定が必要なのでそちらの手順も記載する。

  • 外部のエンドポイント(www.googleapis.com)を呼び出せる envoy cluster の追加
  • Webassemblyhub から image を取得するための Secret 追加

外部のエンドポイントを呼び出せる envoy cluster の追加

JWKS URL にアクセスする にある通り、 DispathHttpCall で指定するホスト名は envoy における cluster 名である。
istio 環境で外部ホストを追加するには下記のような ServiceEntry を追加すれば良い。

apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: external-svc-https
spec:
hosts:
- www.googleapis.com
location: MESH_EXTERNAL
ports:
- number: 443
    name: https
    protocol: TLS
resolution: DNS

これで outbound|443||www.googleapis.com という cluster 名が envoy proxy に追加され、 proxy-wasm から www.googleapis.comへアクセス可能になる。

Webassemblyhub から image を取得するための Secret 追加

oci image を pull できるように、下記コマンドで secret 登録する。
user name と password は wasme login したものと同様のもので良い。

minikube kubectl -- create secret docker-registry webassemblyhub-secret --docker-server=webassemblyhub-secret --docker-username=<user name> --docker-password=<password> -n istio-system

WasmPlugin のデプロイ

下記設定で istio-ingressgateway に proxy-wasm を適用できる。

cat << EOF | kubectl apply -f -
  apiVersion: extensions.istio.io/v1alpha1
  kind: WasmPlugin
  metadata:
    name: custom-wasm-filter
    namespace: istio-system
  spec:
    selector:
      matchLabels:
        istio: ingressgateway
    url: oci://webassemblyhub.io/hiroyoshii/sample:v1.0
    imagePullPolicy: IfNotPresent
    imagePullSecret: webassemblyhub-secret
    phase: AUTHN
    pluginConfig:
      openid_server: authn
      openid_realm: ingress
  EOF

istio ingressgateway の log を確認し、下記のようなログが出ていればOK。

$ minikube kubectl -- logs -n istio-system -l app=istio-ingressgateway -f
...
info    wasm    fetching image hiroyoshii/sample from registry webassemblyhub.io with tag v1.0

疎通&ログ確認

これで istio ingressgateway に JWT 検証を行う proxy-wasm が適用されたため、 curl 等で JWT 設定された HTTP 呼び出しを行えばその動作を確認できる。

今回は proxy-wasm のデバッグログの確認も行いたいため、 istio ingressgateway に下記設定変更を行った。
kubectl edit deployment を実行し、下記 args 内にある proxyLogLeveldebug に設定しなおすと、ログが istio-ingressgateway に表示されるようになる。

    containers:
    - args:
      - proxy
      - router
      - ...   

curl でのアクセスは istio のチュートリアル手順と同様のエンドポイントに対して、下記のように Bearer トークンとして JWT を設定して呼び出しを行い、 JWT 検証ができているか確認している。
JWT の生成は GCP ServiceAccount の JWT 生成エンドポイント を叩いて実施した。

curl -H "Authorization: Bearer <JWT>" -s "http://${GATEWAY_URL}/productpage"

詰まったポイント

調査手順としては以上だが、今回詰まったポイントとその解消方法をメモしておく。

proxy-wasm とは直接関係ない部分であるが、 前述のとおり、DispatchHttpCall に設定するホスト名には envoy の cluster 名を指定する必要がある。これは下記コマンドで envoy config の dump を取得することでアクセスしたい先の cluster_name のフィールドから正確な名前を取得できる。

kubectl exec <istio-ingressgateway pod name> -c istio-proxy -n istio-system -- curl localhost:15000/config_dump

自身で envoy yaml 設定を記述しているなら簡単にわかるが、 istio の ServiceEntry を通して登録された cluster 名が何かがわからなかったため時間がかかってしまったポイントだった。

さいごに

元々やりたかった proxy-wasm による JWT 検証が実装しきれなかったのは残念だが、実装~デプロイまでの手順を調査して色々と学びになった。
やはり手慣れた言語で envoy の拡張を実装・テストできるのは魅力だし、 wasme ツールを使えば開発はかなり楽ができると思う。
今後の proxy-wasm と tinygo の発展が楽しみ。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?