おい、最近のクラウドネイティブな環境構築を見ていて、どうもゾッとする瞬間が多いんだよね。「うちはKubernetes使ってコンテナ化してるからセキュアです」なんてドヤ顔で言ってくる開発者に限って、デフォルトのままPod間通信を素通しにしている。
これ、インシデントの現場から言わせてもらうと「マンションの各部屋の鍵を全部外し、廊下を自由に行き来できるようにしている状態」と同じだ。一度フロントエンドのAPIコンテナが踏み台にされたら、バックエンドのデータベースや決済基盤まで、攻撃者はパスポートなしでラテラルムーブメント(水平展開)し放題になる。
今回は、Kubernetes環境において攻撃者の侵入後の横移動を完全に絶つための「コンテナのネットワーク分離(Network Policies)」について、現場のリアルなリスクと共にお前らに叩き込む。教科書をなぞるだけの退屈な話はしない。今すぐ実務で使えるレベルの防御網の構築方法を解説しよう。
—
1. なぜ「デフォルト全通し」のKubernetesは危険なのか?
Kubernetesの初期状態(CiliumやCalicoなどのCNIプラグインのデフォルト設定による)では、クラスター内のすべてのPodは、どのnamespaceからでも、どのPodへも通信が可能だ。これを「フラットなネットワーク」と呼ぶ。
もし、お前らが公開しているWebアプリケーション(例えばPHP製やNode.js製のAPIサーバー)に、未知のRCE(リモートコード実行)脆弱性が潜んでいたとしよう。攻撃者がそこにペイロードを送り込み、コンテナのシェルを奪ったとする。
ここでNetwork Policiesが導入されていない場合、攻撃者はそのコンテナ内から以下のような悪事を簡単に働ける。
1. 内部スキャン: nmapやスクリプトを使い、同じクラスター内の他のPodのIPアドレスやオープンポートを列挙する。
2. データベースへの直接アクセス: フロントエンドからは見えてはいけないはずの、内部用PostgreSQLやRedisコンテナに直接SQLインジェクションやブルートフォースを仕掛ける。
3. メタデータサーバーの悪用: クラウド環境であれば、コンテナ内からAWSのIMDS(169.254.169.254)などにアクセスし、IAMロールのクレデンシャルを窃取してクラウドインフラ全体を乗っ取る。
これを物理的に防ぐのが、ホワイトリスト形式によるNetwork Policiesだ。
—
2. 現場で使える!NetworkPolicyの鉄則と設計アプローチ
ネットワークポリシーを設計する際の鉄則は、「デフォルト・デナイ(すべて遮断)」を基本とし、必要な通信だけを極小のスコープで許可することだ。
よくある失敗は、ポリシーの適用を忘れて「まあ動くからいいか」と放置すること。これを防ぐために、名前空間(Namespace)単位で「デフォルトで全ての入出力を拒否するポリシー」を最初に適用し、その上で必要なマイクロセグメンテーション(細粒度なアクセス制御)を積み上げていくのがプロのやり方だ。
実装シナリオ
今回は、以下の3層構造のシステムを例にする。
frontendnamespace: 外部からのHTTP/HTTPSリクエストを受け付けるWeb/APIサーバー(Python/Flask等)backendnamespace:frontendからのみアクセスを許可する業務ロジックサーバー(Node.js/Express等)databasenamespace:backendからのみアクセスを許可するデータベースサーバー(PostgreSQL等)
—
3. コピペで動く!セキュアなNetworkPolicy実装サンプル
では、実際に適用するYAML設定を見ていこう。Kubernetesクラスタでこれらを適用すれば、コンテナ間の無駄な通信はすべてパケットレベルでドロップされるようになる。
①【鉄則】Namespace自体のデフォルト拒否ポリシー
まずは、特定のNamespace内において、明示的な許可がない限り全てのIngress(入站)通信をブロックするポリシーを定義する。これを最初に適用するのが定石だ。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: database # ここではデータベース用ネームスペースを例示
spec:
podSelector: {} # 空にすることで、このネームスペース内のすべてのPodに適用
policyTypes:
- Ingress
# ingressルールを記述しないため、外部からのすべての接続要求が拒否される
② バックエンドからデータベースへの最小権限アクセス許可
次に、データベース(database namespace)に対して、特定のバックエンド(backend namespaceの特定のPod)からのみ、PostgreSQLのポート(5432)へのアクセスを許可するポリシーだ。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-database
namespace: database
spec:
# このポリシーが適用されるターゲット(データベースのPod)を指定
podSelector:
matchLabels:
app: postgres-database
policyTypes:
- Ingress
ingress:
- from:
# 1. 同じクラスター内の特定のNamespaceから許可
- namespaceSelector:
matchLabels:
environment: production
project: my-app-backend
# 2. かつ、その中の特定のラベルを持つPodのみに絞り込む(ラテラルムーブメント防止のキモ)
podSelector:
matchLabels:
role: business-logic-server
ports:
- protocol: TCP
port: 5432 # PostgreSQLのデフォルトポートのみ許可
③ アプリケーション層(Python/Flask)でのセキュアな実装とネットワーク監視
インフラ側でNetworkPolicyを固めても、アプリケーション層(コンテナ内)の設計がガバガバだと意味がない。例えば、Python製APIサーバーが内部で不必要な外向きリクエスト(SSRF脆弱性など)を発生させないよう、コードレベルでも厳格なバリデーションを行う必要がある。
以下は、Python(Flask)を用いたセキュアなAPIエンドポイントの実装例だ。不要な外部通信やプロキシ経由のインジェクションを防ぐための実装イメージを見てほしい。
from flask import Flask, request, jsonify
import ipaddress
import socket
import urllib.parse
app = Flask(__name__)
# 許可された内部バックエンドサービスのホスト名(DNS解決後のIPも検証する)
ALLOWED_INTERNAL_SERVICES = {
"backend-service.backend.svc.cluster.local"
}
def is_safe_internal_url(target_url):
"""
SSRF(サーバー側リクエスト偽造)を防ぐため、
プライベートIPやメタデータサーバーへの不正なアクセスをブロックする
"""
try:
parsed = urllib.parse.urlparse(target_url)
hostname = parsed.hostname
if not hostname:
return False
# ドメイン名が許可された内部サービスリストに含まれているか確認
if hostname not in ALLOWED_INTERNAL_SERVICES:
return False
# DNS解決を行い、プライベートIP空間以外を指していないか(または意図したクラスタ内IPか)検証
ip_addr = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip_addr)
# リンクローカルやAWSメタデータIP(169.254.169.254)へのアクセスを厳格に拒否
if ip_obj.is_link_local or ip_obj.is_loopback or ip_obj.is_multicast:
return False
return True
except Exception as e:
# 異常系はすべて安全側に倒して拒否
return False
@app.route('/api/v1/process', methods=['POST'])
def process_data():
data = request.json.get('payload')
if not data:
return jsonify({"error": "Invalid payload"}), 400
# アプリケーションから外部へのリクエストが発生する場合の安全策
target_endpoint = "http://backend-service.backend.svc.cluster.local/internal/v1/exec"
if not is_safe_internal_url(target_endpoint):
# セキュリティアラートをログに出力
app.logger.warning(f"Blocked potential SSRF or unauthorized internal call to: {target_endpoint}")
return jsonify({"error": "Forbidden network access"}}, 403
# 実際の処理(NetworkPoliciesにより、この通信自体もKubernetes層で保護されている)
return jsonify({"status": "success", "message": "Processed securely."})
if __name__ == '__main__':
# デバッグモードは本番環境では絶対にオフにすること
app.run(host='0.0.0.0', port=8080)
—
4. 導入後の検証と現場での注意点
Network Policiesを適用した後に必ずやっておくべきなのが、「意図した通信が通り、意図しない通信が確実にブロックされているかのテスト(疎通確認)」だ。
現場でよくあるミスが、ポリシーを適用した直後にモニタリングツール(Prometheus/Grafana)やログ収集基盤(Fluentd/Datadog)へのログ転送が途絶えるトラブルだ。これらはバックグラウンドで別ネームスペースのサービスと通信しているため、NetworkPolicyで明示的に許可してやる必要がある。
疎通テストのワンライナー(現場の常套手段)
特定のPod内から、別のPodや外部への通信が期待通りに遮断されているかをサクッと確認するには、コンテナ内に一時的に nc (Netcat) や curl を飛ばすコンテナをアタッチするか、既存のPodから以下のように実行する。
# フロントエンドのPod内から、データベースのポートへ意図的に接続テストを行う
# (NetworkPolicyが正しければ、タイムアウトするかConnection refusedになるはず)
kubectl exec -it <frontend-pod-name> -n frontend -- nc -zv postgres-database.database.svc.cluster.local 5432
もしここで接続が成功してしまう(Open と返ってくる)場合、ポリシーのラベルセレクターの記述ミスや、CNIプラグインがNetworkPolicyをサポート・有効化していない可能性が高い。必ず確認してくれ。
—
まとめ
セキュリティにおいて「信じるな、確認せよ(Zero Trust)」の原則はコンテナの世界でも全く変わらない。「同じクラスター内だから安全」という甘い考えは、今日この瞬間から捨ててほしい。
Network Policiesの導入は、最初は設定の手間が増えるように感じるかもしれない。だが、ひとたびインシデントが発生した際、被害を最小限のコンテナ1個に閉じ込めることができるか、それともシステム全体が芋づる式に崩壊するかの「生死を分ける防壁」になる。
自分の管理するクラスターのYAMLを今すぐ見直し、不要なラテラルムーブメントの道をすべて断ち切ってくれ。頼んだぞ。
コメント