おい、ちょっと手を止めてこっちを向いてくれ。
先日、とあるクライアントのKubernetes(K8s)クラスターのペネトレーションテスト(模擬攻撃)を実施したんだ。開発チームは「うちはマネージドのEKSだから大丈夫」「内部ネットワークだから安全」と胸を張っていた。だが、私たちがクラスター内の1つの脆弱なWeb Pod(Python製APIサーバー)のRCE(リモートコード実行)を踏み台にした瞬間、何が起きたと思う?
クラスター内の全Pod間通信が平文で丸見え、かつ、たった1つのNamespaceから隣の機密データ(データベース)を格納したNamespaceへ、何の制限もなくTCPコネクションが繋がっちゃったんだよ。
攻撃者視点から言えば、これは「ディズニーランドのVIPパスポートを最初から渡されているようなもの」だ。外壁(WAFやIngress)だけをガチガチに固めて、一歩中に侵入されたら(Cluster Internal)ノーガード。これが現代の多くのK8s環境が抱える最大の盲点だ。
今日は、この「境界防御の幻想」を打ち破り、Pod間通信の暗号化(mTLS)とネットワークポリシー(NetworkPolicy)によるゼロトラスト環境をどうやって現場に実装するか、実際の攻撃者の手口を踏まえながら徹底的に解説しよう。
—
なぜ「デフォルトのKubernetes」は攻撃者に愛されるのか?
Kubernetesの標準状態(CiliumやCalicoなどの一部CNIを除き、デフォルトのkube-proxyと標準CNI)では、同じクラスター内のPod間通信はすべて暗号化されず、かつ全てのPodが他の全てのPodと通信可能(All-Allow)な状態になっている。
もし君のアプリケーションのコンテナの1つに、例えば古い依存関係やサードパーティ製ライブラリの脆弱性があり、そこに攻撃者が侵入してリバースシェルを確保したとしよう。そこから攻撃者がやることは決まっている。
1. arp や /proc/net/arp、あるいは内部DNS(CoreDNS)を叩いて、周辺にどんなサービス(データベース、管理画面、他のマイクロサービス)が走っているかスキャンする。
2. 平文で流れているHTTP/TCPトラフィックを tcpdump や ngrep でキャプチャし、運悪く混ざっている他のサービスの内部APIトークンやDBのクレデンシャルを引っこ抜く。
3. 横移動(Lateral Movement)によって、最終的にクラスタのコントロールプレーンや機密データストアを掌握する。
これを防ぐには、「通信経路の暗号化(mTLS)」と「通信主体の厳格な制限(Default Deny + NetworkPolicy)」の2つを同時に実装するしかない。
—
1. ネットワークポリシーで「ゼロトラスト」を強制する
まずは足回りの要、NetworkPolicyの設定だ。
基本思想は 「デフォルトですべての通信を遮断(Default Deny)し、必要な通信だけをホワイトリスト方式で許可する」 こと。これに尽きる。
以下のYAMLは、特定のNamespace(例: production)において、まず全ての入出力トラフィックをドロップし、その上でフロントエンド(app: web)からバックエンド(app: api)への特定のポート(例: 8080)のみを許可する堅牢な設定サンプルだ。
# 1. まず Namespace 内のすべての入出力を完全に遮断する (Default Deny)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Namespace内の全てのPodが対象
policyTypes:
- Ingress
- Egress
---
# 2. フロントエンドからバックエンドへの通信のみを許可するホワイトリスト設定
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api # 適用対象:バックエンドのAPIサーバー
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: web # 許可元:フロントエンドのWebサーバーのみ
ports:
- protocol: TCP
port: 8080 # APIサーバーが待ち受けるポート
この設定を入れておけば、万が一Webサーバーが破られても、そこからデータベース(app: db 等)へ直接TCPパケットを飛ばすことは物理的(パケットフィルター的)に不可能になる。攻撃者の横移動を完全にハネ返す防壁だ。
—
2. サービスメッシュ(Istio)によるmTLSで「盗聴」を無効化する
次に通信の暗号化だ。アプリケーションコード側でHTTPSや独自暗号化を実装するのは、開発コストが高すぎるし、証明書のローテーション管理で確実にミスが起きる。
ここで登場するのが サービスメッシュ(Istio等) だ。アプリケーションコードは一切変更せず、サイドカーとして配置されたプロキシ(Envoy)が自動的にすべてのPod間通信をmTLS(相互TLS認証)でラップしてくれる。
以下の設定は、Istioを用いて production Namespace全体でmTLSを「厳格(STRICT)」に強制するためのリソース定義だ。
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # 平文通信を一切許可せず、必ずmTLSを強制する
これを適用すると、万が一ネットワーク層でパケットがキャプチャ(スニッフィング)されても、中身は強固に暗号化されたTLSセッションであるため、攻撃者には意味不明なバイナリの羅列しか見えない。さらに、クライアント証明書の検証により、通信の相手が「正当なKubernetesのPodであること」が暗号学的に保証される。
—
3. セキュアなアプリケーション実装の担保(Pythonの例)
インフラ層でmTLSとネットワークポリシーを固めたとしても、アプリケーション自体が脆弱であれば意味がない。最後に、内部APIと通信するPython製マイクロサービス(FastAPIなど)において、安全なHTTPクライアントを実装するサンプルコードを見ておこう。
内部通信であっても、タイムアウトの設定漏れによるDoS耐性の低下や、不正なレスポンスのパースによる脆弱性を防ぐ必要がある。
import httpx
from fastapi import FastAPI, HTTPException, status
import logging
app = FastAPI()
logger = logging.getLogger("security-logger")
# 内部APIのエンドポイント(サービスメッシュ内であればプレーンなhttpで通信してもEnvoyがmTLS化する)
INTERNAL_API_URL = "http://internal-api-service.production.svc.cluster.local:8080/data"
@app.get("/fetch-secure-data")
async def fetch_data_from_backend():
"""
内部バックエンドAPIへ安全にリクエストを送信するハンドラー。
タイムアウトと例外処理を確実に実装し、インフラ障害や情報漏洩を防ぐ。
"""
# 接続・読み込みタイムアウトを必ず設定する(無限待ちによるスレッド枯渇を防ぐため)
timeout_config = httpx.Timeout(connect=2.0, read=5.0, write=5.0, pool=5.0)
try:
# httpxクライアントを利用した安全なリクエスト
async with httpx.AsyncClient(timeout=timeout_config) as client:
response = await client.get(INTERNAL_API_URL)
# ステータスコードの検証
if response.status_code != 200:
logger.error(f"Backend API error: Status {response.status_code}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Internal upstream service error."
)
return {"status": "success", "data": response.json()}
except httpx.TimeoutException:
logger.warning("Timeout occurred while connecting to internal API.")
raise HTTPException(
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
detail="Upstream service timeout."
)
except httpx.RequestError as e:
# 詳細なエラーメッセージを外に出さず、ログにのみ詳細を記録する
logger.error(f"Network error during internal request: {str(e)}")
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Failed to communicate with internal service."
)
このコードでは、以下のセキュリティプラクティスを網羅している:
1. タイムアウトの厳格化: httpx.Timeout を用いて、バックエンドのハンチングやスレッドプール枯渇攻撃(Slowloris的状況)を防止。
2. エラーハンドリングの抽象化: 内部のエラーや例外のスタックトレースをそのままレスポンスとしてクライアントに返さず(情報漏洩の防止)、ログに閉じ込めつつ適切なHTTPステータス(502, 504など)に変換している。
—
チームへの申し送り事項
セキュリティは「点」ではなく「面」だ。どれだけWAFやクラウド側のIAMを硬くしても、Kubernetesの内部ネットワークが「ノーチェックの野戦病院」状態であれば、侵入された瞬間にゲームオーバーとなる。
今日紹介した NetworkPolicyによるDefault Deny と Istio等によるmTLSの強制 は、モダンなK8s運用において「あって当たり前の基本インフラ」だ。まだ導入していないプロジェクトがあれば、今日の段階で検証環境からでもいい、すぐに組み込んでくれ。
自分のコード、そして自分たちのインフラを守れるのは、他の誰でもない、キーボードを叩いている僕ら自身なのだから。
コメント