Secretという名前に反して、Kubernetesの機密データ管理は穴だらけです。
>
##
🎯 この記事で扱うこと
- etcdがSecretをデフォルトで暗号化せずに平文(Base64)で保存する理由と危険性
- Podを作成できる権限一つでSecret RBAC制御全体を迂回する攻撃経路
- PodがSecretを毎回読み込んでも監査ログ(Audit Log)が事実上空白である構造的理由
- 各脆弱性に対する実質的な対応方法
📌 導入 — etcdはKubernetesの心臓でありアキレス腱
Kubernetesクラスターのすべての状態情報は、etcdという分散キーバリューストアに格納されます。Deployment情報、ConfigMap、そして今日注目するSecretまで、すべてここにあります。
etcdを奪取すれば、クラスター全体を手に入れたも同然です。しかし、この重要なストレージには3つの構造的な脆弱性が存在します。単純な設定ミスではなく、設計上の決定と運用上の利便性が生んだ構造的な問題です。

🔍 脆弱性1 — etcdの暗号化、なぜデフォルトではないのか?
問題の核心
Kubernetes Secretの値は、デフォルトでBase64でエンコードされ、暗号化なしにetcdに保存されます。Base64は暗号化ではありません。単に別の形式に変換しただけです。echo “bXlwYXNzd29yZA==” | base64 -d の一行で原文が得られます。
etcdに直接アクセスできる人なら誰でも、このデータを簡単に復号して読み取ることができます。これが、保存時の暗号化を必ず有効にする必要がある理由です。
なぜデフォルトではないのか?
Kubernetesが暗号化をデフォルトで有効にしないのには、現実的な理由があります。
① キー管理の複雑性: 暗号化キーをどこに保存するのか?ローカルキーでSecretデータを暗号化すればetcdの奪取には対応できますが、ホスト自体が侵害された場合には保護されません。暗号化キーがEncryptionConfiguration YAMLファイルに保存されており、腕の立つ攻撃者であればそのファイルにアクセスできるためです。
② 既存データ移行: 暗号化を有効にしても、すでに保存されているSecretは自動的に暗号化されません。既存のSecretをすべて再度書き込む必要があります。
③ パフォーマンスオーバーヘッド: すべての読み書きに暗号化/復号が追加されます。
現在提供されている暗号化方式
Kubernetesは複数の暗号化プロバイダーを提供しています。identityはデフォルトで暗号化を全く行いません。AES-CBCはパディングオラクル脆弱性のためもはや安全ではなく、キーをコントロールプレーンノードに保存するためホスト侵害には無力です。AES-GCMはAES-CBCよりも改善されていますが、やはりキーがホストにあるという限界があります。
正しい対応
# /etc/kubernetes/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms: # ✅ 本番環境推奨: KMS連携によるキーの外部管理
name: myKmsPlugin
endpoint: unix:///tmp/socketfile.sock
apiVersion: v2
- identity: {} # fallback — 復号専用としてのみ使用
kube-apiserver設定の追加:
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/enc.yaml # この行を追加
プロダクション環境では、AWS KMS、GCP KMS、Azure Key Vaultなどの外部KMSと連携し、キー自体をクラスター外部で管理するのが正解です。
🔍 脆弱性2 — Pod作成権限でSecret RBACを迂回する
最も巧妙な脆弱性
これが3つの中で最も巧妙です。あるユーザーがsecretsリソースに対するget権限を持っていなくても、Podを作成できる権限があればSecretにアクセスできます。
RBACシステムは、権限のあるユーザーのみがPodを作成するように強制しますが、そのユーザーがPod定義の中に何を入れるかは制御しません。適切なAdmission Controlがなければ、Pod作成権限を持つユーザーは任意のイメージと任意の権限を持つコンテナを作成できます。
攻撃シナリオ3つ
シナリオA: 環境変数でSecretを抜き出す
# Secretのget権限がなくても、このようにPodを作成すると...
apiVersion: v1
kind: Pod
metadata:
name: secret-stealer
spec:
containers:
- name: attacker
image: busybox
command: ["sh", "-c", "echo $DB_PASSWORD && sleep 3600"]
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials # アクセスしたいSecret
key: password
Podログを見ると、DB_PASSWORDの値がそのまま出力されます。
シナリオB: Service Accountトークン奪取
Podが高い権限を持つService Accountのトークンを奪取するように設計された悪性イメージを使用するPodを作成できます。攻撃者はこのトークンを利用してAPIアクションを実行し、権限を昇格させることができます。
# Pod内部でマウントされたトークンを確認
cat /var/run/secrets/kubernetes.io/serviceaccount/token
# このトークンでAPIサーバーにリクエスト
curl -H "Authorization: Bearer $(cat /var/run/secrets/...)" https://k8s-api/api/v1/secrets
シナリオC: hostPathマウントでetcdに直接アクセス
ノードセレクターを使用してコントロールプレーンノードにPodをスケジュールし、ホストファイルシステムをマウントできる場合、そのノードにchrootしてルート権限を獲得し、etcdデータベースを直接読み取ることができます。
対応方法
# Admission Controllerで特権Podをブロック
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
# またはPod Security Admissionを使用
---
# namespaceにPSAラベルを適用
kubectl label namespace production
pod-security.kubernetes.io/enforce=restricted
pod-security.kubernetes.io/enforce-version=latest
核心原則:
- automountServiceAccountToken: false をデフォルトで設定
- hostPath, hostPID, hostNetwork マウント禁止ポリシー
- Admission Controller(OPA Gatekeeper, Kyverno)でイメージの出所および権限を検証
🔍 脆弱性3 — Secretアクセス監査ログの構造的空白
この部分が核心です。PodがマウントされたSecretを読み込むたびに監査ログが記録されません。なぜ?
Kubernetes監査ログの動作方式
Kubernetes監査ログはkube-apiserverコンポーネント内部で開始されます。APIサーバーに対する各リクエスト(request)ごとに監査イベントが生成されます。
ここに核心的な問題があります。監査ログはAPIサーバーを介したリクエストに対してのみ記録されます。
SecretがPodに渡される方式
[파드 생성 시]
API Server → kubelet → 파드 볼륨 마운트
↑
이 시점에 1번만 Secret GET 요청 발생 → 로그 기록됨
[파드 실행 중]
파드 컨테이너 → /var/run/secrets/... 파일 직접 읽기
↑
이건 그냥 파일시스템 read() 시스템 콜
→ API 서버를 거치지 않음 → 감사 로그 없음
Secretが環境変数として注入された場合も同様です。コンテナランタイムがPod起動時に一度値を取得して環境変数として設定し、その後のアクセスはすべてプロセスレベルで発生します。APIサーバーは無関係です。
KubernetesはSecretアクセスに関する詳細な監査ログをデフォルトで提供しません。これにより、誰がいつSecretにアクセスしたかを監視することが困難になります。
なぜこれを「放置」するのか?
設計の観点から見ると、意図されたトレードオフに近いものです:
- パフォーマンス: Secretをファイルとしてマウントすれば、コンテナが数百回読み込んでもAPIサーバーの負荷はありません。毎回API呼び出しで処理すると、クラスター全体のパフォーマンスが急落します。
- ボリュームマウントのアーキテクチャ的限界: kubeletはSecretをtmpfsにマウントします。その後のアクセスはOSカーネルレベルのファイルI/Oです。Kubernetesがこれを傍受する方法はありません。
- 監査ログのデフォルト無効化: デフォルトではKubernetesクラスターは監査ログが有効になっておらず、監査ポリシーを作成しkube-apiserver設定に明示する必要があります。
監査ログ設定 + 限界認識
# /etc/kubernetes/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
# Secret APIリクエストはMetadataレベルで記録 (値の露出防止)
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
# Podの作成/削除は追跡
- level: RequestResponse
verbs: ["create", "delete", "patch"]
resources:
- group: ""
resources: ["pods"]
⚠️ 注意: RequestResponseレベルでSecretをロギングすると、Secret値自体が監査ログに記録されます。監査ログが別の情報漏洩経路となります。Metadataレベルが現実的な妥協案です。
真の解決策 — ランタイム監査
PodのSecretファイルアクセスまで検知するには、OSレベルのツールが必要です。
# Falcoルール例 — /var/run/secretsへのアクセスを検出
- rule: Read Kubernetes Service Account Token
desc: Detect any read of a Kubernetes service account token
condition: >
open_read
and fd.name startswith /var/run/secrets/kubernetes.io/serviceaccount
and not proc.name in (known_sa_readers)
output: >
"Service account token read (user=%user.name command=%proc.cmdline
file=%fd.name container=%container.name)"
priority: WARNING
Falco、Tetragon、eBPFベースのランタイムセキュリティツールがこの空白を埋めます。
⚠️ 注意事項 / よくある間違い
- AES-CBCはもはや安全ではありません — 必ずKMSまたはAES-GCMを使用してください
- 監査ログにSecret RequestResponseレベル設定を禁止 — ログにSecret値がそのまま記録されます
- pods/exec権限もSecret露出経路です — execでコンテナに侵入するとマウントされたSecretファイルにアクセス可能。この経路はKubelet APIを直接使用するためAPIサーバーを迂回し、監査ログすら残りません。
- 暗号化キーを誤って削除するとクラスターの復旧が不可能になります。キー管理はVaultやクラウドKMSに委任してください。
✅ まとめ — 3つの脆弱性対応要約
| 脆弱性 | 核心問題 | 対応 |
| 1. 暗号化未適用 | Base64のみ適用、暗号化なし | KMS連携 + EncryptionConfiguration |
| 2. Pod作成で迂回 | RBACがPodコンテンツを制御できない | PSA Restricted + OPA Gatekeeper |
| 3. 監査空白 | APIサーバー迂回ファイルアクセスは未記録 | Falco/Tetragon eBPFランタイム監視 |
3つの脆弱性すべてが「機能欠陥」ではなく、パフォーマンス・柔軟性とセキュリティ間のトレードオフから生じています。これを理解することで、正しい対応が可能になります。
次のステップとしては、HashiCorp VaultのDynamic SecretまたはKubernetes Secrets Store CSI Driverを通じてSecretをetcdから完全に分離するアーキテクチャを検討することをお勧めします。

コメントを残す