IstioのRequestAuthenticationとAuthorizationPolicyで個人的ハマりそうなポイント


# はじめに

こんにちは。[先日書いた記事の通り](https://qiita.com/u-kai/items/b92958661b6d4e8fcf22)、最近お家 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`などを指定することができます。詳しくは[公式ドキュメント](https://istio.io/latest/docs/reference/config/security/request_authentication/#JWTRule:~:text=valid%20JWT%20token.-,apiVersion%3A%20security.istio.io/v1%0Akind%3A%20RequestAuthentication%0Ametadata%3A,%2D%20from%3A%0A%20%20%20%20%2D%20source%3A%0A%20%20%20%20%20%20%20%20requestPrincipals%3A%20%5B%22*%22%5D,-The%20next%20example)を参照してください。

この機能を有効化すると、**`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 トークンによる認証がないリクエストを拒否するかを設定することで、認証認可を実装することができます。
わかりやすい例は[公式ドキュメント](https://arc.net/l/quote/iahpssqx)に書いてありますので参照してください。

### AuthorizationPolicy での評価順

これも個人的なハマりポイントです。
ちゃんと[ドキュメント](https://arc.net/l/quote/rtrwfqwe)を見れば書いてありますが、`AuthorizationPolicy`の評価順は以下になります。

1. CUSTOM ポリシーがリクエストに一致し、評価結果が拒否ならリクエストを拒否
1. DENY ポリシーが一致すればリクエストを拒否
1. ALLOW ポリシーがなければリクエストを許可
1. ALLOW ポリシーが一致すればリクエストを許可
1. リクエストを拒否

`DENY`が`ALLOW`よりも優先されるのは結構ありがちですが、`DENY`で拒否した評価の中に許可したいリクエストがある場合は、`ALLOW`するのではなく、`DENY`の条件から外すように設定する必要があります。
もしくは許可したくないものを`DENY`で定義するのではなく、許可したいものを`ALLOW`で定義するように設定する必要があります。

私がハマったのは以下のような設定をしたケースです。やりたかったことは、正常な jwt トークンがあるリクエスト以外は拒否することでした。

```yaml
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`は全ての条件がマッチしないと自身がマッチしないことになります。
(文章で書くとややこしいですね。。。)

例えば以下のような設定があるとします。説明上、どのように評価されるのかをコメント分で記載しています。

```yaml
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`を使って認証認可を実装する方法を紹介しました。

めちゃめちゃ便利な機能なのですが、個人的にはハマりポイントが多い印象です。
また、インデントのズレで設定ミスが生じるかつ、それが認証認可というセキュリティの要的要素に影響するので、アクセス制御を確認する自動テストは必須だと感じました。

最後に、今回書いた知見はほとんどが公式ドキュメントに記載されていることでした。
ハマった時間は多かったですが、公式ドキュメントをしっかり読むと一瞬で解決した、という経験が得ら、公式ドキュメントは大事だなとしみじみ感じました。
ぜひ公式ドキュメントも参考にしてみてください！

ここまで読んでいただきありがとうございました！

[^1]: 実際には`Istio`だけではなく、外部 User を認証する仕組みの組み合わせが必要です。私の場合は`Coginito`を外部 User 認証に利用し、jwt トークンを`Istio`で検証しています。
[^2]: ただし、Istio が有効化されている Workload に限ります。
[^3]: `AUDIT`と`CUSTOM`は別で話そうかと思います。
[^4]: Istio が「認証と認可は Java と JavaScript ぐらい違うのだ！」と、教えれくれているのかも知れませんね。。。
[^5]: 私はパブリッククラウドとして AWS をよく利用するのですが、AWS の IAM とは違うな〜という感想です。IAM は明示的に許可をしないものは全て拒否される一方、`AuthorizationPolicy`は明示的に拒否するとそれ以外は許可されるという感じが違うな〜と感じました。`ALLOW`だけの場合は`ALLOW`以外のことはできないので、`IAM`と似ているかと思います。