新しいポリシーをかける前に、誰が止められるかを先に見る
目標
本物のCilium 1.20.1(enable-policy=default)で、ポリシーがいつから何をブロックするのかを確認します。方向ごとのデフォルト拒否、エンドポイントの監査モードで事前にかけてみること、監査解除後の実際のブロック、ingress: [{}]の正反対の意味、enableDefaultDeny=false、2種類のポリシーが1つのリポジトリに入る様子を、エンドポイントの適用列・実際のリクエスト・Hubbleの判定とあわせて見ます。
なぜ重要なのか
運用中のサービスに初めてegressポリシーをかける瞬間が、最も危険です。ドキュメントになかった依存関係(このラボのlegacy)があると、その呼び出しがすぐに切れます。Ciliumの監査モードは、判定を計算しても拒否はせず、フローにAUDITEDとして残すので、誰がブロックされるのかを先に見てから強制適用できます。
ただし、監査モードはエンドポイント単位なので、すでに強制されていた方向まで緩んでしまいます。また、同じ形のYAMLでも、KubernetesのNetworkPolicyとCiliumNetworkPolicyでは意味が異なることがあります。適用列がEnabledだからといって、デフォルト拒否が有効になっている保証もありません。こうした違いは、ドキュメントを読むだけでは混同しやすいので、実際にリクエストを送って確認します。
クラスター全体のモードをalwaysに変えると、ポリシーのないエンドポイント(corednsなど)までブロックされます。開発VMで元に戻したあと、サービスが復帰するまでに約70秒かかるため、このラボでは変更しません。監査モードも、エンドポイント1つだけに有効にします。
環境の準備に約5分かかります。セッションが終わると、/root/cca-auditのファイルは消えます。
ステップ
kubectl apply -f /opt/fixtures/cca-audit/workload.yamlで、cca-auditに、Pod api・ledger・legacy・frontend・batch(いずれも8080のサーバー)と、同じ名前のServiceを起動してください。ポリシーをかける前に、/root/cca-audit/baseline.jsonを作成します。podsの下の5つのアプリ名ごとに、uid(Pod)、endpoint_id、identity(CiliumEndpointのstatus)、ingress・egress(agentのcilium-dbg endpoint listのPOLICY列の値)を記録し、最上位には、batch_to_api(batch Podからのリクエスト(http://api:8080/)のHTTPコードの文字列で、応答がなければ"000")とrecorded_at(記録した瞬間のUTC時刻、date -u +%Y-%m-%dT%H:%M:%SZ)を記録します。- cca-auditにCiliumNetworkPolicy
api-ingressを作成してください。endpointSelectorはapp=api、ingressは1項目(fromEndpointsはapp=frontend、toPortsはTCP "8080")で、egressは置きません。適用が反映されたら、/root/cca-audit/direction.jsonに、frontend_to_api、batch_to_api、api_to_legacy(各リクエストのHTTPコードの文字列で、応答がなければ"000")と、api_ingress・api_egress(apiエンドポイントのPOLICY列の値)、recorded_at(記録した瞬間のUTC時刻)を記録します。ステップ3のegressポリシーよりも先に記録する必要があります。 - apiエンドポイントで監査モードを有効にしてください(
cilium-dbg endpoint config <api 번호> PolicyAuditMode=Enabled、プレースホルダーはapiのエンドポイント番号です)。そのあと、CNPapi-egressを適用します。endpointSelectorはapp=api、egressは2項目で、(1) toEndpointsはapp=ledger、TCP "8080"、(2) toEndpointsはk8s:io.kubernetes.pod.namespace: kube-system・k8s:k8s-app: kube-dns、ポート"53" protocol ANYです。api→ledger、api→legacy、batch→apiにリクエストを送ったあと、agentの中でhubble observeでcca-auditのフローを読み取り、/root/cca-audit/audit.jsonに記録します。記録する項目は、endpoint_id、audit_mode、api_ingress・api_egress(POLICY列)、api_to_legacy・batch_to_api(コードの文字列)、flows(api→legacyとbatch→apiのAUDITEDフローが含まれるcompact出力の行のリスト)です。 - apiエンドポイントの監査モードを無効にしてください(PolicyAuditMode=Disabled)。同じリクエストをもう一度送り、
/root/cca-audit/enforce.jsonに、audit_mode、api_ingress・api_egress、api_to_ledger・api_to_legacy・batch_to_api(コードの文字列)、flows(監査を無効にしたあとの、api→legacyとbatch→apiのDROPPEDフローが入ったcompact形式の行のリスト)を記録します。 - cca-auditに2つのポリシーを作成してください。標準のNetworkPolicy
np-empty(podSelectorはapp=batch、policyTypesは[Ingress]、ingress: [{}])と、CiliumNetworkPolicycnp-empty(endpointSelectorはapp=frontend、ingress: [{}])です。ledger Podからbatchとfrontendにリクエストを送り、/root/cca-audit/empty.jsonに、batch_from_ledger・frontend_from_ledger(コードの文字列)、batch_ingress・frontend_ingress(各エンドポイントのingressのPOLICY列)を記録します。 - cca-auditにCNP
ledger-observeを作成してください。endpointSelectorはapp=ledger、enableDefaultDeny: {ingress: false}、ingressは1項目(fromEndpointsはapp=api、TCP "8080")です。api→ledgerとbatch→ledgerにリクエストを送ってフローを読み取り、/root/cca-audit/observe.jsonに、ledger_ingress(POLICY列)、api_to_ledger・batch_to_ledger(コードの文字列)、flows(2つのリクエストのledger側のINGRESS policy-verdictの行が入ったcompact形式の行のリスト)を記録します。 - agentで
cilium-dbg policy get -o jsonを読み取り、cca-auditネームスペースのルールごとに、ラベルio.cilium.k8s.policy.nameとio.cilium.k8s.policy.derived-fromを探してください。/root/cca-audit/sources.jsonに、derived_from(ポリシー名からderived-fromの値への辞書、5つ)とrevision(出力のrevisionの数値)を記録します。 /root/cca-audit/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を7行書きます。項目は、api_audit_mode(現在のapiのPolicyAuditMode)、api_policy_columns(apiのingress列/egress列、例: A/Bの形式)、api_to_legacy(allowedまたはdenied)、np_empty_ingress_rule・cnp_empty_ingress_rule(それぞれallow-allまたはdeny-all)、ledger_ingress_column、ledger_default_deny_ingress(ledger-observeのenableDefaultDeny.ingressの値、trueまたはfalse)です。値はすべて、現在の状態と一致している必要があります。
参考
- ポリシーの適用モードとエンドポイントのデフォルトポリシー(enableDefaultDeny): https://docs.cilium.io/en/v1.20/security/policy/intro/
- L3ルールとIngress/Egress Default Deny(
- {}の例): https://docs.cilium.io/en/v1.20/security/policy/layer3/ - 判定からポリシーを作る(監査モード、エンドポイント単位の設定): https://docs.cilium.io/en/v1.20/security/policy-creation/
- KubernetesのNetworkPolicy: https://kubernetes.io/docs/concepts/services-networking/network-policies/
- Hubbleのフロー:
kubectl -n kube-system exec ds/cilium -c cilium-agent -- hubble observe --since <시각> --namespace cca-audit -o compact(プレースホルダーは時刻です) - 適用列:
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg endpoint list。エンドポイント番号はkubectl -n cca-audit get cepで確認します。
ポリシーが1つもないときの適用状態を、先に書き留める
kubectl apply -f /opt/fixtures/cca-audit/workload.yamlで、cca-auditに、Pod api・ledger・legacy・frontend・batch(いずれも8080のサーバー)と、同じ名前のServiceを起動してください。ポリシーをかける前に、/root/cca-audit/baseline.jsonを作成します。podsの下の5つのアプリ名ごとに、uid(Pod)、endpoint_id、identity(CiliumEndpointのstatus)、ingress・egress(agentのcilium-dbg endpoint listのPOLICY列の値)を記録し、最上位には、batch_to_api(batch Podからのリクエスト(http://api:8080/)のHTTPコードの文字列で、応答がなければ"000")とrecorded_at(記録した瞬間のUTC時刻、date -u +%Y-%m-%dT%H:%M:%SZ)を記録します。
cilium-configのenable-policyがdefaultの場合、どのポリシーにも選択されていないエンドポイントはブロックされません。endpoint listの2つのPOLICY列は、方向ごとにデフォルト拒否が有効になっているかどうかを示します。Podにはcurlがないので、python3のurllibでリクエストしてください。この記録は、後のポリシーができる前に作成されている必要があります。採点ツールは、recorded_atをポリシーの作成時刻と比較します(ファイルの更新時刻は見ないので、あとから修正して保存してもかまいません)。
入ってくる側だけを閉じたのに、出ていく側はそのままだ
cca-auditにCiliumNetworkPolicy api-ingressを作成してください。endpointSelectorはapp=api、ingressは1項目(fromEndpointsはapp=frontend、toPortsはTCP "8080")で、egressは置きません。適用が反映されたら、/root/cca-audit/direction.jsonに、frontend_to_api、batch_to_api、api_to_legacy(各リクエストのHTTPコードの文字列で、応答がなければ"000")と、api_ingress・api_egress(apiエンドポイントのPOLICY列の値)、recorded_at(記録した瞬間のUTC時刻)を記録します。ステップ3のegressポリシーよりも先に記録する必要があります。
defaultモードでは、デフォルト拒否は方向ごとに有効になります。あるルールがingress部分を持ってエンドポイントを選択すると、その方向だけが許可リスト方式になります。ブロックされたリクエストは、拒否応答ではなく応答なしになるので、短いタイムアウトを設定してください。反映に数秒かかることがあるので、ポーリングが安全です。
新しいegressポリシーを監査モードで先にかけて、誰がブロックされるのかを見る
apiエンドポイントで監査モードを有効にしてください(cilium-dbg endpoint config <api 번호> PolicyAuditMode=Enabled、プレースホルダーはapiのエンドポイント番号です)。そのあと、CNP api-egressを適用します。endpointSelectorはapp=api、egressは2項目で、(1) toEndpointsはapp=ledger、TCP "8080"、(2) toEndpointsはk8s:io.kubernetes.pod.namespace: kube-system・k8s:k8s-app: kube-dns、ポート"53" protocol ANYです。api→ledger、api→legacy、batch→apiにリクエストを送ったあと、agentの中でhubble observeでcca-auditのフローを読み取り、/root/cca-audit/audit.jsonに記録します。記録する項目は、endpoint_id、audit_mode、api_ingress・api_egress(POLICY列)、api_to_legacy・batch_to_api(コードの文字列)、flows(api→legacyとbatch→apiのAUDITEDフローが含まれるcompact出力の行のリスト)です。
監査モードは、ポリシーの判定を計算しても、拒否する代わりに通過させ、フローにAUDITEDとして残します。エンドポイント単位のオプションなので、そのエンドポイントのすべての方向に適用される点を、ステップ2の結果と比べてみてください。リングバッファは数分で押し出されてしまうので、フローは観測した直後にファイルへ残し、--sinceにリクエストの直前の時刻を渡すと、必要な行だけが集まります。egressを閉じると、名前解決もegressトラフィックであることを忘れないでください。
監査を解除すると、同じ2つのリクエストが実際に落ちる
apiエンドポイントの監査モードを無効にしてください(PolicyAuditMode=Disabled)。同じリクエストをもう一度送り、/root/cca-audit/enforce.jsonに、audit_mode、api_ingress・api_egress、api_to_ledger・api_to_legacy・batch_to_api(コードの文字列)、flows(監査を無効にしたあとの、api→legacyとbatch→apiのDROPPEDフローが入ったcompact形式の行のリスト)を記録します。
監査モードが示したAUDITEDが、そのままDROPPEDになるか、許可した宛先(ledger)は依然として通過するかを確認してください。フローの末尾が判定の理由です。前のステップで残った古いDROPPEDの行と混ざらないように、監査を無効にする直前の時刻から読んでください。
同じようにingress: [{}]と書いたのに、一方は開き、一方は閉じた
cca-auditに2つのポリシーを作成してください。標準のNetworkPolicy np-empty(podSelectorはapp=batch、policyTypesは[Ingress]、ingress: [{}])と、CiliumNetworkPolicy cnp-empty(endpointSelectorはapp=frontend、ingress: [{}])です。ledger Podからbatchとfrontendにリクエストを送り、/root/cca-audit/empty.jsonに、batch_from_ledger・frontend_from_ledger(コードの文字列)、batch_ingress・frontend_ingress(各エンドポイントのingressのPOLICY列)を記録します。
2つのAPIは、空のルール項目を異なる意味に解釈します。KubernetesのNetworkPolicyでは、fromのないingress項目は、すべての送信元を意味します。Ciliumでは、fromEndpointsのような送信元フィールドがないルール項目は、誰も許可せず、デフォルト拒否だけを有効にします。どちらも適用列は同じに見える点も確認してください。
適用列はEnabledなのに、誰もブロックされない
cca-auditにCNP ledger-observeを作成してください。endpointSelectorはapp=ledger、enableDefaultDeny: {ingress: false}、ingressは1項目(fromEndpointsはapp=api、TCP "8080")です。api→ledgerとbatch→ledgerにリクエストを送ってフローを読み取り、/root/cca-audit/observe.jsonに、ledger_ingress(POLICY列)、api_to_ledger・batch_to_ledger(コードの文字列)、flows(2つのリクエストのledger側のINGRESS policy-verdictの行が入ったcompact形式の行のリスト)を記録します。
enableDefaultDenyを無効にしたルールは、エンドポイントのデフォルトモードを決めるときに除外されます。そのため、ルールは計算されますが、拒否にはつながりません。policy-verdictの行で、policy-verdict:の直後にあるマッチの種類が、2つのリクエストでどう異なるかを比べてください。POLICY列だけでデフォルト拒否の有無を判断してはいけない理由です。
2種類のポリシーは、1つのリポジトリに入る
agentでcilium-dbg policy get -o jsonを読み取り、cca-auditネームスペースのルールごとに、ラベルio.cilium.k8s.policy.nameとio.cilium.k8s.policy.derived-fromを探してください。/root/cca-audit/sources.jsonに、derived_from(ポリシー名からderived-fromの値への辞書、5つ)とrevision(出力のrevisionの数値)を記録します。
Ciliumは、標準のNetworkPolicyも自分のルール形式に変換してCNPと同じリポジトリに入れ、どこから来たのかをラベルで付けます。ルールごとにLabelsリストのkey/valueを見てください。revisionは、リポジトリが変わるたびに上がります。
適用モードのインシデントレポート
/root/cca-audit/report.txtに、키=값形式の行(プレースホルダーはキーと値です)を7行書きます。項目は、api_audit_mode(現在のapiのPolicyAuditMode)、api_policy_columns(apiのingress列/egress列、例: A/Bの形式)、api_to_legacy(allowedまたはdenied)、np_empty_ingress_rule・cnp_empty_ingress_rule(それぞれallow-allまたはdeny-all)、ledger_ingress_column、ledger_default_deny_ingress(ledger-observeのenableDefaultDeny.ingressの値、trueまたはfalse)です。値はすべて、現在の状態と一致している必要があります。
前のステップの記録を書き写さず、現在の状態を読み直してください。採点ツールもリクエストを再送して判定します。監査モードが再び有効になっていると、レポートの複数の値が変わります。