Kubernetesクラスターが攻撃対象になるとき
去年、痛い教訓を得た。深夜にSSHブルートフォース攻撃を受け、寝ぼけた頭で慌てて対応しなければならなかった。それ以来、習慣ができた――どのインフラを構築するにしても、セキュリティは初日から設定する。問題が起きてから考えるのでは遅い。
Kubernetesも例外ではない。クラスターを構築してアプリが動けばOK、そのままproductionにデプロイ――セキュリティは後回し。そして「後で」はたいてい、インシデントが発生するまで永遠に来ない。
これは理論の話ではない。2018年、RedLockチームはTeslaのKubernetesクラスターがAPIサーバーを認証なしでインターネットに公開していることを発見した――攻撃者はそれを利用して仮想通貨のマイニングに使った。コンテナはroot権限で動作し、Pod間は自由に通信でき、シークレットは暗号化なしのbase64で保存され、APIサーバーへのアクセス制限もなかった。Teslaだけの話ではなく、これはこれまでレビューした多くのproductionクラスターの実態だ。
なぜKubernetesはデフォルトで安全でないのか?
K8sは柔軟性と使いやすさを優先して設計されている。セキュリティは自分で追加するレイヤーであり、最初から備わっているものではない。具体的には:
- RBACはデフォルトで有効でない場合がある――古いバージョンや、作成直後のマネージドクラスターなど。
- PodはSecurityContextを指定しなければroot権限で動作する。
- Network Policyは存在しない――自分で書くまで、クラスター内のすべてのPodが自由に通信できる。
- シークレットはbase64に過ぎない:etcdまたはSecretリソースへの読み取り権限があれば、すべての内容を読める。
- デフォルトのService AccountはすべてのPodにマウントされ、Kubernetes APIを呼び出せる。
これらを組み合わせると、コンテナ1つが侵害されるだけで、攻撃者はクラスター全体へ権限昇格できる。ラボで試したことがあるが、侵害されたPodからcluster-admin権限を得るまで10分もかからなかった。
設定すべきセキュリティレイヤー
1. RBAC — アクセス権限の制御
すべての基盤となるのは最小権限の原則だ:必要なものだけ、それ以上は与えない。
# 「app」namespace内でPodの読み取りのみを許可するRoleを作成
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: app
name: pod-reader
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: app
subjects:
- kind: ServiceAccount
name: my-service-account
namespace: app
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
ClusterRoleにverbs: ["*"]やresources: ["*"]を設定することは特に避けるべきだ――急いでセットアップした多くのクラスターで見かけるミスだ。余剰な権限を検出するために定期的にセキュリティ監査しよう:
# サービスアカウントの権限を確認
kubectl auth can-i --list --as=system:serviceaccount:app:my-service-account
# anonymousユーザーに割り当てられたClusterRoleBindingを検索
kubectl get clusterrolebindings -o json | jq '.items[] | select(.subjects[]?.name=="system:anonymous")'
2. Pod Security — コンテナの動作を制限する
Kubernetes 1.25以降、Pod Security AdmissionがPodSecurityPolicy(1.21で非推奨)に取って代わった。namespaceレベルでポリシーを適用する:
# productionnamespaceに「restricted」ポリシーを適用
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/warn=restricted
Pod/DeploymentのspecではSecurityContextを明示的に宣言すること:
securityContext:
runAsNonRoot: true
runAsUser: 1000
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
コンテナはUID 1000で動作し、ファイルシステムは読み取り専用、Linuxケーパビリティはすべて削除される。コンテナが侵害されても、攻撃者にできることは限られる――狭いサンドボックスの中に閉じ込められる。
3. Network Policy — Pod間のファイアウォール
デフォルトでは、PodAはPodBを理由なく直接呼び出せる。Network Policyを使えば、どのトラフィックを許可するかを正確に定義できる:
# frontendからbackendへの通信のみ許可、それ以外は禁止
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: app
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
role: database
ports:
- protocol: TCP
port: 5432
重要な注意点:Network PolicyはCNIプラグインのサポートが必要だ――Calico、Cilium、Weaveのいずれも対応している。Flannelを使っている場合は注意:ポリシーを作成しても一切効果がなく、警告も出ない。
4. シークレット管理 — Kubernetesのデフォルトシークレットを信用するな
base64は暗号化ではない。etcdまたはkubectl get secretの読み取り権限があれば、即座に平文を取得できる。対処の方向性は2つある:
方法1 — etcdの保存時暗号化を有効にする(コントロールプレーンへの権限が必要):
# EncryptionConfigurationを作成
cat > /etc/kubernetes/encryption-config.yaml <<EOF
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: $(head -c 32 /dev/urandom | base64)
- identity: {}
EOF
# kube-apiserverに追加: --encryption-provider-config=/etc/kubernetes/encryption-config.yaml
方法2(推奨) — External Secrets OperatorをHashiCorp VaultまたはAWS Secrets Managerと連携させる。シークレットはetcdに保存されず、実行時にのみPodにインジェクトされる:
# External Secrets Operatorをインストール
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
5. 監査ログとランタイムセキュリティ
異常な動作を早期に検出することは、防御と同じくらい重要だ。APIサーバーで監査ログを有効にする:
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets", "configmaps"]
- level: RequestResponse
verbs: ["delete", "create"]
resources:
- group: ""
resources: ["pods"]
Falcoはランタイム検出レイヤーを追加する――コンテナ内のシェル起動、異常なファイルアクセス、不審なネットワーク接続をリアルタイムで検出する。productionでは欠かせないものだ:
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco --namespace falco --create-namespace \
--set falco.grpc.enabled=true \
--set falco.grpcOutput.enabled=true
優先順位別チェックリスト
NSAとCISAがK8sセキュリティに関する共同アドバイザリを発表し、「銀の弾丸はない――複数の防御レイヤーを重ねることでのみ効果を発揮する」と結論付けた。1つのレイヤーを突破されても、次のレイヤーが止める。実際の優先順位は:
- APIサーバーをインターネットに公開しない ――VPNまたはbastionホストを使用する。最も深刻で、かつ最も簡単に修正できるミスだ。
- Kubernetesを定期的に更新する ――K8sはリリースサイクルでCVEにパッチを当てるため、古いバージョンには既知の脆弱性が存在する。
- 最小権限のRBAC ――定期的にレビューし、不要な権限を削除する。
- すべてのPodにSecurityContextを設定 ――見落としがないよう、HelmチャートまたはデフォルトテンプレートにRBACを組み込む。
- Network Policy ――deny-allから始め、実際のニーズに合わせて段階的に開放する。
- デプロイ前にイメージをスキャン ――TrivyまたはGrypeをCI/CDパイプラインに直接統合する。
- 適切なシークレット管理 ――最低でも保存時暗号化、理想的には外部ボールト。
- 監査ログ+ランタイム監視 ――productionではFalcoは必須、妥協なし。
# プッシュ前にTrivyでイメージをスキャン
trivy image --severity HIGH,CRITICAL your-image:tag
# YAMLマニフェストの設定ミスを確認
trivy config ./k8s-manifests/
# CIS Kubernetes BenchmarkでクラスターをAudit
docker run --pid=host --userns=host --rm -v /etc:/etc:ro \
-v /var:/var:ro -v /usr/lib/systemd:/usr/lib/systemd:ro \
aquasec/kube-bench:latest
チームによく強調していることがある:セキュリティはプロセスの一部でなければならない――個別のタスクではなく。TrivyをCIパイプラインに統合し、admission controllerでPod Securityを強制し、RBACレビューをスプリントレトロに組み込む――それが習慣になれば、もう重荷には感じなくなる。
Kubernetesクラスターが「完全に安全」な状態になることはない。しかし上記のステップを実践することで、リスクを許容できるレベルまで引き下げられる――そして何より重要なのは、被害が出た後ではなく、異常が発生したときにすぐ気づけるようになることだ。

