h-otterの備忘録

インフラを中心にコンピュータ関連をいいかんじにやっていきます

n0stackの認証

n0stackの認証を実装するときに、今更パスワード認証はな~~という気持ちがありました。 ただ、WebAuthnやOpenID ConnectをgRPCインターフェイスで実現するのはなかなか難しかったためどうするか頭を悩ませていました。 その際、ちょうどVMにログインするための公開鍵をn0stackに保存するように変更したため、それでn0stackにログインできればいいのではと考えました。 gRPCを利用するときは、たいていSSH鍵もそのPCに入っているはずなので鍵の管理も容易です。 本稿はその認証方法の紹介です。

h-otter.hatenablog.jp

認証方法

認証方法は公開鍵認証の金字塔であるSSHの方法を参考にしています。 以下のファイルがPoCです。

github.com

実際にn0stackで認証する場合、シーケンス図で表すと以下のようになります。

f:id:h-otter:20190922202623p:plain
認証のシーケンス図

また、コードとの関係は

  1. ユーザーが自分の公開鍵とユーザーアカウントに紐づける
  2. n0stackに認証開始を通知
  3. ユーザーがチャレンジを受け取る n0stack/jwt_example_test.go at 46bf0ae443115774a59a498ea3abe02fb0adc657 · n0stack/n0stack · GitHub
  4. ユーザーはチャレンジとSSH秘密鍵をもとにChallenge JWTを作成 n0stack/jwt_example_test.go at 46bf0ae443115774a59a498ea3abe02fb0adc657 · n0stack/n0stack · GitHub
  5. n0stackはユーザーアカウントに紐づいているSSH公開鍵でChallenge JWTを検証、正しければ実際に認証に利用するAuthn JWTを発行 n0stack/jwt_example_test.go at 46bf0ae443115774a59a498ea3abe02fb0adc657 · n0stack/n0stack · GitHub
  6. ユーザーはAuthn JWTを使って利用したいサービスにアクセス
  7. サービスはn0stackからAuthn用の公開鍵を取得
  8. Authn JWTを検証、正しければユーザーにサービスを提供する n0stack/jwt_example_test.go at 46bf0ae443115774a59a498ea3abe02fb0adc657 · n0stack/n0stack · GitHub

といった形になります。 ここでAuthn用の秘密鍵と公開鍵はYubikeyを参考に、一つのシークレットから自動生成することで秘密情報の管理を簡単にしています。 万が一シークレットが漏洩した場合でも、シークレットを切り替えた時点でAuthn用の公開鍵が切り替わるためAuthn JWT無効化され、ユーザーが再発行すればサービスを継続できるため被害を最小限に防ぐことができます。 また、gRPCの :authority ヘッダ (httpにおけるHostヘッダ) によってAuthn用の公開鍵と秘密鍵の組を変えることで、フィッシングへの対策としています。

developers.yubico.com

中間者攻撃に対してはChallenge JWTに :authority を含めることで対策しています。 攻撃者Evilが example.comトークンを取得しようと考えた場合、シーケンス図的には以下のようになります。 Challenge JWTの改ざんは不可能であると仮定すれば、中間者がいることを検知できます。

f:id:h-otter:20190922202550p:plain
中間者攻撃のシーケンス図

クライアントライブラリに悪意がある場合はどうしようもありませんが、それはSSHも同様なので気にしないことにしています。

gRPCにおける実装

先ほどの認証方法で記載した通り、Authn JWTを発行するまでは2 ~ 5の手順に対応して以下のようにメッセージを交換する必要があります。

f:id:h-otter:20190923140823p:plain

そこで、 Bidirectional streaming RPC を利用しています。 これは一つのRPCの通信でTCPのように送受信できるgRPCインターフェイスです。 これによりチャレンジなどをデータベースなどに一時的に保存する必要がなくなるといったメリットがあります。 また、protobufのoneofを使い、メッセージの型を変えています。

developers.google.com

メッセージの定義は n0stack/authentication.proto at ced0429b1c0d3a81feca86596590427c07af1708 · n0stack/n0stack · GitHub でされており、具体的なサーバの実装は n0stack/api.go at ced0429b1c0d3a81feca86596590427c07af1708 · n0stack/n0stack · GitHub のようになっています。 エラーハンドリングなどはまだ雑ですが目をつぶっていただきたいです :pray: :pray: :pray:。 Bidirectional streaming RPC と oneof の実装についてのブログが少なくて少し混乱しましたが、とてもシンプルに記述ができます。

受信側は以下のようになり、 stream.Recv() でメッセージを受け取った後にoneofで指定したメッセージにキャストします。

また、送信は以下のように stream.Send() でメッセージを送信できます。 oneofの都合によりstructのネストが少し深くなりますが、そこを除けば最小限の記述量で済みます。

ユーザー側の実装は n0stack/authn_client.go at v1alpha · n0stack/n0stack · GitHub で行っています。 特筆すべき点はないですが、 Bidirectional streaming RPC によってサーバー側と同様に可読性の高いコードになっています。

まとめ

本稿ではSSHに利用している公開鍵でgRPCの認証を行う方法を紹介しました。 WebAuthnなどによって公開鍵認証が再び注目されている気がします。 CLISSH公開鍵は親和性が高いので、これからgRPCの認証方法として増えてほしいですね。 また、gRPCの Bidirectional streaming RPC は非常に便利でした。 grpc-gatewayなどによってhttpに変換できないことやテストが難しいことなどの欠点はありますが、おすすめです

すべてのインターフェイスが依存する認証方法を確立できたので、今後はクラウド基盤の機能を増やしていく予定です。 その際、v0.2ではリソースの作成ができてからレスポンスを返す同期APIでしたが、様々なフィードバックからとりあえず受理してレスポンスを返す非同期APIにしてほしいという要望が多かったため、そのように変更を加えます。 次の記事ではそこらへんか、すでにたまっている知見のどちらかを書くと思います。 今後ともよろしくお願いします。

ADR: http://docs.n0st.ac/en/master/developer/adr/grpc_authentication.html