ポリシーを一つ付けた瞬間に起きること
一言でいうと
あるエンドポイントを選択するポリシーが1つでも適用されると、その方向はただちに許可リスト方式に切り替わります。方向は互いに独立しており、denyは常にallowに勝ちます。
なぜ必要なのか
Kubernetesのデフォルトの状態は、「すべてのPodがすべてのPodと通信できる」です。決済サービスが社内のWikiに接続できてしまい、侵害されたフロントエンドがデータベースに直接アクセスできてしまいます。ゼロトラストの出発点は、このデフォルトを覆すことです。
標準のNetworkPolicyでも始められますが、実務ではすぐに壁にぶつかります。HTTPパス単位の制御ができず、外部APIをドメイン名で許可できず、明示的な拒否ルールがないので「何があっても防がなければならないもの」を表現する方法がなく、何よりも拒否されたトラフィックを見る方法がありません。 CiliumNetworkPolicyは、このギャップを埋めます。
どう動くのか
考え方のモデルは次のとおりです。
ingress 정책 egress 정책
"누가 나에게 올 수 있나" "나는 어디로 갈 수 있나"
[소스 아이덴티티] --> (엔드포인트) --> [목적지 아이덴티티/CIDR/FQDN]
|
per-endpoint 정책 맵에서 O(1) 판정
key: (아이덴티티, 포트, 프로토콜, 방향)
ここから、実務で最も多く事故を起こす2つのルールが出てきます。
1つ目は、方向ごとに独立していることです。あるPodにingressポリシーを1つ付けると、そのPodのingressは許可リスト方式になりますが、egressは、egressポリシーが付くまで依然としてすべて許可のままです。「ポリシーをかけたから安全だ」という思い込みは、ここから生まれます。
2つ目は、denyが常にallowに勝つことです。同じトラフィックにallowとdenyの両方がマッチした場合、結果は拒否になります。そのため、クラウドのメタデータエンドポイントの遮断のように、絶対に破られてはならない項目は、egressDenyでもう一重敷いておきます。
セレクターの微妙な違いも、試験の常連です。
ingress:
- fromEndpoints:
- {} # 같은 네임스페이스의 모든 엔드포인트 = 허용
ingress:
- fromEndpoints: [] # 아무것도 매칭되지 않음 = 명시적 기본 거부
空のオブジェクトが1つ入った配列と、空の配列は、まったく反対の意味になります。
L7ポリシーが付くと、経路が変わります。eBPFは該当するフローだけを選んでノードのEnvoyに渡し、EnvoyがHTTPを解析して、ルールに合わなければ403を返します。L4の遮断とは違って接続自体は成立するので、アプリケーションのログには「接続はできるのに403」と見えます。この違いを知らないと、ファイアウォールの問題だと思い込んで見当違いの場所を調べることになります。
最もよくある設定ミスは別にあります。toFQDNsを使いながら、DNSルールを一緒に設定しないことです。toFQDNsは、パケットのSNIを覗き込む方式ではありません。CiliumのDNSプロキシがそのPodのDNS応答を横取りして「このPodは、api.example.comがまもなく54.x.x.xであることを知った」と学習し、そのIPをipcacheに登録して、IPベースで許可する方式です。したがって、DNSクエリを許可し、プロキシが観察できるようにするルールがなければ、toFQDNsは永遠にマッチしません。 症状は、「DNSは動くのに接続が拒否される」、またはその逆として現れます。
本番への展開は4段階で行います。観察(Hubbleで実際の通信マトリクスを収集)→ 許可ポリシーだけを先にデプロイ → policy-audit-modeで「ブロックされたはずのトラフィック」だけを記録 → ネームスペース単位で強制適用。バッチ処理のようにまれにしか動かないトラフィックを見逃さないためには、観察期間を最低でも1か月の周期まで取るほうが安全です。
最後に、標準のNetworkPolicyとCNPは同じeBPFデータパスで評価され、どちらか一方でも許可すれば許可として合算されます。2種類を混ぜて使うと「なぜ開いているのか」を2か所で探さなければならなくなるので、チームとして標準のリソースを1つに決めておくほうがよいでしょう。
現場での姿
筆者のホームラボで、これを実際に計測しました。nginxを起動し、ポリシーなしで呼び出したときは、GETもPOSTも200でした。そのあと、rules.http: [{method: GET}]だけを含むCiliumNetworkPolicyを適用したところ、結果は次のように分かれました。
GET / -> 200
POST / -> 403
同じIP、同じポートなのに、メソッドで結果が分かれました。Podにサイドカーは注入しておらず、アプリケーションのコードもそのままでした。iptablesはL3/L4で動作するので、この区別は原理的にできません。
さらに興味深かったのは、Hubbleのログの形でした。
client:47918 -> api:80 http-request FORWARDED (HTTP/1.1 GET http://api/)
client:47932 -> api:80 http-request DROPPED (HTTP/1.1 POST http://api/)
client:47932 <- api:80 http-response FORWARDED (HTTP/1.1 403 0ms POST)
リクエストはDROPPEDなのに、応答はFORWARDEDです。プロキシが作り出した403が、正常な応答として流れ出たからです。L4のドロップであれば、応答の行自体がありません。このログの形の違いが、「ポリシーに止められたのか、ネットワークが切れたのか」を見分ける最も早い手がかりです。
次のラボですること
標準のNetworkPolicyで、デフォルト拒否とPod/ネームスペースセレクターによる許可を実際にapplyしてみて、CiliumNetworkPolicyで、HTTPメソッド・パスの制限、toFQDNsと対になるDNSルール、そしてメタデータエンドポイントの遮断をファイルとして作成します。