はじめに
こんにちは。先日書いた記事の通り、最近お家 k8s にハマっており、いろんなエコシステムを試しています。
今回はその中でも Istio の RequestAuthentication と AuthorizationPolicy を使って認証認可を実装する方法を紹介します。
RequestAuthentication と AuthorizationPolicy は、簡単に認証認可の仕組みを実装できる機能でとても便利です。
この機能を適切に有効化することで、アプリケーションのセキュリティを向上させることができます。
実際私もお家 k8s から外部に API を公開していますが、RequestAuthentication と AuthorizationPolicy を使って認証認可を実装しているため、安心して運用できています。1
ただ、この機能を意図通りに実装するのになかなかハマったため、今回はハマりポイントも含めて記載していきます。
そもそも認証認可とは
これはよく言われることですが、簡単に認証認可を説明すると以下の通りです。
- 認証(Authentication) :誰であるかを確認すること
- 認可(Authorization):何が許可されてるかを確認すること
大体の場合は認証を行い、その後で認可を行うことがほとんどです。
認証、認可なしに API を公開することは、データ漏洩や過剰なリクエスト実行などのリスクがあるため、基本的には避けるべきです。
RequestAuthentication とは
RequestAuthentication とは Istio が提供する認証機能で、リクエストを受け取る際にリクエストに含まれている jwt トークンを検証して認証する機能です。
設定できる項目は大まかに、何に対して機能を有効化するか、と、どのような jwt トークンを検証するのかというシンプルなものです。
何に対して機能を有効化するか、というところでは、labelによる指定か、ServiceまたはGatewayの指定か選べます。これらの指定をしないとデプロイされるnamespace全体に対して有効化されます。2
jwt トークンの検証についてはissuerやjwksUriなどを指定することができます。詳しくは公式ドキュメントを参照してください。
この機能を有効化すると、issuerで設定した jwt トークンとは異なる、または不正は jwt トークンを持つリクエストは 401 エラーを返すようになります。
AuthorizationPolicy とは
AuthorizationPolicy とは Istio が提供する認可機能で、リクエストを受け取る際にリクエストの内容を解釈して、許可されたリクエストかどうかを判断する機能です。
設定できる項目は大まかに、何に対して機能を有効化するか、と、どのようなリクエストに対してどのようなアクションをするかというものです。
何に対して機能を有効化するかはRequestAuthenticationと同様で、label指定か、ServiceまたはGatewayの指定、もしくはnamespace全体に対して有効化されます。
アクションには以下の項目があります。
-
ALLOW: 許可 -
DENY: 拒否 -
AUDIT: 監査ログ出力 -
CUSTOM: カスタムポリシー
主に利用するアクションはALLOWとDENYかと思います。3
指定しなかった際のデフォルトはALLOWになっています。
どのようなリクエストに対して、というところでは、多くのコントロールが可能です。
例えば、宛先のpathやmethod、送信元の IP アドレス、jwt のissuer,subjectの組み合わせやnamespaceなどと幅広い設定が可能です。
ハマりどころ
RequestAuthentication だけではアクセス制御が十分ではない
これは個人的な一番のハマりポイントです。
RequestAuthentication では jwt トークンの検証を行うことができますが、jwt トークンが存在していない場合は、そのリクエストを許可してしまいます。
認証情報がない場合は認証しないということですね。
RequestAuthentication だけ実装し、AuthorizationPolicy を実装していないと認可判定をしない状態になります。
そうなると、不正な jwt トークンを指定したリクエストに対しては RequestAuthentication で 認証が失敗するので、401 エラーを返してくれますが、そもそも認証情報がないリクエストに対してはエラーを返さず、そのまま認可もない状態でリクエストが通ってしまいます。4
そのため、RequestAuthentication と AuthorizationPolicy を併用して実装することが重要です。
AuthorizationPolicy で jwt トークンが正常なリクエストを許可するか、jwt トークンによる認証がないリクエストを拒否するかを設定することで、認証認可を実装することができます。
わかりやすい例は公式ドキュメントに書いてありますので参照してください。
AuthorizationPolicy での評価順
これも個人的なハマりポイントです。
ちゃんとドキュメントを見れば書いてありますが、AuthorizationPolicyの評価順は以下になります。
- CUSTOM ポリシーがリクエストに一致し、評価結果が拒否ならリクエストを拒否
- DENY ポリシーが一致すればリクエストを拒否
- ALLOW ポリシーがなければリクエストを許可
- ALLOW ポリシーが一致すればリクエストを許可
- リクエストを拒否
DENYがALLOWよりも優先されるのは結構ありがちですが、DENYで拒否した評価の中に許可したいリクエストがある場合は、ALLOWするのではなく、DENYの条件から外すように設定する必要があります。
もしくは許可したくないものをDENYで定義するのではなく、許可したいものをALLOWで定義するように設定する必要があります。
私がハマったのは以下のような設定をしたケースです。やりたかったことは、正常な jwt トークンがあるリクエスト以外は拒否することでした。
apiVersion: security.istio.io/v1
kind: RequestAuthentication
metadata:
name: req-authn
namespace: prod
spec:
jwtRules:
- issuer: "issuer-foo"
jwksUri: https://example.com/.well-known/jwks.json
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: istio-system
spec:
action: DENY
rules:
- from:
- source:
notRequestPrincipals: ["*"]
上の設定のnotRequestPrincipals: ["*"]とは、リクエストに含まれる jwt トークンによって認証されたユーザー以外のリクエストという意味になります。
それに対してaction: DENYとしているため、正常な jwt トークンがないリクエストは拒否され、jwt トークンが正常なリクエストは許可されるはずです。
もちろん、上記の通り動作します。正常な jwt トークンがあればリクエストは許可されます。
ただし、正常なリクエストだが、jwt トークンがないリクエストがある場合、そのリクエストも拒否されてしまいます。
私が引っかかったのが、CORS の preflight リクエストです。
preflight リクエストはHTTPのOPTIONSメソッドで実行されるリクエストで、基本的に jwt トークンを含まないため、この設定だと拒否されてしまいます。(どうすれば良いかは次のセクションのハマりポイントと一緒に説明します。)
以上のことから、考慮漏れによってリクエストが拒否されることがあるので注意が必要です。
また、ALLOWだけ設定した場合、ALLOWとして記述していないリクエストは拒否されるというのもハマりポイントでした。
実行順の 3 にある通り、CUSTOMとDENYによって拒否されず、ALLOWの設定がない場合はリクエストは許可されます。
ただし、そこにALLOWが加わると、評価順の 5 に書かれている通り、最終的に評価されなかったリクエストは拒否されることになります。
私の感覚ですが、
-
DENYに指定されなかったものは暗黙的に許可される -
ALLOWに指定されなかったものは暗黙的に拒否される
といった感じです。
暗黙的に逆の効果が反映されるので、私の感覚とは異なりハマりましたし、今後もハマりそうです。5
AuthorizationPolicy での評価判定
上の評価順と少し似ているのですが、AuthorizationPolicyの評価判定もハマりポイントの一つかと思います。
評価判定は、spec.rulesに記載されているルールで決まります。
rulesはRuleオブジェクトの配列で、どれか一つのRuleでもマッチするとspec.actionに記載されているアクションが実行されます。
そしてRuleにはfrom,to,whenの 3 つのフィールドがあり、全てがマッチするとRuleがマッチします。
fromはSourceオブジェクトの配列、toはOperationオブジェクトの配列、whenはConditionオブジェクトの配列です。
Sourceはリクエスト送信元の情報に関する条件、Operationはリクエストの宛先の情報に関する条件、ConditionはIstioがサポートしている属性についての条件を記述できます。
fromとtoは自身の配列のどれか一つでもマッチすればfrom,to自身がマッチしたことになり、whenは全ての条件がマッチしないと自身がマッチしないことになります。
(文章で書くとややこしいですね。。。)
例えば以下のような設定があるとします。説明上、どのように評価されるのかをコメント分で記載しています。
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: jwt-auth-policy-allow-requests
spec:
targetRefs:
- name: backend-gateway
kind: Gateway
group: gateway.networking.k8s.io
action: ALLOW
rules:
############# OR #############
- from: # 一つ目のRule
- source:
requestPrincipals: ["*"]
########### AND ##########
- to: # 二つ目のRule
####### OR #######
- operation:
### AND ####
paths: ["/health"]
methods: ["GET"]
### AND ####
- operation:
paths: ["/ready"]
methods: ["GET"]
####### OR #######
from:
- source:
ipBlocks: ["xxx.xxx.xxx.xxx"]
########### AND ##########
- to: # 三つ目のRule
- operation:
methods: ["OPTIONS"]
############# OR #############
最初のRuleはfromのみかつその中の条件も一つで、送信元が何らかの 正常な jwt トークンを持っている場合にマッチします。
rulesはどれか一つのRuleでもマッチすればspec.actionが実行されるため、正常な jwt トークンを持っているリクエストは上のAuthorizationPolicyで許可(ALLOW)されます。
次のRuleはfromとtoが存在し、それぞれがマッチしないとRule自体がマッチしません。
そしてこのRuleには上のRule(正常な jwt トークンを持っているリクエスト)がマッチしなかったリクエストに対して評価されます。
toには二つのOperationオブジェクトが存在しており、どちらかがマッチしていればto自体がマッチします。
一つ目のOperationはpathが/healthかつmethodがGETのリクエストにマッチし、二つ目のOperationはpathが/readyかつmethodがGETのリクエストにマッチします。
そしてfromには一つのSourceオブジェクトが存在しており、そこでは、特定の IP アドレスからのリクエストにマッチするようになっています。
つまり、二つ目のRuleは/healthか/readyのGETメソッドのリクエストかつ特定の IP アドレスからのリクエストに対して許可されるようになっています。
最後のRuleはtoのみで、OperationのmethodがOPTIONSのリクエストに対して許可されるようになっています。
まとめると、以下のAuthorizationPolicyは以下のような挙動をします。
- 正常な jwt トークンを持っているリクエストは許可される
- 特定の IP アドレスからの
/healthか/readyのGETメソッドのリクエストは許可される -
OPTIONSメソッドのリクエストは許可される
このAuthorizationPolicyを利用すれば、前のセクションで説明したCORSのpreflightリクエストだけは許可しつつ、正常な jwt トークンを持っていないリクエストを拒否することができます。
柔軟な設定ができる分、複雑で評価ロジックをミスしやすく、ハマりどころになりやすいと感じました。
また、ちゃんと適切にアクセス制御が実装されているかのテストがないと、評価順を理解していてもミスは生じ得るなと感じました。
例えばtoやfromを同じRuleに書くか、別のRuleに書くかで全く挙動が変わりますが、これはインデントが違うだけで起こりうることですので注意が必要です。
もしかすると、一つのAuthorizationPolicyに複数のRuleを書くこと自体が適切ではないのかも、と思いましたが、そうなるとAuthorizationPolicy自体が増えることになり、それはそれで管理が大変になるかもしれません。
ここら辺のベストプラクティスはまだまだ模索中です。
終わりに
今回はIstioのRequestAuthenticationとAuthorizationPolicyを使って認証認可を実装する方法を紹介しました。
めちゃめちゃ便利な機能なのですが、個人的にはハマりポイントが多い印象です。
また、インデントのズレで設定ミスが生じるかつ、それが認証認可というセキュリティの要的要素に影響するので、アクセス制御を確認する自動テストは必須だと感じました。
最後に、今回書いた知見はほとんどが公式ドキュメントに記載されていることでした。
ハマった時間は多かったですが、公式ドキュメントをしっかり読むと一瞬で解決した、という経験が得ら、公式ドキュメントは大事だなとしみじみ感じました。
ぜひ公式ドキュメントも参考にしてみてください!
ここまで読んでいただきありがとうございました!
-
実際には
Istioだけではなく、外部 User を認証する仕組みの組み合わせが必要です。私の場合はCoginitoを外部 User 認証に利用し、jwt トークンをIstioで検証しています。 ↩ -
ただし、Istio が有効化されている Workload に限ります。 ↩
-
AUDITとCUSTOMは別で話そうかと思います。 ↩ -
Istio が「認証と認可は Java と JavaScript ぐらい違うのだ!」と、教えれくれているのかも知れませんね。。。 ↩
-
私はパブリッククラウドとして AWS をよく利用するのですが、AWS の IAM とは違うな〜という感想です。IAM は明示的に許可をしないものは全て拒否される一方、
AuthorizationPolicyは明示的に拒否するとそれ以外は許可されるという感じが違うな〜と感じました。ALLOWだけの場合はALLOW以外のことはできないので、IAMと似ているかと思います。 ↩