更新日: 2026年8月22日
HashiCorp Vault と External Secrets Operator(ESO)を組み合わせると、Kubernetes ネイティブな Secret リソースの脆弱性を大部分排除しつつ、Vault を単一の信頼できるソース(Source of Truth)としたゼロトラストなシークレット管理基盤を構築できます。本稿は、私が複数のマルチテナント Kubernetes プラットフォームで実運用してきた構成をベースに、脅威モデル・爆発半径(blast radius)・自動ローテーション・GitOps 統合までを一気通貫で解説する 2026 年版の実装ガイドです。正直なところ、初めて Vault を本番投入したときは Auto-Unseal の設定ミスで復旧に半日溶かしたので、そのあたりの落とし穴もあわせて共有します。まずは etcd に base64 で置かれる標準 Secret の限界と、Vault + ESO がその何を解決するのかから始めましょう。
- Kubernetes 標準の
Secret は base64 で etcd に平文相当で保存されるため、EncryptionConfiguration・KMS・RBAC・NetworkPolicy の 4 層でも運用は複雑化しやすい。
- Vault は KV v2・Transit・PKI・Database の各シークレットエンジンで「保存」「暗号化 API」「動的発行」を分離し、静的シークレットの総量を減らせる。
- External Secrets Operator は Vault などの外部ストアから
ExternalSecret CRD 経由で Kubernetes Secret を同期し、GitOps 中に平文シークレットを含めない運用を可能にする。
- Vault の Kubernetes 認証は ServiceAccount JWT を TokenReview API で検証し、Pod ID に紐づいた短命トークン発行によりゼロトラストの原則(明示的検証・最小権限・侵害前提)を満たす。
- Sealed Secrets・SOPS・Vault Agent Injector・CSI Secrets Store CSI との使い分けは、GitOps 適合性・ローテーション頻度・アプリ改修可否の 3 軸で決まる。
- 本番導入時は、Vault の HA・Auto-Unseal(KMS/HSM)・監査ログ二重化・ESO の
refreshInterval・Argo CD の SyncOptions をセットで設計すること。
なぜ Vault と External Secrets Operator を使うのか
Kubernetes の標準 Secret は「暗号化されているように見える base64 データ」でしかありません。etcd に対する EncryptionConfiguration と KMS プロバイダを設定して初めて保存時暗号化が実現しますが、それでも API 経由で get 権限を持つあらゆる主体は復号済み値を取得できます。さらに GitOps を採用すると「シークレットをどうリポジトリに置くか」という古典的問題が浮上します。
ここで私が繰り返し設計に採用しているのが、HashiCorp Vault を単一のシークレットソースとし、External Secrets Operator(ESO)で Kubernetes 側に同期する構成です。この設計の勘所は 3 つあります。第一に、シークレットの正本を Kubernetes の外に置くことで、クラスタ侵害時の爆発半径がクラスタ ScopedRole を越えないこと。第二に、Vault の動的シークレットエンジン(Database・AWS・PKI)でデータベース資格情報や証明書を都度発行し、静的シークレットの総量を削減できること。第三に、ESO の refreshInterval で自動ローテーションを宣言的に運用できることです。
この構成は特に、ArgoCD や Flux による GitOps を採用しているチーム、Zero Trust アーキテクチャの実装を進めているチーム、SOC 2 Type II や ISO/IEC 27001 の監査対応でシークレットローテーション証跡を求められているチームで威力を発揮します。関連するCosign と Kyverno によるサプライチェーン防御と組み合わせれば、コンテナイメージの署名検証とシークレット配布の両方を宣言的に守ることができます。
Kubernetes シークレット管理の脅威モデルと爆発半径
設計判断を語る前に、私が必ずホワイトボードに書く 4 つの脅威シナリオを整理します。何が最初に壊れるのか(What breaks first?)を明確にしておくと、対策の優先順位がぶれません。ちなみに、実際のインシデントレビューでもこの 4 分類のどれかに大抵は落ちます。
脅威 1: 侵害された Pod からのシークレット窃取
攻撃者がアプリコンテナ内でシェル取得した場合、標準 Secret をボリュームマウントしていれば cat /var/run/secrets/... で即座に取得可能です。Vault Agent Injector 方式ならメモリ上のみに配置できますが、それでも同 Pod 内なら読めます。真の防御は「シークレットの寿命を短くする」= 動的シークレット化に尽きます。
脅威 2: 悪意のある Cluster Admin または RBAC の設定ミス
secrets リソースに対する list・get 権限を過剰付与するのは典型的な事故要因です。Vault + ESO では、Vault ポリシーで名前空間(Namespace)ごとに読み取り可能なシークレットパスを厳密に区切り、Kubernetes 側では ESO の ServiceAccount にのみ権限を集約します。
脅威 3: etcd バックアップの流出
etcd スナップショットが暗号化されずにオブジェクトストレージへ置かれるのは、私が最も頻繁に指摘するアンチパターンです。Vault 経由のみでシークレットを扱えば、etcd に載る値はエフェメラルで短命な同期結果のみになります。
脅威 4: Git リポジトリ経由の情報漏えい
GitOps 全盛期において、Kubernetes マニフェストに平文シークレットを含めてはいけないのは自明ですが、Sealed Secrets のような「暗号化したまま Git に入れる」方式でも、鍵の紛失・アルゴリズム将来の脆弱化・ローテーション欠如がリスクになります。ESO と ExternalSecret の組み合わせは、Git 上には参照だけを置き、実データは常に Vault という原則を守ります。
Kubernetes 上に Vault を Helm で高可用構成でデプロイする
本番相当の Vault は最低 3 ノードの Raft ストレージ HA 構成が基本です。以下は 2026 年 8 月時点で私が推奨する Helm values の要点で、Auto-Unseal は AWS KMS を例にしています(GCP KMS・Azure Key Vault・HSM も同じ考え方です)。
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
cat > vault-values.yaml <<'EOF'
server:
ha:
enabled: true
replicas: 3
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 0
tls_cert_file = "/vault/userconfig/vault-tls/tls.crt"
tls_key_file = "/vault/userconfig/vault-tls/tls.key"
}
storage "raft" {
path = "/vault/data"
retry_join { leader_api_addr = "https://vault-0.vault-internal:8200" }
retry_join { leader_api_addr = "https://vault-1.vault-internal:8200" }
retry_join { leader_api_addr = "https://vault-2.vault-internal:8200" }
}
seal "awskms" {
region = "ap-northeast-1"
kms_key_id = "alias/vault-unseal"
}
service_registration "kubernetes" {}
extraEnvironmentVars:
VAULT_CACERT: /vault/userconfig/vault-tls/ca.crt
volumes:
- name: vault-tls
secret:
secretName: vault-tls
volumeMounts:
- name: vault-tls
mountPath: /vault/userconfig/vault-tls
readOnly: true
injector:
enabled: false # ESO 中心の運用ではインジェクターは不要
EOF
helm upgrade --install vault hashicorp/vault \
--namespace vault --create-namespace \
--version 0.30.0 \
-f vault-values.yaml
初回のみ、いずれかの Pod で vault operator init -recovery-shares=5 -recovery-threshold=3 を実行し、Recovery Keys を厳格に分割保管します。KMS Auto-Unseal を使っているため通常運用で Unseal Key を触ることはありませんが、Recovery Keys はRoot Token の再発行とKMS 障害時の緊急復旧に必要です。
Vault の Kubernetes 認証と ServiceAccount JWT の仕組み
ESO が Vault からシークレットを取得するには「誰であるか」を Vault に証明する必要があります。ここで採用するのが Kubernetes 認証メソッドです。仕組みはシンプルで、ESO Pod にマウントされた ServiceAccount JWT を Vault に提示し、Vault は API サーバの TokenReview API でその JWT の有効性と紐づく ServiceAccount 名を検証します。
# Vault 側: Kubernetes 認証を有効化
vault auth enable kubernetes
# API サーバの JWT 発行者情報を Vault に登録
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
disable_iss_validation=false
# 名前空間 payments-prod の ESO SA に KV v2 の /apps/payments/* を読ませるポリシー
cat > payments-read.hcl <<'EOF'
path "secret/data/apps/payments/*" {
capabilities = ["read"]
}
EOF
vault policy write payments-read payments-read.hcl
vault write auth/kubernetes/role/payments-prod \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=payments-prod \
policies=payments-read \
ttl=15m \
max_ttl=1h
ここで最も重要なのは ttl と max_ttl の短さです。仮に ESO Pod ごと侵害されても、そこから盗まれる Vault トークンは 15 分で失効します。より厳格にするには、Vault Enterprise の Namespaces 機能でテナント境界を分離するか、OSS 環境でも Kubernetes 側で Pod Security Standards の restricted プロファイルを強制し、権限昇格を止めます。
Kubernetes 側で気をつけるのは、serviceAccountToken のプロジェクション設定です。ESO はデフォルトで境界のあるオーディエンス(audiences: [vault])を指定した Projected Volume を使うため、他コンポーネント(例えば API サーバ自身)向けに発行された JWT を Vault に流用される攻撃を防げます。
External Secrets Operator のインストールと SecretStore 設計
ESO 本体のインストールは Helm で完結します。私は必ず ClusterExternalSecret・PushSecret のような Cluster-scoped リソースの利用可否を検討し、テナント境界と噛み合っているかを確認します。
helm repo add external-secrets https://charts.external-secrets.io
helm upgrade --install external-secrets external-secrets/external-secrets \
--namespace external-secrets --create-namespace \
--version 0.10.5 \
--set installCRDs=true \
--set webhook.port=9443 \
--set certController.create=true
次に、テナント(例: payments-prod)ごとの SecretStore を定義します。ClusterSecretStore は全名前空間から参照可能でトポロジが単純になりますが、その分「1 つのポリシーミスがクラスタ全体に波及する」ため、私はマルチテナント環境ではあえて名前空間スコープの SecretStore を使い分けます。
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: payments-prod
spec:
provider:
vault:
server: "https://vault.vault.svc:8200"
path: "secret"
version: "v2"
caProvider:
type: ConfigMap
name: vault-ca
key: ca.crt
auth:
kubernetes:
mountPath: "kubernetes"
role: "payments-prod"
serviceAccountRef:
name: external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payments-db
namespace: payments-prod
spec:
refreshInterval: "5m"
secretStoreRef:
kind: SecretStore
name: vault-backend
target:
name: payments-db
creationPolicy: Owner
template:
type: Opaque
metadata:
labels:
app.kubernetes.io/managed-by: external-secrets
data:
- secretKey: DATABASE_URL
remoteRef:
key: apps/payments/db
property: url
- secretKey: DATABASE_PASSWORD
remoteRef:
key: apps/payments/db
property: password
refreshInterval: 5m の意味は「Vault 側でシークレットが更新されたら最大 5 分で Kubernetes Secret に反映される」です。ただし Pod 側で環境変数として消費している場合は再起動が必要になるため、動的シークレットではボリュームマウント+シグナル通知や、後述の Reloader を組み合わせます。
動的シークレット・自動ローテーション・TTL 設計
Vault の真価は、静的シークレットの保管ではなく動的シークレットにあります。Database シークレットエンジンを設定すれば、アプリが要求した瞬間に PostgreSQL 上でユーザを作成し、TTL 経過後に自動削除できます。
vault secrets enable database
vault write database/config/payments-postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="payments-app" \
connection_url="postgresql://{{username}}:{{password}}@pg.internal:5432/payments?sslmode=require" \
username="vault-admin" \
password="$(pwgen -s 32 1)"
vault write database/roles/payments-app \
db_name=payments-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
ESO 側は generatorRef または Vault プロバイダの database パス直接参照でこの資格情報を取得できます。私が推奨するのは refreshInterval: 45m(TTL の 3/4)を設定し、Vault が失効させる前に前倒しで新しい認証情報に切り替えるパターンです。組み合わせで Stakater Reloader を使えば、Secret 更新時に対象 Deployment を自動ローリングリスタートできます。
証明書についても同じ考え方が使えます。Vault の PKI 二次 CA を発行し、cert-manager の Vault Issuer 経由で mTLS 証明書を短命(24h〜72h)で回すことで、証明書の失効管理コストを実質ゼロにできます。これはeBPF によるランタイム脅威検知と組み合わせると、侵害された Pod からの横移動を検知した瞬間に証明書を無効化する運用も現実的になります。
Sealed Secrets・SOPS・Vault Agent Injector・CSI Driver の比較
Kubernetes のシークレット管理手法は複数あり、どれが正解かは要件次第です。以下の比較表は、私がアーキテクチャレビューで最初に描くものです。
| 観点 |
Vault + ESO |
Vault Agent Injector |
Secrets Store CSI + Vault |
Sealed Secrets |
SOPS + age/KMS |
| 正本の保存場所 | Vault | Vault | Vault | Git(暗号化) | Git(暗号化) |
| GitOps 適合性 | 非常に高い | 高い | 高い | 中 | 中 |
| アプリ改修 | 不要 | ほぼ不要 | マウント設定必要 | 不要 | 不要 |
| 動的シークレット対応 | ◎ | ◎ | ○ | × | × |
| 自動ローテーション | ◎(refreshInterval) | ○(テンプレート更新) | ○(rotationPollInterval) | ×(手動) | ×(手動) |
| ブートストラップ複雑度 | 中 | 中 | やや高 | 低 | 低 |
| Vault 停止時の影響 | キャッシュ済み Secret は生存 | Pod 起動不可 | Pod 起動不可 | 影響なし | 影響なし |
| 推奨規模 | マルチテナント本番 | 単一アプリ | コンプライアンス重視 | SMB/ラボ | 単一チーム |
マルチテナントで運用性・GitOps・ローテーションの 3 拍子を求めるなら、ESO 一択と言い切って構いません。逆にごく小規模なチームや、Vault の運用コストを負えない環境では Sealed Secrets や SOPS が現実解です。
Argo CD と GitOps でのシークレット管理パターン
Argo CD で ESO を運用する場合、私は以下 3 点を強く推奨します。
ExternalSecret は Argo CD 管理対象、実 Secret は管理対象外: Application の syncOptions に Replace=false と ServerSideApply=true を指定し、Secret リソース自体には argocd.argoproj.io/compare-options: IgnoreExtraneous ラベルを付ける。これで Argo CD が Vault 更新に伴う Secret 差分を「OutOfSync」と誤認しない。
- ブートストラップの卵と鶏問題を Sealed Secrets で回避: Vault の TLS 証明書やルート CA など、ESO 自身が動く前に必要なシークレットは、Sealed Secrets で最小限に閉じる。ESO 起動後はそちらへ移行。
- ApplicationSet でテナントごとに
SecretStore を展開: 名前空間ごとに SecretStore・ServiceAccount・RBAC を Helm/Kustomize テンプレート化し、ApplicationSet の generator で自動生成する。テナント追加時のドリフトを最小化。
これらはLynis と OpenSCAP による Linux セキュリティ監査で見てきたのと同じ「宣言+証跡」の原則を Kubernetes に持ち込む思想です。
監視・監査・コンプライアンス(SOC 2・ISO 27001)
Vault の監査ログは Sink 単位で複数出力できます。私は必ず「ファイル Sink(ローカル調査用)+ Syslog Sink(SIEM 転送用)」の 2 系統を有効にし、SIEM 側で 90 日以上保持します。
vault audit enable file file_path=/vault/audit/vault-audit.log
vault audit enable syslog tag="vault" facility="AUTH"
ESO 側は Prometheus メトリクスをネイティブで公開しており、externalsecret_sync_calls_error・externalsecret_status_condition をアラート対象にします。私が本番で必ず設定するアラートは以下の 3 つです。
- Vault シール状態
vault_core_unsealed == 0 が 60 秒継続
- ESO 同期エラー率が 5 分平均で 5% 超
- Vault 認証失敗が 10 分間で 20 回超(総当たりや誤設定の兆候)
コンプライアンス面では、SOC 2 CC6.1(論理アクセス制御)と ISO/IEC 27001 A.9(アクセス制御)の観点で、Vault のポリシー適用証跡と ESO の同期証跡が「シークレットが誰にいつ発行されたか」を機械可読な形で残すため、監査対応が劇的に楽になります。私が実際に監査人へ提示するのは Vault の audit ログ、ESO の Kubernetes Event、Argo CD の同期履歴の 3 点セットです。
よくある失敗と本番運用チェックリスト
最後に、私が実運用で踏んだ/踏まれた落とし穴を共有します。設計フェーズで潰しておいてください。
- ネットワーク到達性の設計漏れ: ESO Pod → Vault の疎通が NetworkPolicy で切れているケース。
egress で Vault の Service に対する 8200/TCP を明示的に許可する。
- Vault トークン TTL の短すぎ/長すぎ: 30 秒などにすると更新の Race Condition で 5xx が頻発。15 分〜1 時間を出発点にする。
refreshInterval と Deployment 再起動戦略の不整合: envFrom でシークレットを注入している場合、Secret の更新は Pod に伝わらない。Reloader 導入か、ボリュームマウントに変更する。
- クラスタバックアップに Vault が含まれない: Vault 自身の Raft スナップショットは
vault operator raft snapshot save で別ストレージへ定期的に取得する。
- Vault 停止時のフェイルモード未検証: 「Vault が落ちたときアプリはどう振る舞うか」を実際に停止して確認する。既存
Secret は生き残るが、新規 Pod のスケジューリング直前更新は失敗し得る。
よくある質問
ExternalSecret と Sealed Secrets の違いは何ですか?
ExternalSecret は Vault 等の外部ストアからシークレットを「参照」して Kubernetes Secret に同期する仕組みで、Git には参照定義のみを置きます。Sealed Secrets は暗号化済みシークレットそのものを Git に置く方式で、正本を外部化しません。動的シークレットや自動ローテーションが必要なら ExternalSecret、Vault の運用コストを負えない小規模環境なら Sealed Secrets が向いています。
Vault のシールドされたシークレットはどのように保護されていますか?
Vault は保存時にすべてのシークレットを AES-256-GCM で暗号化し、その暗号鍵(Master Key)は Shamir Secret Sharing または KMS/HSM ベースの Auto-Unseal で保護されます。Vault プロセス停止時は暗号鍵がメモリから消え、次回起動時には Unseal もしくは Auto-Unseal が必要です。これにより、単純なディスク持ち出しでは平文シークレットは復元できません。
Vault トークンの TTL を短くしすぎるとどうなりますか?
TTL を極端に短く(例: 30 秒)すると、ESO の更新処理と Vault の失効判定が競合し、断続的に permission denied エラーが発生します。私は最小 15 分・最大 1 時間を出発点として、実運用のログを見ながら調整することを勧めます。動的シークレットの TTL は別軸で、Database なら 1 時間、証明書なら 24〜72 時間が現実的な目安です。
External Secrets Operator でシークレットローテーションはどう自動化しますか?
ExternalSecret の refreshInterval を設定すると、指定間隔で Vault から最新値を取得し Kubernetes Secret を更新します。ただし Pod 側で envFrom を使っていると再起動が必要なため、Stakater Reloader などで Secret 更新をトリガに Deployment をローリングリスタートさせるのが定石です。動的シークレットでは TTL の 3/4 程度を refreshInterval にして先行更新するとダウンタイムを回避できます。
Vault が停止したら Kubernetes 上のワークロードはどうなりますか?
すでに同期された Kubernetes Secret はそのまま生存するため、既存 Pod は動き続けます。影響を受けるのは Vault 到達性を必要とする新規同期・新規 Pod スケジューリング直前の再取得・動的シークレットの延長です。可用性要件が高い場合は Vault を 3 ノード以上の Raft HA で構成し、ESO の refreshInterval を Vault のダウンタイム許容時間より短く保つ設計にします。