Envoy External Authorization で gRPC サービスから認可を分離する実装の検証


Loading...

はじめに

はじめまして、KCS 部の nokamoto です。

私の所属するチームでは KADOKAWA グループ向けプライベートクラウド (以下 KCS) が利用されやすくなるように、KCS のインターフェースとなる Web Console や API やドキュメントを標準化するプロジェクトを進めています。

今回は KCS の各サービスが標準的な API を公開できるようにする取り組みの一つである gRPC 関連の仕組み化について投稿します。

標準化において KCS サービスのリリースのアジリティを上げるため、各 KCS サービスが持つビジネスロジックと全 API で必要な認可検証を分離する必要があります。
そこで、認可検証の分離をどのような方法を使えば実現できるか検討を行い、External Authorization — envoy tag-v1.13.1 documentation の仕組みが利用できると考え検証を行いました。

この投稿は検証過程で envoy のドキュメントには実装について具体的な記述が見つけられなかったこと、HTTP を利用した実装の参考はいくつか *1 *2 見つけることができたものの一通り gRPC を利用した実装の参考が少なかったことから、自分自身で検証した内容を一通り整理してまとめる目的も合わせて行なっています。

検証内容をまとめたリポジトリは以下になります。

GitHub - nokamoto/envoy-external-authz

検証を始めるために golang + gRPC を利用した Hello World *3 を一番参考にさせて貰いました。

検証対象は envoy 1.13.1 で行なっています。

要約

https://raw.githubusercontent.com/nokamoto/envoy-external-authz/master/example.png


検証結果として以下を実装することができました。

  • envoy.service.auth.v2.Authroization を実装して任意の認可サービス呼び出しを分離する
  • Authrization サービスの検証結果から upstream に gRPC metadata を 伝搬 する

 

External Authorization の実装方法

# envoy-proxy.yaml
http_filters:
- name: envoy.ext_authz

envoy で external authorization の呼び出しを設定し、呼び出す先の gRPC Authorization サービスの実装を行います。
Authorization サービス自体は github.com/envoyproxy/go-control-plane/envoy/service/auth/v2 を参照して実装することができます。

// cmd/authz/server.go
http := req.GetAttributes().GetRequest().GetHttp()
apikey, found := http.GetHeaders()[metadata.XMyAPIKey]
if s.apikey != apikey {
    return &unauthorized, nil
}

Authorization サービスでは gRPC メソッドや metadata の情報を受け取り検証することができます。

// cmd/authz/server.go
auth.CheckResponse{
  Status: &status.Status{
    Code:    int32(code.Code_PERMISSION_DENIED),
    Message: "external authorization failed",
  },
}

認可サービスでの検証結果で権限が足りなかった場合などはエラーコードを返します。
その場合、envoy は upstream にリクエストを送らずに gRPC ステータス PERMISSION_DENIED を返します。

// cmd/authz/server.go
auth.CheckResponse{
  Status: &status.Status{
    Code: int32(code.Code_OK),
  },
}

upstream にリクエストを送っても問題ない場合は成功を返します。

この時 Authorization サービスで発生した情報を伝搬させる場合は HttpResponse を設定することが出来ます。

// cmd/authz/server.go
{
    Header: &core.HeaderValue{
        Key:   metadata.XMyUsername,
        Value: "foo",
    },
    Append: &wrappers.BoolValue{
        Value: false,
    },
},

HttpResponse の コメント には、既に存在している metadata キーペアに追加する場合は append を true にし、新たに metadata キーバリューを追加する場合は false に設定するといった設定方法が記載されています。
今回は新たなキーバリューを追加するため append を false に設定しています。

実装結果

// cmd/client/main.go
req := &protobuf.EchoRequest{
    Value: strconv.Itoa(i),
}
ctx := metadata.AppendToOutgoingContext(context.Background(), md.XMyAPIKey, os.Getenv(APIKey))
res, err := echo.Echo(ctx, req)

// cmd/echo/echo.go
md, found := metadata.FromIncomingContext(ctx)
log.Printf("metadata=%v, found=%v", md, found)

この Authorization サービスを適用して検証用に定義した Echo サービスの呼び出しを行います。
今回の Authorization サービスの実装では、クライアントは x-my-apikey を設定してリクエストを行い、適切な値が設定されていた場合のみ Echo サービスへリクエストが送られます。

# echo stdout
metadata=map[:authority:[envoy:80] content-type:[application/grpc] user-agent:[grpc-go/1.28.0] x-envoy-expected-rq-timeout-ms:[15000] x-forwarded-proto:[http] x-my-apikey:[648a9d5e7f9d36e3a2b00db576e96f69] x-my-username:[foo] x-request-id:[079e4183-ea6d-49dc-986d-7b5aa7682809]], found=true

その際には Authorization サービスで付与された x-my-username を Echo サービスで取得することができました。

まとめ

gRPC を利用した envoy external authorization を適用することで、認可サービスの呼び出しをビジネスロジックから分離することが可能であることが実装により検証できました。
envoy は gRPC のゲートウェイとして他にも有用な機能を有していると考えられるので、次のステップとしては envoy の構築と共に他にも共通するロジックを分離していくことで gRPC を活用してサービスの API 公開を容易にする環境整備を進める予定です。