ゼロトラストのポリシーを積み上げる
目標
mTLSの強制と認可ポリシーを順番に積み上げながら、評価順序(CUSTOM → DENY → ALLOW)と、「ALLOWが1つでもあるとデフォルト拒否に反転する」という性質を、マニフェストで体得します。
なぜ重要なのか
メッシュのセキュリティ導入が失敗する理由は、ほとんどの場合、順序を飛ばすことです。平文の送信元を整理せずにSTRICTを有効にすると、サイドカーのないクライアントがただちに切断され、コールグラフを知らないままデフォルト拒否を敷くと、何を開けるべきか分からないまま障害が起きます。
特に忘れやすい事実が1つあります。principalsはmTLSの証明書から得られるので、PERMISSIVE状態の平文のリクエストにはprincipalが空で、そうなるとどんなprincipalsの条件にもマッチしません。「ポリシーは間違いなく合っているのに403」の、最もよくある原因です。そのため、このラボは、mTLS → デフォルト拒否 → 最小権限 → セーフティネット → エンドユーザー認証の順序を、そのままたどります。
このラボは、kwokベースのAPIオブジェクト・マニフェスト設計のラボです。KubernetesのオブジェクトはAPIに保存されますが、実際のpaymentsコンテナ・Envoy・JWT検証サーバーが実行されるわけではありません。Istioのポリシーは/root/ica-security/の下にファイルとして作成し、HTTPの応答や暗号化の動作は採点しません。例のIdPのアドレスには接続しません。
ステップ8は、既存のALLOWを維持したまま、8080ポートのJWTのないリクエストにDENYを適用する設計です。別のJWTのALLOWは、2つの条件のANDではなく、許可経路のORを作るので使いません。ほかのポートでのJWTの必須化や、メトリクスへのアクセスの許可を保証するポリシーではありません。
ステップ
- ネームスペース
ica-securityを作成し、ラベルistio-injection=enabledを付けてください。 - ネームスペース
ica-securityに、ServiceAccountpayments-apiとorders-apiを作成してください。 - 同じネームスペースに、Deployment
payments-apiをデプロイしてください。Podのラベルはapp=payments-api、serviceAccountNameはpayments-api、イメージはnginx:1.27-alpineです。そして、Servicepayments-apiを、ポート8080(名前http)と9090(名前http-metrics)の2つで作成してください。 /root/ica-security/pa-mesh.yamlにPeerAuthenticationを作成してください。名前default、ネームスペースistio-system、mtls.mode: STRICTで、selectorは置きません。続けて、/root/ica-security/pa-workload.yamlに、ネームスペースica-security、selectorapp: payments-api、mtls.mode: STRICTでありながら、portLevelMtlsでポート9090だけがPERMISSIVEのポリシーを作成してください。/root/ica-security/ap-deny-all.yamlにAuthorizationPolicyを作成してください。名前default-deny、ネームスペースica-security、specは空のオブジェクトです。/root/ica-security/ap-allow-orders.yamlにAuthorizationPolicyを作成してください。selectorapp: payments-api、action: ALLOWで、ルール1つに、from.source.principalsはcluster.local/ns/ica-security/sa/orders-api、to.operation.methodsはPOST、to.operation.pathsは/v1/chargesです。/root/ica-security/ap-deny-admin.yamlにAuthorizationPolicyを作成してください。selectorapp: payments-api、action: DENYで、ルールはto.operation.pathsに/admin/*が1つだけで、fromは置きません。metadata.annotationsにistio.io/dry-run: "true"を付けてください。/root/ica-security/ra-jwt.yamlにRequestAuthenticationを作成してください。名前payments-api-jwt、ネームスペースica-security、selectorapp: payments-api、jwtRulesは1つで、issuerhttps://idp.example.com/、jwksUrihttps://idp.example.com/.well-known/jwks.json、audiences[payments-api]です。続けて、/root/ica-security/ap-require-jwt.yamlに、名前require-jwt、同じネームスペースとselector、action: DENYのAuthorizationPolicyを作成してください。ルール1つのfrom.source.notRequestPrincipalsは["*"]、同じルールのto.operation.portsは["8080"]です。追加のfrom・to・rulesの条件とdry-runは置きません。ステップ6の最小権限のALLOWは、そのまま維持してください。
参考
- SPIFFE IDを
principalsに書くときは、spiffe://のプレフィックスを省略して、cluster.local/ns/.../sa/...の形で書きます。 notRequestPrincipals: ["*"]は、検証されたJWTの主体がないリクエストを選びます。DENYはALLOWより先に評価されます。- DENYでHTTPの属性を使うと、TCPのリクエストの欠落した属性にもマッチすることがあります。この課題は、対象ポートを8080に限定します。
- 同じsourceの中にprincipalsとrequestPrincipalsを一緒に置くのも、有効なAND設計です。別々のALLOWポリシーに分けることと区別してください。
- よくある間違い1: デフォルト拒否を作るときに、
specキー自体を抜くことです。そうすると、ポリシーが意図どおりに解釈されません。 - よくある間違い2: メッシュ全体のPeerAuthenticationにselectorを付けることです。その瞬間、全体ではなくワークロードのポリシーになります。
- dry-runのアノテーションの値は、文字列
"true"です。引用符なしで書くとブール値になり、アノテーションのルールに反します。
セキュリティのラボ用のネームスペースを作成する
ネームスペースica-securityを作成し、ラベルistio-injection=enabledを付けてください。
サイドカーがなければ、mTLSも認可もかけられません。自動注入のラベルを付けてください。
ワークロードごとの専用ServiceAccountを作成する
ネームスペースica-securityに、ServiceAccount payments-apiとorders-apiを作成してください。
SPIFFE IDの最後の欄がServiceAccountです。複数のワークロードがdefaultを共有すると、ポリシーで互いを区別できません。
ServiceAccountを付けたワークロードをデプロイする
同じネームスペースに、Deployment payments-apiをデプロイしてください。Podのラベルはapp=payments-api、serviceAccountNameはpayments-api、イメージはnginx:1.27-alpineです。そして、Service payments-apiを、ポート8080(名前http)と9090(名前http-metrics)の2つで作成してください。
PodのspecにserviceAccountNameを明示して初めて、そのアイデンティティで証明書が発行されます。Serviceには、アプリケーションのポートとメトリクスのポートを一緒に開けてください。
メッシュ全体のSTRICTとポートの例外
/root/ica-security/pa-mesh.yamlにPeerAuthenticationを作成してください。名前default、ネームスペースistio-system、mtls.mode: STRICTで、selectorは置きません。続けて、/root/ica-security/pa-workload.yamlに、ネームスペースica-security、selector app: payments-api、mtls.mode: STRICTでありながら、portLevelMtlsでポート9090だけがPERMISSIVEのポリシーを作成してください。
メッシュ全体のポリシーは、ルートネームスペースに決まった名前で置き、selectorは付けません。selectorが付いた瞬間に、ワークロードのポリシーになります。例外のポートは、portLevelMtlsで狭く指定してください。
ネームスペースのデフォルト拒否を作成する
/root/ica-security/ap-deny-all.yamlにAuthorizationPolicyを作成してください。名前default-deny、ネームスペースica-security、specは空のオブジェクトです。
ルールが1つもないALLOWポリシーが、そのまま全面拒否です。specフィールド自体を抜いてはいけず、空の状態で置く必要があります。
コールグラフのとおりにだけ開ける
/root/ica-security/ap-allow-orders.yamlにAuthorizationPolicyを作成してください。selector app: payments-api、action: ALLOWで、ルール1つに、from.source.principalsはcluster.local/ns/ica-security/sa/orders-api、to.operation.methodsはPOST、to.operation.pathsは/v1/chargesです。
principalsは、トラストドメインから始まるパスの形式です。メソッドとパスは、to.operationの下に入ります。このステップでは、ServiceAccountの条件だけを使ってください。
管理用のパスをDENYでもう一重に防ぐ
/root/ica-security/ap-deny-admin.yamlにAuthorizationPolicyを作成してください。selector app: payments-api、action: DENYで、ルールはto.operation.pathsに/admin/*が1つだけで、fromは置きません。metadata.annotationsにistio.io/dry-run: "true"を付けてください。
DENYはALLOWより先に評価されるので、ALLOWのミスを防ぐセーフティネットになります。送信元とは無関係に防ぐ必要があるので、fromは置かないでください。本番に上げる前の段階なので、シャドウ評価のアノテーションも付けます。
エンドユーザーのトークンの必須化
/root/ica-security/ra-jwt.yamlにRequestAuthenticationを作成してください。名前payments-api-jwt、ネームスペースica-security、selector app: payments-api、jwtRulesは1つで、issuer https://idp.example.com/、jwksUri https://idp.example.com/.well-known/jwks.json、audiences [payments-api]です。続けて、/root/ica-security/ap-require-jwt.yamlに、名前require-jwt、同じネームスペースとselector、action: DENYのAuthorizationPolicyを作成してください。ルール1つのfrom.source.notRequestPrincipalsは["*"]、同じルールのto.operation.portsは["8080"]です。追加のfrom・to・rulesの条件とdry-runは置かないでください。ステップ6の最小権限のALLOWは、そのまま維持してください。
RequestAuthenticationはトークンの検証です。別のALLOWは既存の許可と合わさるので、必須化ではありません。JWTの主体がないリクエストをDENYで先に拒否し、対象ポートは文字列の8080に限定してください。