おい、そこの君。ちょっと手を止めて画面を見てくれ。
先週、うちのグループ会社で派手にやらかしたインシデントのフォレンジック結果が上がってきた。原因はなんだと思う? 「外部からの巧妙なゼロデイ攻撃」か? いや、そんな映画みたいな話じゃない。侵入経路は、たった一台の、放置されていた古い開発用ログ収集Podの脆弱性だ。
問題はそこからだ。攻撃者はそのPodを踏み台にして、いとも簡単に本番環境のデータベース(MySQL)が走るコンテナに到達し、顧客情報をきれいさっぱり持ち去っていった。
なぜそんなことが起きたのか? 答えはシンプルだ。「K8sのクラスタ内は安全だろう」という、根拠のない甘えがあったからだ。 デフォルトのKubernetesは、初期状態では「全Pod間の通信がフリーパス(Default Allow)」になっている。つまり、一度城壁の内側に侵入されれば、あとはお前の好き勝手に暴れてくれと言っているようなものなのさ。
今日は、この「城壁の中の無秩序」を断ち切り、ラテラルムーブメント(横方向への侵入拡大)を完全に封じ込めるための技術、「Kubernetes NetworkPolicyによるマイクロセグメンテーション」について、現場の泥臭い知見を交えて徹底的に解説する。
—
1. 攻撃者がクラスタ内でやり口と、NetworkPolicyの必要性
モダンなクラウドネイティブ環境では、すべてのサービスがPodという単位でコンテナとして動いている。Webアプリ、API、バックエンドのワーカー、そしてデータベース。これらが同じフラットなネットワーク空間に同居している状態を想像してほしい。
もし、公開されているWebアプリのコンテナ(例: app=frontend)にリモートコード実行(RCE)の脆弱性があった場合、攻撃者はそこを足がかりにする。
もしNetworkPolicyを設定していなければ、攻撃者はそのフロントエンドPodのシェルから、クラスタ内のあらゆるPodに対して直接通信を試みることができる。内部で動いている管理用APIだろうが、基幹データベースだろうが、ネームプレートさえ分かればお構いなしだ。
ここで私たちが実装すべきなのが、「ゼロ・トラスト」の思想に基づいたネットワークの要塞化だ。
具体的には、以下の2ステップを踏む。
1. デフォルト拒否(Default Deny)の鉄則: ネームスペース内のすべてのPodに対し、まず「一切の通信を禁止する」という絶対的な制限をかける。
2. ホワイトリスト方式の最小特権許可: 必要な通信(例: frontend から backend への特定のポート番号での通信など)だけを、ラベルセレクタを使ってピンポイントで許可する。
言葉で言うのは簡単だが、これを怠ると、インシデント発生時に「被害の局限化(Blast Radiusの縮小)」ができず、クラスタ全体が全滅する。実務の現場でこれをどう実装するのか、具体的なマニフェストを見ていこう。
—
2. コピペで動くセキュアな実装サンプル
それでは、実際に本番環境へ適用できるNetworkPolicyのマニフェストを解説しよう。今回は、よくある 3層構造(フロントエンド、バックエンドAPI、データベース)のWebアプリケーションを想定した実例だ。
前提として、これらのリソースは production という名前の専用ネームスペースにデプロイされているとする。
ステップ1: ネームスペース全体の「デフォルト拒否(Default Deny)」
まずは、そのネームスペースにおけるすべての「入出力」を遮断する。これを最初に適用するのがプロの鉄則だ。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
# ネームスペース内のすべてのPodを対象にする(空のPodSelectorは「すべて」を意味する)
podSelector: {}
# イングレス(入ってくる通信)とエグレス(出ていく通信)の両方を対象に指定
policyTypes:
- Ingress
- Egress
このマニフェストを適用した瞬間、このネームスペース内のPodは外部との通信はもちろん、Pod同士の通信も完全に遮断される。健康診断(Liveness/Readinessプローブ)やDNS名前解決が壊れる可能性があるため、導入時は既存アプリへの影響を十分検証してくれ。
ステップ2: 最小権限の通信許可(ホワイトリスト方式)
デフォルトですべてを塞いだら、次に「必要な通信だけ」を通すポリシーを定義していく。
今回は以下の2つの正当な通信パスを許可する。
1. app=frontend のPodから、app=backend のPodへのAPI通信(TCPポート 8080)
2. app=backend のPodから、app=database のPodへのDB通信(TCPポート 3306)
さらに、外部へのDNS名前解決(ポート 53)もエグレスとして許可しておく必要がある。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
# このポリシーが適用されるのは「バックエンド」のPod
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
# 送信元が「フロントエンド」のPodであることの条件指定
- podSelector:
matchLabels:
app: frontend
ports:
# バックエンドが待ち受けているポートのみ許可
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-database
namespace: production
spec:
# このポリシーが適用されるのは「データベース」のPod
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
# 送信元が「バックエンド」のPodであること
- podSelector:
matchLabels:
app: backend
ports:
# データベースのデフォルトポートのみ許可
- protocol: TCP
port: 3306
このように、ラベルセレクタ(matchLabels)を厳格に運用し、通信の向きとポート番号を絞り込むことで、万が一 frontend が乗っ取られても、そこから database に直接触ることは物理的(論理的)に不可能になる。これがラテラルムーブメントを完全に阻止する仕組みだ。
—
3. 現場のセキュリティチーフからの実践的なアドバイスと落とし穴
NetworkPolicyを導入するにあたり、現場のエンジニアからよく受ける相談や、失敗しがちなポイントをいくつかシェアしておこう。
1. CNIプラグインの選定に注意しろ
NetworkPolicyは、ただKubernetesのマニフェストを投げるだけでは動かない。下層のCNI(Container Network Interface)プラグインがNetworkPolicyのコントローラーをサポートしている必要がある。
例えば、標準の kube-router や Calico、Cilium、あるいはマネージドサービスであれば Amazon VPC CNI や Azure CNI などが対応している。GKEの場合は、クラスタ作成時にNetwork Policyの有効化を明示的にチェックしておく必要がある。ここを忘れていると、「設定したのに通信が通る(=実はただの飾りになっている)」という最悪のセキュリティホールが生まれる。
2. トラブルシューティングの勘所
「ポリシーを入れたらアプリケーションが動かなくなった」というのは、導入初期の通過儀礼だ。慌ててポリシーを全削除する前に、ログやパケットを追え。
よくある原因は、外部APIへのHTTPS通信(ポート443)や、CoreDNSへの名前解決(UDP/TCP 53)のエグレス(Egress)制限漏れだ。エグレス制御を入れる場合は、最低限以下の名前解決と外部通信のルールを忘れないようにしてほしい。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-and-external
namespace: production
spec:
podSelector:
matchLabels:
app: backend # または制限したいPodのラベル
policyTypes:
- Egress
egress:
# 1. Kubernetes内部のCoreDNSへの通信を許可
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 2. 必要に応じて外部APIへのHTTPS通信を許可(CIDRやIPブロックで絞るのがベスト)
- to:
- ipBlock:
cidr: 0.0.0.0/0 # 本番では特定の外部APIのIPに絞るべき
ports:
- protocol: TCP
port: 443
セキュリティとは、一朝一夕で完璧なものができるわけではない。「動くものを作ってから締める」のではなく、「最初から絞った上で、必要なものだけを穴あけする(White-listing)」という習慣をチーム全体の共通認識にしてほしい。
明日から、君たちの担当しているクラスタのネームスペースに default-deny が入っているか、今すぐ確認してみたまえ。セキュリティの担保は、他でもない、我々インフラ・開発エンジニアの手にかかっているのだから。
コメント