IstioとAuth0で認証・認可を体験してみた

この記事は Retty Advent Calendar 2020 10日目の記事です。

adventar.org

はじめに

こんばんは、最近趣味でサックス🎷 を習い始めたエンジニアの櫻井です。

ServiceMeshの話題が出てからだいぶ経ちますがそろそろ自分でも触っておきたいなと思いServiceMeshを実現するツールの1つであるIstioにチャレンジしてみました。
今回はIstioの公式サンプルであるBookinfo ApplicationにJWT(JSON Web Token)による認証・認可を入れてみます。

この認証と認可については色々調べてみたものの、あんまり参考記事を見つけることができなかったので少々梃子摺りました。(なお本記事についてはService MeshやIstioについての詳しい説明は割愛しているため、まずそれらがなんぞや?という場合にはリンクの記事を読むことをオススメします)

IstioとAuth0で認証・認可を体験する

環境情報について

この記事のサンプルに沿って動かす場合にはこの情報を参考に環境を準備してください。
KubernetesはDocker for Macのものを使っていきます。

Mac OS 10.15.6
Docker for Mac 2.5.0.0
  Docker version 19.03.13, build 4484c46d9d
  Kubernetes version
    Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.3", GitCommit:"b3cbbae08ec52a7fc73d334838e18d17e8512749", GitTreeState:"clean", BuildDate:"2019-11-14T04:25:00Z", GoVersion:"go1.12.13", Compiler:"gc", Platform:"darwin/amd64"}
    Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.8", GitCommit:"9f2892aab98fe339f3bd70e3c470144299398ace", GitTreeState:"clean", BuildDate:"2020-08-13T16:04:18Z", GoVersion:"go1.13.15", Compiler:"gc", Platform:"linux/amd64"}
Istio 1.7.0

IstioのBookinfo Applicationを動かす

まずはIstioのサンプルであるBookinfo Applicationを動かしてみましょう。
GithubのIstioのリポジトリzipをダウンロードします。(git clone した場合は git checkout f3ad34dfecf0a81b9445b7ba3fd8ac50e20e13ad を実行します)

本記事のコマンドはダウンロードしたzipを解凍してできたディレクトリのパスにいる想定で記載しております。
またistioctlコマンドは公式のGetting Startedのページを参考にインストールされているものとします。

# istioのprofileをdemoに設定(通常はdefault)
# istioではprofileによってinstallされるcomponentが異なります詳細はドキュメントを参照してください
# https://istio.io/latest/docs/setup/additional-setup/config-profiles/
$ istioctl install --set profile=demo
✔ Istio core installed
✔ Istiod installed
✔ Egress gateways installed
✔ Ingress gateways installed
✔ Installation complete

# defaultのnamespaceに istio-injection=enabled のラベルを付与します
# これによりdefaultのnamespaceにpodができた際には自動的にsidecarがinjectionされるようになります
$ kubectl label namespace default istio-injection=enabled
namespace/default labeled

# Bookinfo Applicationのサンプルを立ち上げるためのyamlファイルを適用
$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created

# 上記で作成したpodでcurlを実行して疎通確認を行う
$ kubectl exec "$(kubectl get pod -l app=ratings -o jsonpath='{.items[0].metadata.name}')" -c ratings -- curl -s productpage:9080/productpage | grep -o "<title>.*</title>"
<title>Simple Bookstore App</title>

# ingress gatewayを作成する
$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created

# default namespaceで何か問題がないかをチェック
$ istioctl analyze
✔ No validation issues found when analyzing namespace: default.

# 環境変数を設定
$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
$ echo $INGRESS_HOST
localhost

$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
$ echo $INGRESS_PORT
80

$ export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT
$ echo $GATEWAY_URL
localhost:80

# 作成したBookinfo ApplicationにアクセスするためのURLを確認
$ echo "http://$GATEWAY_URL/productpage"
http://localhost:80/productpage

最後に表示されたURLにブラウザでアクセスしてみましょう。
下図のような画面にアクセスできたらBookinfo Applicationの構築は完了です。

f:id:rettydev:20201116001222p:plain
Bookinfo Applicationの画面

Booking Applicationの構築が完了すると、公式ページにあるように下図のような構成となって動作します。
リクエストをingress gatewayが受け取り、各PodへのリクエストはSidecarとして配置されたenvoyを通して行われることとなります。今回の記事で行う認可はこのenvoyの部分で行うこととなるため各アプリケーションへはなんのコードの修正をしなくても簡単に導入することができるようになるわけです。

f:id:rettydev:20201129233752p:plain

認証と認可について

続いては認証と認可についてのお話です。
自分も前はあまり認証と認可についての違いをすぐに説明できなかったのですが、同僚のこの言葉を聞いてからはチョットワカルようになりました。

曰く、認証は Who you are? で、認可は What you can? とのこと。簡単に言ってしまえば、認証は誰であるかを見極めるものであり、認可は二点間の通信が可能かどうかを見極めるものであるということです。

上記でもあまりピンと来ない、あるいはもっと詳しく知りたい場合はGoogle先生が優しく教えてくれるので聞いてみましょう。

今回は認証にはAuth0を、認可にはJWTを使っていきたいと思います。

Auth0の登録と設定

Auth0の登録の手順については省略しますが、メールアドレス&パスワードでの登録のほかGithubGoogleMicrosoftアカウントによる登録ができますので好きな手段で登録をしましょう。

登録をしたらAPIのMenuから新規APIの作成をします。

f:id:rettydev:20201121230131p:plain
新規APIの追加

すると新規API作成のメニューが開くので適当に情報を入れて作成します。
ここで入力したIdentifierは後ほど重要になります。
このサンプルでは https://saku2saku.example.com としましたので、以降ではそれを使っていきます。

f:id:rettydev:20201121230529p:plain
新規APIの情報入力画面

認証APIができあがったらTestタブを開きましょう。
作ったAPIを使って認証できるサンプルがありますのでcurlの例を使ってTokenを発行してみましょう。

f:id:rettydev:20201121231617p:plain
認証APIのTestタブ

APIを叩いてみてJWTが発行されるのを確認します。
(Client IDとClient secretは伏せています)

❯ curl --request POST \
  --url https://saku2saku.us.auth0.com/oauth/token \
  --header 'content-type: application/json' \
  --data '{"client_id":"xxxxxxx","client_secret":"xxxxxxxxxx","audience":"https://saku2saku.example.com","grant_type":"client_credentials"}'
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im85aXFQbFM2c2ljcEh4VFhzSEp4cSJ9.eyJpc3MiOiJodHRwczovL3Nha3Uyc2FrdS51cy5hdXRoMC5jb20vIiwic3ViIjoiZmlSbUUxY0Z5NHhhNUpCWVdyRDU1R1FHNWxneHo0Vm1AY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vc2FrdTJzYWt1LmV4YW1wbGUuY29tIiwiaWF0IjoxNjA1OTY3NzIxLCJleHAiOjE2MDYwNTQxMjEsImF6cCI6ImZpUm1FMWNGeTR4YTVKQllXckQ1NUdRRzVsZ3h6NFZtIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.SYfn9KF9K2ajkJOn3rKaX_T5-VScFZvrc04CrYpCRvpiFYtejmaWd4zmHdm-Kkj7ZbCk-QAB4VBa5tiSVwj9u7IiHI31dBnLsN0wJN79ZX0dil_N2VwcRUsRnFBcuO3lYLR_WKK-hlOriww3ONBvqbnXHVXdrBg7Fs7l3Ir4TfUSGm3ZXUZnL5WEfvrotNSV1sazOzQkqYCB2YEnXYWLPR9PlcnPWBDaYVe2VXHXMqy9jsU8EJZQT5zjPup7NJqbafhVwoghJdP8EeYnACsrKvhussnk0_jVWjfI2bt0d-IEFyj3JmzqDaNZWTZa3AbabzxrYMsGZ_0Z4igNxFDp7Q","expires_in":86400,"token_type":"Bearer"}

無事認証ができ、JWTが発行されるのを確認できました。
このTokenを使ってこの後のサンプルを動かしていきましょう。
(Tokenの有効期限は返り値のexpires_inにある通り1日となっています)

JWTについて

JWT(JSON Web Token)についての公式の説明を見てみましょう。

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties.

雑に英訳すると、URL-safeな二点間における通信の権限要求をコンパクトに表現できるものである、といった感じでしょうか。

JWTは3つのパートに分かれており、各パートは . によって区切られています

ヘッダー.ペイロード.電子署名

各パートの内容は以下のとおりです。

  • ヘッダー
    • 暗号化の種別情報
  • ペイロード
    • 任意の情報を格納可能
    • ただし、予約語となっているKeyもある
  • 電子署名
    • 暗号化の種別情報に応じて作成される署名
    • これを元に正しいリクエストかどうかを判別される

それぞれのパートはBase64エンコードされているだけなので、Base64デコードすれば中身を見ることができます。そのため、ペイロードの中身は簡単に見ることができてしまうため入れる情報については気をつける必要があります。

試しにAuth0で発行されたJWTをデコードしてみましょう。

デコード前

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im85aXFQbFM2c2ljcEh4VFhzSEp4cSJ9.eyJpc3MiOiJodHRwczovL3Nha3Uyc2FrdS51cy5hdXRoMC5jb20vIiwic3ViIjoiZmlSbUUxY0Z5NHhhNUpCWVdyRDU1R1FHNWxneHo0Vm1AY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vc2FrdTJzYWt1LmV4YW1wbGUuY29tIiwiaWF0IjoxNjA1OTY3NzIxLCJleHAiOjE2MDYwNTQxMjEsImF6cCI6ImZpUm1FMWNGeTR4YTVKQllXckQ1NUdRRzVsZ3h6NFZtIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.SYfn9KF9K2ajkJOn3rKaX_T5-VScFZvrc04CrYpCRvpiFYtejmaWd4zmHdm-Kkj7ZbCk-QAB4VBa5tiSVwj9u7IiHI31dBnLsN0wJN79ZX0dil_N2VwcRUsRnFBcuO3lYLR_WKK-hlOriww3ONBvqbnXHVXdrBg7Fs7l3Ir4TfUSGm3ZXUZnL5WEfvrotNSV1sazOzQkqYCB2YEnXYWLPR9PlcnPWBDaYVe2VXHXMqy9jsU8EJZQT5zjPup7NJqbafhVwoghJdP8EeYnACsrKvhussnk0_jVWjfI2bt0d-IEFyj3JmzqDaNZWTZa3AbabzxrYMsGZ_0Z4igNxFDp7Q

デコード後

{"alg":"RS256","typ":"JWT","kid":"o9iqPlS6sicpHxTXsHJxq"}
{"iss":"https://saku2saku.us.auth0.com/","sub":"fiRmE1cFy4xa5JBYWrD55GQG5lgxz4Vm@clients","aud":"https://saku2saku.example.com","iat":1605967721,"exp":1606054121,"azp":"fiRmE1cFy4xa5JBYWrD55GQG5lgxz4Vm","gty":"client-credentials
(電子署名部分は見てもしょうがないので省略)

もっと詳細に知りたい方はRFC 7519のドキュメントが参照できるので読んでみると良いかもです。
ただ今回のサンプルを動かすくらいならざっくりとした理解でも問題ないです。

Istioへの組み込み

認証APIもでき、JWTが発行できるようになり、JWTについての概要も理解できたところでいよいよIstioに認証・認可を組み込んでいきます。

認可の設定と動作検証

まずはAuth0で発行されるJWTをIstioのSidecarでInjectionされるenvoyが解釈できるように設定します。

RequestAuthenticationyamlファイルを作成します。

apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
  name: require-auth0-jwt
spec:
  selector:
    matchLabels:
      app: productpage
  jwtRules:
  - issuer: https://saku2saku.us.auth0.com/
    jwksUri: https://saku2saku.us.auth0.com/.well-known/jwks.json

ここで記述した issuer にはJWTの発行元を、 jwksUri はJWTを検証するための公開鍵情報のURIを指定します。 issuer には先程デコードした情報にあったとおり https://saku2saku.us.auth0.com/ を、 jwksUri にはAuth0のドキュメントにあるように https://YOUR_DOMAIN/.well-known/jwks.json つまり https://saku2saku.us.auth0.com/.well-known/jwks.json を指定します。 ファイルができたらkubectlコマンドで設定を適用しましょう。

$ kubectl apply -f ~/Desktop/auth0_RequestAuthentication.yaml
requestauthentication.security.istio.io/require-auth0-jwt created 

これでenvoyがauth0で作ったサンプルのAPIのJWTを解釈できるようになりました。
ただ、ここまでだけではまだIstioは何の制御もしてないため今までと変わらずにサンプルアプリケーションにアクセスできてしまいます。 試しに改めてBookinfo Applicationのページにアクセスしてみましょう。

f:id:rettydev:20201116001222p:plain
Bookinfo Applicationの画面

エラーになることなくアクセスできちゃいましたね。

では次にAuthorizationPolicyの設定ファイルを作成します。

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: policy
spec:
  selector:
    matchLabels:
      app: productpage
  action: ALLOW
  rules:
  - when:
    - key: request.auth.audiences
      values:
      - https://saku2saku.example.com

この設定ファイルでは以下のような設定をしています。

  • app: productpage のラベルを持つコンテナに対しての設定
  • JWTの request.auth.audienceshttps://saku2saku.example.com のリクエストであれば許可する
    • JWTがない、あるいはJWTの request.auth.audiences が期待するものと違った場合にはリクエストは許可されない
    • 先程のJWTの説明であったとおり、Auth0で作った認証APIが発行するJWTのペイロードには "aud":"https://saku2saku.example.com" が含まれるため一致して許可と判定されます

またAuthorizationPolicyのRuleですがこのような仕様になっています。

  1. 1つでもDENYポリシーにマッチするものがあればリクエストは拒否される
  2. ワークロードにマッチするALLOWポリシーが1つもない場合リクエストは許可される
  3. リクエストに対して、いずれかのALLOWポリシーがマッチすればリクエストは許可される
  4. 1, 2, 3のいずれにも該当しなければリクエストは拒否される

また、今回は request.auth.audiences を条件に記載しましたが、keyにはこちらの種別を指定可能なので実現したいものに合わせて選びましょう。

では先程のyamlを適用して確認してみましょう。

$ kubectl apply -f ~/Desktop/auth0_AuthroiztionPolicy.yaml
authorizationpolicy.security.istio.io/policy created

改めてBookinfo Applicationのページにアクセスしてみると RBAC: access denied と表示されてアクセスできなくなったことが確認できます。

f:id:rettydev:20201122001226p:plain

今度はJWTを設定した状態でアクセスしてみます。
ChromeであればModHeaderなどの拡張を使って先程発行されたTokenを設定します。 f:id:rettydev:20201126145109p:plain

設定したらもう一度再読み込みをしてみましょう。
無事Bookinfo Applicationのページが表示されるようになりましたね。 f:id:rettydev:20201126145127p:plain

おわりに

今回Istioのサンプルと認証・認可を試すにあたりIstioの本を買おうといろいろ探したものの、国内で販売しているKubernetes本ではIstioに関する説明はほとんど載っていませんでした(´・ω・`)
しょうがないので英語のIstio本を購入してみるものの、Istioの進化も早いせいか結構認可周りの話は最新よりちょっと古い感じでした...
色々調べたところJX通信社さんのこの記事が一番参考になりました。今回の記事はその内容に詳細手順とちょっとした説明を付加してみたものにはなります。

tech.jxpress.net

今回の内容を使うとアプリケーションは自身の処理に集中しつつ、Sidecarのほうでアクセスの許可を柔軟に設定できるようになるためぜひ活用してみたいところですね。

なお、今回の例に使ったModHeadersの拡張機能が有効になりっぱなしだと一部のWebサービスが正常に動かなくなりますのでご注意くださいませ。(筆者はまさにその問題が起きてアレッて小一時間ほどハマりました😇 )

参考記事