ポリシーを追加したら決済APIが開いた
目標
実際のIstioのサイドカーで、ワークロードのアイデンティティとJWTの主体の組み合わせを検証し、許可の範囲が広がってしまった事故を直します。
なぜ重要なのか
ファイル名やkubectl applyの成功だけでは、セキュリティを確認できません。送信元・トークン・メソッド・パス・ポートを変えた実際のリクエストで、正常な許可と迂回の拒否を一緒に確認します。4つの比較用サーバーは最後まで残るので、全体の採点のときにも再び検査します。
環境と安全の範囲
このVMの中のica-policyネームスペースだけを変更してください。本番のクラスターや外部のIdPではありません。k3s v1.35.8+k3s1とIstio 1.31.0が用意されていて、orders・strangerと4つのサーバーの実際のコンテナが実行されます。公開鍵は用意された素材を使い、実際のユーザートークンは入れません。unionは、事故を再現するためにわざと広く許可する隔離の対象です。セッションを終了すると、VMと作業ファイルが回収されます。
すべてのポリシーはsecurity.istio.io/v1を使い、リクエストのヘルパーはpython3 /opt/fixtures/ica-policy/runtime.py observe です。ヘルパーはポリシーを変更せず、実際のHTTPコードと本文を出力します。ポリシーを適用した直後は、伝播を待ってから再観測してください。採点は、2回連続で同じ結果が出て初めて通過します。
ステップ
- ica-policyネームスペースのPodの一覧を取得し、apiVersion・kindと、各itemsのmetadata.name・metadata.uidだけを抜き出して、/root/ica-policy/inventory.jsonに保存してください。サイドカーの詳細を含む全体の出力は、入力の制限128KiBを超えることがあります。baseline・union・intersection・scoped・orders・strangerの6つが実際にReadyで、サイドカーがある必要があります。Podを作り直した場合は、UIDも記録し直してください。
- /root/ica-policy/baseline.yamlに、ica-policyのallow-baselineというAuthorizationPolicyを作成して適用してください。app=baseline、action=ALLOW、単一のルールのsource.principalsはcluster.local/ns/ica-policy/sa/orders 1つ、operationはPOSTと/v1/chargesが1つずつです。JWTの条件は入れないでください。baselineの観測で、ordersの正常・トークンなしは200、strangerの正常なトークンは403であることを確認してください。
- 隔離されたunionの対象でだけ、意図的に誤った構成を再現します。/root/ica-policy/union.yamlに、ica-policyのjwt-unionというAuthorizationPolicyを作成・適用してください。app=union、action=ALLOW、rules 1つのfrom 1つに、requestPrincipals=[https://issuer.example.invalid/*]だけを置き、toは入れないでください。既存のallow-unionは保持してください。unionの観測で、トークンのないorders、正常なトークンのstranger、正常なordersのGET /admin/testまで200であることを確認してください。本番に適用しないでください。
- /root/ica-policy/intersection.yamlに、ica-policyのallow-intersectionのポリシーを作成・適用してください。app=intersection、action=ALLOW、単一のルールの同じsourceに、principals=[cluster.local/ns/ica-policy/sa/orders]と、requestPrincipals=[https://issuer.example.invalid/*]を一緒に置きます。同じルールのoperationは、POSTと/v1/chargesだけを許可します。既存のallow-intersectionをこの内容に置き換えますが、ほかのALLOWは追加しないでください。正常なordersだけが200で、トークンなし・stranger・別のパスは403である必要があります。
- /root/ica-policy/scoped.yamlに、ica-policyのjwt-scopedのポリシーを作成・適用してください。app=scoped、action=DENY、単一のルールに、from.source.notRequestPrincipals=[*]とto.operation.ports=["8080"]を一緒に置きます。既存のallow-scopedはそのまま保持し、追加の条件・ルール・dry-runは入れないでください。正常なordersの決済の呼び出しだけが200で、トークンなし・stranger・別のパスは403である必要があります。
- intersectionの初期のJWT設定は、payments-apiとother-apiのaudienceをどちらも許可します。audienceの観測でこれを確認したあと、/root/ica-policy/authentication.yamlに、ica-policyのjwt-intersectionというRequestAuthenticationを作成・適用してください。app=intersection、jwtRules 1つのissuer=https://issuer.example.invalid, audiences=[payments-api], jwksは、/opt/fixtures/ica-policy/public-jwks.jsonの実際の公開鍵のJSON文字列です。jwksUriや追加の発行者は入れないでください。正常なトークンは200、別のaudienceは403、別の発行者・期限切れ・形式エラーは401であることを、本文と一緒に確認してください。
- portsの観測コマンドのJSON出力を、/root/ica-policy/ports.jsonに保存してください。scopedにトークンなしでPOST /v1/chargesを呼び出すordersの、8080の応答は403/RBAC拒否、8081の応答は200/synthetic-orderである必要があります。観測結果を、手で成功の値に書き換えないでください。採点は、現在のリクエストをもう一度送って突き合わせます。
- /root/ica-policy/incident.jsonに、allow_composition(ポリシー間のORまたはAND)、source_fields(同じsourceのフィールド間のORまたはAND)、jwt_missing_fix(scopedで使ったaction)、deny_ports(保護した文字列のポートの配列)、audience_error(JWTのaudience拒否の層: jwt_authnまたはrbac)、principal_error(ワークロードのアイデンティティの認可の拒否の層: jwt_authnまたはrbac)を書いてください。正常なintersectionとscopedのポートの境界を保持してください。
参考
- 保存したYAMLと実際に適用されたポリシーの両方が合っている必要があります。kubectl -n ica-policy get authorizationpolicy,requestauthentication -o yamlで突き合わせてください。
- 採点は、受講生のファイルを直しません。ファイルに成功コードだけを書いておいても、実際のリクエストが違えば失敗します。
- 8080と8081は、このラボではHTTPです。この結果を、TCP全体やアンビエントメッシュの検証に広げないでください。
現在のメッシュのPodのアイデンティティを記録する
ica-policyネームスペースのPodの一覧を取得し、apiVersion・kindと、各itemsのmetadata.name・metadata.uidだけを抜き出して、/root/ica-policy/inventory.jsonに保存してください。サイドカーの詳細を含む全体の出力は、入力の制限128KiBを超えることがあります。baseline・union・intersection・scoped・orders・strangerの6つが実際にReadyで、サイドカーがある必要があります。Podを作り直した場合は、UIDも記録し直してください。
名前が同じだけで、同じPodだとは限りません。UIDとReady、実際のistio-proxyコンテナを区別して見てください。
トークンのない正常なワークロードが通過する基準線
/root/ica-policy/baseline.yamlに、ica-policyのallow-baselineというAuthorizationPolicyを作成して適用してください。app=baseline、action=ALLOW、単一のルールのsource.principalsはcluster.local/ns/ica-policy/sa/orders 1つ、operationはPOSTと/v1/chargesが1つずつです。JWTの条件は入れないでください。baselineの観測で、ordersの正常・トークンなしは200、strangerの正常なトークンは403であることを確認してください。
正常なJWTかどうかと、送信元のワークロードは、別の軸です。baselineは、送信元・メソッド・パスだけを制限します。
JWTの許可を別に付けて、事故を再現する
隔離されたunionの対象でだけ、意図的に誤った構成を再現します。/root/ica-policy/union.yamlに、ica-policyのjwt-unionというAuthorizationPolicyを作成・適用してください。app=union、action=ALLOW、rules 1つのfrom 1つに、requestPrincipals=[https://issuer.example.invalid/*]だけを置き、toは入れないでください。既存のallow-unionは保持してください。unionの観測で、トークンのないorders、正常なトークンのstranger、正常なordersのGET /admin/testまで200であることを確認してください。本番に適用しないでください。
新しい許可は、既存の許可をより厳しくするフィルターではありません。どの条件が各リクエストを通過させるのかを、別々に説明してみてください。
同じsourceで2つのアイデンティティを結合する
/root/ica-policy/intersection.yamlに、ica-policyのallow-intersectionのポリシーを作成・適用してください。app=intersection、action=ALLOW、単一のルールの同じsourceに、principals=[cluster.local/ns/ica-policy/sa/orders]と、requestPrincipals=[https://issuer.example.invalid/*]を一緒に置きます。同じルールのoperationは、POSTと/v1/chargesだけを許可します。既存のallow-intersectionをこの内容に置き換えますが、ほかのALLOWは追加しないでください。正常なordersだけが200で、トークンなし・stranger・別のパスは403である必要があります。
2つのアイデンティティを、fromの別々の項目に分けて入れることと、同じsourceに入れることは、違います。
JWTなしのDENYをHTTPポートに限定する
/root/ica-policy/scoped.yamlに、ica-policyのjwt-scopedのポリシーを作成・適用してください。app=scoped、action=DENY、単一のルールに、from.source.notRequestPrincipals=[*]とto.operation.ports=["8080"]を一緒に置きます。既存のallow-scopedはそのまま保持し、追加の条件・ルール・dry-runは入れないでください。正常なordersの決済の呼び出しだけが200で、トークンなし・stranger・別のパスは403である必要があります。
DENYだけを残すと、拒否の条件に当てはまらないリクエストの最小権限が、なくなることがあります。既存のALLOWとブロックの範囲を、一緒に見てください。
別のAPI用のトークンの再利用を防ぐ
intersectionの初期のJWT設定は、payments-apiとother-apiのaudienceをどちらも許可します。audienceの観測でこれを確認したあと、/root/ica-policy/authentication.yamlに、ica-policyのjwt-intersectionというRequestAuthenticationを作成・適用してください。app=intersection、jwtRules 1つのissuer=https://issuer.example.invalid, audiences=[payments-api], jwksは、/opt/fixtures/ica-policy/public-jwks.jsonの実際の公開鍵のJSON文字列です。jwksUriや追加の発行者は入れないでください。正常なトークンは200、別のaudienceは403、別の発行者・期限切れ・形式エラーは401であることを、本文と一緒に確認してください。
鍵は新しく作りません。用意された公開鍵を文字列として挿入し、audienceの許可リストだけを、求められる範囲に絞ってください。
保護したポートと残したポートを区別する
portsの観測コマンドのJSON出力を、/root/ica-policy/ports.jsonに保存してください。scopedにトークンなしでPOST /v1/chargesを呼び出すordersの、8080の応答は403/RBAC拒否、8081の応答は200/synthetic-orderである必要があります。観測結果を、手で成功の値に書き換えないでください。採点は、現在のリクエストをもう一度送って突き合わせます。
portとtargetPortを混同せず、適用されたポリシーの文字列のportsの一覧を確認してください。このステップは、8081まで保護する課題ではありません。
ポリシーの組み合わせによる事故の原因を報告する
/root/ica-policy/incident.jsonに、allow_composition(ポリシー間のORまたはAND)、source_fields(同じsourceのフィールド間のORまたはAND)、jwt_missing_fix(scopedで使ったaction)、deny_ports(保護した文字列のポートの配列)、audience_error(JWTのaudience拒否の層: jwt_authnまたはrbac)、principal_error(ワークロードのアイデンティティの認可の拒否の層: jwt_authnまたはrbac)を書いてください。正常なintersectionとscopedのポートの境界を保持してください。
403をすべて同じエラーとして扱わないでください。応答の本文と、どのポリシーがそのリクエストを拒否したかを、結び付けて報告してください。