認可ポリシーはフィルターチェーンに翻訳される
一言でいうと
AuthorizationPolicyは、ワークロードのinboundリスナーのenvoy.filters.http.rbacフィルターになります。DENYポリシーは1つのフィルターに、ALLOWポリシーはもう1つのフィルターにまとまり、DENYフィルターが前に立ちます。2つのフィルターは連鎖していて、リクエストがアプリに届くには両方を通過する必要があります。dry-runは、同じポリシーがrulesではなくshadow_rulesに入るものです。
なぜ必要なのか
認可ルールをアプリごとにコードで書くと、言語ごとに違う実装になり、監査するときにどこに何があるのかわかりません。Istioは、これをプロキシに移しました。ルールはKubernetesリソースとして1か所に書き、実行は、すべてのPodの前にいるEnvoyが同じ方法で行います。
ところが、人が書くルールには「これは止め(DENY)、あれは許可する(ALLOW)」が混ざっていて、EnvoyのRBACフィルター1つは、actionを1つしか持てません。そのため、変換にルールが必要です。このルールを知らないと、本番でよく見る2つの事故を説明できません。ALLOWポリシーを1つ追加したら残りのトラフィックがすべて403になる事故と、パスだけを書いたDENYポリシー1つがデータベース接続まで切ってしまう事故です。
どう動くのか
Istioのドキュメントにある評価順序は、5行です。
| 順序 | 条件 | 判定 |
|---|---|---|
| 1 | CUSTOMポリシーが拒否 | 拒否 |
| 2 | DENYポリシーにマッチ | 拒否 |
| 3 | このワークロードにALLOWポリシーが1つもない | 許可 |
| 4 | ALLOWポリシーにマッチ | 許可 |
| 5 | それ以外 | 拒否 |
istiodは、この表をフィルターの連鎖に変換します。2行目はaction: DENYのRBACフィルター、4・5行目はaction: ALLOWのRBACフィルターです。ALLOWフィルターは「マッチするポリシーがあって初めて通過」なので、マッチしなければその場で拒否します。5行目が別に要らない理由です。3行目は、フィルターを作らないことで実装されます。RBACフィルターのrulesにポリシーが1つもないと、すべてのリクエストを拒否するため、ALLOWポリシーがないワークロードには、ALLOWフィルター自体を置きません。
フィルターの連鎖で、各フィルターにできることは、「次へ渡す」と「その場で拒否する」の2つだけです。次のフィルターを飛ばして、すぐに許可する道はありません。したがって、リクエストがアプリに届くには、DENYフィルターもALLOWフィルターも通過する必要があります。論理ではANDです。そのため、2つのフィルターの順序を入れ替えても、判定は同じです。順序が変わると違ってくるのは統計です。2つのフィルターが同じhttp.<stat_prefix>.rbac.allowed/deniedを一緒に増やすので、1つのリクエストが、最初のフィルターでallowedを増やし、2つ目でdeniedを増やすことがあります。
ポリシーの属性は、ほとんどそのまま変換されます。paths: ["/admin*"]はurl_path.path.prefix: /adminになり(だから/administratorにも当たります)、methods: ["GET"]は:methodヘッダーの完全一致になります。ポリシーごとにns[default]-policy[deny-admin]-rule[0]のような名前が付くので、拒否ログやconfig_dumpから、元のリソースをたどれます。
dry-runは、ポリシーにistio.io/dry-run: "true"アノテーションを付けるものです。istiodは、そのポリシーをshadow_rulesに入れます。RBACフィルターは、shadow側を評価して、shadow_allowed/shadow_deniedの統計と動的メタデータだけを残し、判定には使いません。rulesがないフィルターは、何も拒否しません。shadow_rules_stat_prefixを与えると、統計名の途中に、プレフィックスが挟まります。
現場での姿
ALLOWを1つ追加したら、すべて止まった。3行目のためです。ALLOWポリシーが0個のときは「すべて許可」だったワークロードが、1個になった瞬間に「マッチしたものだけ許可」に変わります。ヘルスチェックのパスや、別のサービスからの呼び出しが、新しいALLOWになければ、すぐに403になります。本文がRBAC: access deniedなら、プロキシが止めたもので、アプリの403とは、本文で見分けます。
DENYポリシーがTCPサービスを切った。DENYは、リクエストにない属性をマッチと見なします。止める側が、抜け穴を作らないための設計です。そのため、パスだけを書いたDENYは、パスという概念のないTCP接続をすべてマッチさせて、切ってしまいます。istioctl validateがこれを警告し、portsで絞るよう勧めます。終了コードは0なので、CIでは見逃しやすいです。
新しいポリシーをいきなり有効にするのは怖い。dry-runで先に入れ、shadow_deniedが、想定したリクエストでだけ増えるかを見てから、アノテーションを外します。本番のダッシュボードでこの統計を見るには、プレフィックスまで含めた正確な名前を知っている必要があります。
公式ドキュメント: Authorization Policy・Envoy RBAC filter
次のラボですること
DENYとALLOWのポリシーを書いてistioctlの警告を読んだ後、評価順序で4つのリクエストの結果を先に予測します。2つのポリシーを2つのRBACフィルターに変換してEnvoyで起動し、予測と突き合わせます。フィルターの順序を入れ替え、ALLOWフィルターを外し、DENYをshadow_rulesに移しながら、評価順序の3行を自分で確認します。