はじめに
こんにちは。先日書いた記事の通り、最近お家 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
と似ているかと思います。 ↩