DevOps GitOpsシークレット管理:Sealed SecretsからExternal Secretsまでの6つの本番パターン
SecretをGitにコミット?時限爆弾を埋めている
GitOpsの中核は「GitがSingle Source of Truth」ですが、Kubernetes SecretはデフォルトでBase64エンコードに過ぎません——暗号化ではありません。Base64エンコードされたSecretをGitリポジトリにプッシュするのは、データベースパスワード、API Key、TLS証明書を平文で公開するのと同じです。リポジトリが漏洩すれば(内部の誤操作、サードパーティのサプライチェーン侵害、フォークリポジトリの消し忘れ)、すべてのシークレットが瞬時に暴露されます。
2026年、DevOps GitOpsシークレット管理はもはや「オプション」ではなく、本番運用の必須要件です。本記事では6つの本番グレードのシークレット管理パターンを解説し、Sealed Secretsの暗号化ストレージからExternal Secrets Operatorの動的インジェクションまで、GitOpsにおけるシークレットセキュリティを完全に解決します。
主要な学び
- GitOpsシークレット管理の3つのアーキテクチャパターン:暗号化ストレージ、外部インジェクション、ハイブリッド
- Sealed Secrets、External Secrets Operator、SOPSの完全なデプロイと設定
- シークレットの自動ローテーションとマルチクラスター同期の実装
- 5つのよくある本番の落とし穴と10の高頻度エラーを回避
- ツール選択の比較意思決定マトリックス
目次
- GitOpsシークレット管理のコア概念
- パターン1: Sealed Secrets暗号化ストレージ
- パターン2: External Secrets Operator + Vault
- パターン3: SOPS + Age/GPG暗号化
- パターン4: シークレット自動ローテーション
- パターン5: マルチクラスター同期
- パターン6: 監査とコンプライアンス
- 5つのよくある落とし穴と解決策
- 10のよくあるエラートラブルシューティング
- 高度な最適化テクニック
- 比較分析
- オンラインツール推奨
GitOpsシークレット管理のコア概念
3つのアーキテクチャパターン
┌─────────────────────────────────────────────────────────────┐
│ GitOpsシークレット管理:3つのアーキテクチャ │
├─────────────────┬──────────────────┬────────────────────────┤
│ 暗号化ストレージ │ 外部インジェクション│ ハイブリッド │
│ (Sealed Secrets)│ (ESO + Vault) │ (SOPS + ESO) │
├─────────────────┼──────────────────┼────────────────────────┤
│ Gitに暗号文保存 │ Gitに参照のみ保存 │ Gitに暗号文+参照保存 │
│ クラスタ内で復号 │ クラスタ内で平文 │ クラスタ内で復号 │
│ │ をプル │ またはプル │
│ オフライン監査 │ 外部サービス依存 │ 柔軟な組み合わせ │
│ ローテーションは │ ネイティブ対応 │ 部分対応 │
│ 再暗号化が必要 │ │ │
└─────────────────┴──────────────────┴────────────────────────┘
K8s Secretの根本的な問題
# 標準的なSecretを作成
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!'
# 「暗号化」はBase64エンコードに過ぎない——誰でもデコード可能
kubectl get secret db-credentials -o yaml
# デコードは1行のコマンドで可能
kubectl get secret db-credentials \
-o jsonpath='{.data.password}' | base64 -d
# 出力: SuperSecret123!
# ToolsKu Base64エンコーダ/デコーダで確認
# https://toolsku.com/ja/encode/base64
DevOps GitOpsシークレット管理のコア原則
| 原則 | 説明 | 違反の結果 |
|---|---|---|
| ゼロ平文 | Gitに平文シークレットを保存しない | リポジトリ漏洩=全面侵害 |
| 最小権限 | 各アプリは必要なシークレットのみアクセス | 横方向的なシークレット漏洩 |
| 自動ローテーション | シークレットを定期的に自動更新 | 長期シークレットのブルートフォース |
| 監査可能 | すべてのシークレットアクセスを記録 | 漏洩元を追跡不可 |
| 復旧可能 | シークレット紛失後に迅速に復旧 | ビジネス中断 |
パターン1: Sealed Secrets暗号化ストレージ
Sealed SecretsはBitnamiがオープンソースで提供するGitOpsシークレット管理ソリューションです。コアアイデア:ローカルでkubesealを使ってSecretを暗号化し、SealedSecretリソースを生成してGitにコミット、クラスタ内のSealed Secrets Controllerが自動的にK8s Secretに復号します。
アーキテクチャ
┌──────────────────────────────────────────────────────┐
│ 開発者ワークステーション │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ kubeseal │ │
│ │ (平文, │ │ (暗号化ツール)│ │
│ │ コミット不可)│ │ │ │
│ └────────────┘ └─────┬──────┘ │
│ │ 暗号化 │
│ ┌─────▼──────┐ │
│ │sealed-secret│ │
│ │.yaml(暗号文)│ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Git Repository │
│ (暗号化されたSealedSecretのみ保存) │
└──────────────────────────┬───────────────────────────┘
│ git pull (ArgoCD/Flux)
┌──────────────────────────▼───────────────────────────┐
│ K8s Cluster │
│ ┌──────────────────────────────┐ │
│ │ Sealed Secrets Controller │ │
│ │ (秘密鍵でSealedSecretを復号) │ │
│ └──────────────┬───────────────┘ │
│ │ 復号 │
│ ┌─────▼──────┐ │
│ │ K8s Secret │ │
│ │ (平文, │ │
│ │ クラスタ内) │ │
│ └────────────┘ │
└───────────────────────────────────────────────────────┘
Sealed Secretsのインストール
# Controllerをクラスタにインストール
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml
# Controllerの準備完了を待機
kubectl wait --for=condition=available --timeout=120s \
deployment/sealed-secrets-controller -n kube-system
# kubeseal CLIのインストール
KUBESEAL_VERSION=0.27.0
curl -sLO "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-linux-amd64"
chmod +x kubeseal-linux-amd64
sudo mv kubeseal-linux-amd64 /usr/local/bin/kubeseal
# インストール確認
kubeseal --version
Secretの暗号化
# 平文Secretファイルから暗号化
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='SuperSecret123!' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
# ファイルから作成
kubectl create secret generic tls-cert \
--from-file=tls.crt=server.crt \
--from-file=tls.key=server.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-tls.yaml
# Namespaceと名前を指定(strictモード)
kubectl create secret generic api-key \
--from-literal=key=abc123xyz \
--namespace production \
--dry-run=client -o yaml | \
kubeseal --format yaml \
--scope namespace-wide > sealed-api-key.yaml
SealedSecretリソース例
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
username: AgBfj8k2mN3pQ7sT9vWxYz...
password: AgCdH5lM6nO8qR0tU2wXyZa...
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
暗号化スコープ制御
# strict: 同じNamespaceと名前でのみ復号可能(デフォルト)
kubeseal --scope strict
# namespace-wide: 同じNamespace内でリネーム可能
kubeseal --scope namespace-wide
# cluster-wide: クラスタ内の任意のNamespaceで使用可能
kubeseal --scope cluster-wide
| スコープ | セキュリティレベル | 柔軟性 | ユースケース |
|---|---|---|---|
| strict | 最高 | 最低 | 本番シークレット |
| namespace-wide | 中 | 中 | 同一Namespaceの複数アプリ |
| cluster-wide | 最低 | 最高 | 証明書などの共有リソース |
マスターキーのバックアップとリストア
# マスターキーのバックアップ(紛失するとすべてのSealedSecretが復号不可!)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml
# 新しいクラスタにマスターキーをリストア
kubectl apply -f sealed-secrets-key-backup.yaml
# マスターキーのローテーション
kubectl delete secret -n kube-system sealed-secrets-key
# Controllerが自動的に新しいキーを生成、古いSealedSecretも引き続き使用可能
パターン2: External Secrets Operator + Vault
External Secrets Operator(ESO)は、外部シークレット管理システム(Vault、AWS Secrets Manager、GCP Secret Managerなど)とKubernetesを統合します。GitリポジトリにはSecretStoreの参照のみを保存し、クラスタ内のESO Controllerが外部システムからシークレットをプルしてK8s Secretを作成します。
アーキテクチャ
┌──────────────────────────────────────────────────────┐
│ Git Repository │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ SecretStore │ │ ExternalSecret│ │
│ │ (Vault接続 │ │ (シークレット │ │
│ │ 設定) │ │ 参照マッピング)│ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────┬───────────────────────────────┘
│ git pull
┌──────────────────────▼───────────────────────────────┐
│ K8s Cluster │
│ ┌──────────────────────────────┐ │
│ │ External Secrets Operator │ │
│ │ (ExternalSecretを監視) │ │
│ └──────────────┬───────────────┘ │
│ │ シークレット取得 │
│ ┌────────────▼────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ K8s │ │ HashiCorp│ │
│ │ Secret │ │ Vault │ │
│ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────┘
ESOのインストール
# Helmでインストール
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets \
external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace \
--set installCRDs=true \
--set replicaCount=2 \
--set leaderElect=true
# 確認
kubectl get pods -n external-secrets
HashiCorp Vault SecretStoreの設定
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-role"
serviceAccountRef:
name: "external-secrets-sa"
Vaultポリシー設定
# Vaultポリシー:ESOを特定のパスに制限
path "secret/data/production/*" {
capabilities = ["read"]
}
path "secret/data/production/database/*" {
capabilities = ["read", "list"]
}
# 他の環境へのアクセスを拒否
path "secret/data/staging/*" {
capabilities = ["deny"]
}
path "secret/data/development/*" {
capabilities = ["deny"]
}
ExternalSecretリソース
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
template:
type: Opaque
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.internal:5432/mydb"
data:
- secretKey: username
remoteRef:
key: secret/data/production/database
property: username
- secretKey: password
remoteRef:
key: secret/data/production/database
property: password
マルチソース:ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: eso-aws-sa
namespace: external-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: aws-api-keys
namespace: production
spec:
refreshInterval: 5m
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: aws-api-keys
dataFrom:
- extract:
key: production/api-keys
パターン3: SOPS + Age/GPG暗号化
SOPS(Secrets OPerationS)はMozillaが開発したシークレット暗号化ツールで、AES、PGP、Ageなどの複数の暗号化バックエンドをサポートしています。Sealed Secretsとは異なり、SOPSはファイル自体を暗号化し、YAML/JSON/ENVなどのフォーマットに対応。値のみを暗号化しキーは暗号化しないため、Git diffでシークレットの構造変更を確認できます。
アーキテクチャ
┌──────────────────────────────────────────────────────┐
│ 開発者ワークステーション │
│ ┌────────────┐ ┌────────────┐ │
│ │ secret.yaml │───▶│ SOPS │ │
│ │ (平文, │ │ + Age/GPG │ │
│ │ コミット不可)│ │ │ │
│ └────────────┘ └─────┬──────┘ │
│ │ 暗号化 │
│ ┌─────▼──────┐ │
│ │secret.enc. │ │
│ │yaml(暗号文) │ │
│ └─────┬──────┘ │
└──────────────────────────┼───────────────────────────┘
│ git push
┌──────────────────────────▼───────────────────────────┐
│ Git Repository │
│ (暗号化ファイルを保存、キー名は可視、 │
│ 値は暗号化) │
└──────────────────────────┬───────────────────────────┘
│ git pull
┌──────────────────────────▼───────────────────────────┐
│ CI/CD Pipeline │
│ ┌──────────────────────────────┐ │
│ │ sops --decrypt + kubectl apply│ │
│ │ またはFlux Kustomization SOPS │ │
│ │ インテグレーション │ │
│ └──────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
SOPSとAgeのインストール
# SOPSのインストール
curl -sLO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops
# Ageのインストール(推奨暗号化バックエンド、GPGよりシンプル)
curl -sLO https://github.com/FiloSottile/age/releases/download/v1.2.0/age-v1.2.0-linux-amd64.tar.gz
tar xzf age-v1.2.0-linux-amd64.tar.gz
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/
# Ageキーペアの生成
age-keygen -o age.key
# 公開鍵: age1abc123xyz...
# 確認
sops --version
age --version
Secretファイルの暗号化
# secret.yaml(暗号化前)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: "SuperSecret123!"
API_KEY: "sk-proj-abc123xyz456"
REDIS_URL: "redis://redis.internal:6379"
# Age公開鍵で暗号化
sops --encrypt \
--age age1abc123xyz456... \
--encrypted-regex '^(DB_PASSWORD|API_KEY)$' \
--in-place secret.yaml
# secret.yaml(暗号化後)
apiVersion: v1
kind: Secret
metadata:
name: app-config
namespace: production
type: Opaque
stringData:
DB_HOST: "db.internal.example.com"
DB_PASSWORD: ENC[AES256_GCM,data:Wk5kPQ==,tag:abc123==,type:str]
API_KEY: ENC[AES256_GCM,data:Zm9vYmFy,tag:def456==,type:str]
REDIS_URL: "redis://redis.internal:6379"
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1abc123xyz456...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-06-16T10:00:00Z"
mac: ENC[AES256_GCM,data:abc==,tag:xyz==,type:str]
pgp: []
encrypted_regex: ^(DB_PASSWORD|API_KEY)$
version: 3.9.0
Flux CD SOPSインテグレーション
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-secrets
namespace: flux-system
spec:
interval: 10m
path: ./clusters/production
prune: true
sourceRef:
kind: GitRepository
name: flux-system
decryption:
provider: sops
secretRef:
name: sops-age-key
# Fluxの復号用にAge秘密鍵Secretを作成
kubectl create secret generic sops-age-key \
--namespace flux-system \
--from-file=age.agekey=age.key \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-sops-key.yaml
マルチキー暗号化(チームコラボレーション)
# .sops.yaml設定ファイル——プロジェクトレベルの暗号化設定
cat > .sops.yaml << 'EOF'
creation_rules:
- path_regex: ^clusters/production/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # 本番環境キー
- age1def789uvw012 # SREチームキー
- path_regex: ^clusters/staging/.*\.yaml$
key_groups:
- age:
- age1ghi345rst678 # ステージングキー
- path_regex: ^clusters/.*\.yaml$
key_groups:
- age:
- age1abc123xyz456 # デフォルトキー
EOF
パターン4: シークレット自動ローテーション
シークレットローテーションはDevOps GitOpsシークレット管理で最も見落とされがちな要素です。変更されない静的シークレットは、一度漏洩すると無限の攻撃ウィンドウを生み出します。2026年のベストプラクティス:すべての本番シークレットを90日ごとに、高機密シークレットを7日ごとにローテーション。
ローテーションアーキテクチャ
┌──────────────────────────────────────────────────────┐
│ シークレット自動ローテーションアーキテクチャ │
│ │
│ ┌──────────┐ トリガー ┌──────────────┐ │
│ │ CronJob │────────────▶│ Vault Rotate │ │
│ │ (定期実行) │ │ (新規生成) │ │
│ └──────────┘ └──────┬───────┘ │
│ │ 新しいシークレット │
│ ┌──────▼───────┐ │
│ │ ExternalSecret│ │
│ │ (自動リフレッシュ)│ │
│ └──────┬───────┘ │
│ │ 更新 │
│ ┌──────▼───────┐ │
│ │ K8s Secret │ │
│ │ (自動更新) │ │
│ └──────┬───────┘ │
│ │ ローリングリスタート │
│ ┌──────▼───────┐ │
│ │ Pods │ │
│ │ (新しいキーを読取)│ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
ESO自動ローテーション設定
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: rotating-api-key
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: api-key
creationPolicy: Owner
template:
type: Opaque
metadata:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
data:
- secretKey: api-key
remoteRef:
key: secret/data/production/api
property: key
Vault動的シークレット
# Vaultデータベース動的クレデンシャル設定
resource "vault_database_secret_backend_connection" "postgresql" {
backend = "database"
name = "postgresql-production"
allowed_roles = ["app-readonly", "app-readwrite"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@db.internal:5432/mydb?sslmode=require"
username = "vault_admin"
password = "VaultAdminPassword123!"
}
}
resource "vault_database_secret_backend_role" "app_readwrite" {
backend = "database"
name = "app-readwrite"
db_name = vault_database_secret_backend_connection.postgresql.name
default_ttl = 3600
max_ttl = 86400
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}}\";"
]
}
アプリケーションのシークレット変更自動検知
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
annotations:
secret.reloader.stakater.com/reload: "api-key,db-credentials"
spec:
containers:
- name: api-server
image: registry.example.com/api-server:v2.1.0
envFrom:
- secretRef:
name: api-key
- secretRef:
name: db-credentials
# Reloaderのインストール——Secret変更時にPodを自動ローリングリスタート
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update
helm install reloader stakater/reloader \
--namespace reloader \
--create-namespace \
--set reloader.watchGlobally=false
パターン5: マルチクラスター同期
マルチクラスター環境では、シークレットをクラスター間で安全に同期する必要があります。DevOps GitOpsシークレット管理の要件:各クラスターが独自のシークレットライフサイクルを持ちつつ、一貫性を維持する。
マルチクラススターアーキテクチャ
┌──────────────────────────────────────────────────────┐
│ マルチクラスター同期アーキテクチャ │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ HashiCorp Vault (中央) │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │prod-east/│ │prod-west/│ │ │
│ │ │ secrets │ │ secrets │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └──────────┬───────────────┬──────────┘ │
│ │ │ │
│ ┌────────▼──────┐ ┌──────▼────────┐ │
│ │ Cluster East │ │ Cluster West │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ ESO │ │ │ │ ESO │ │ │
│ │ │Controller│ │ │ │Controller│ │ │
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │
│ │ ▼ │ │ ▼ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │
│ │ │ Secrets │ │ │ │ Secrets │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ └───────────────┘ └───────────────┘ │
└──────────────────────────────────────────────────────┘
クラスターごとの独立したSecretStore
# Cluster East
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-east
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/east"
role: "eso-east-role"
serviceAccountRef:
name: "external-secrets-sa"
# Cluster West
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-west
namespace: production
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes/west"
role: "eso-west-role"
serviceAccountRef:
name: "external-secrets-sa"
TLS証明書のマルチクラスター配布
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: wildcard-tls
namespace: ingress-nginx
spec:
refreshInterval: 24h
secretStoreRef:
name: vault-east
kind: SecretStore
target:
name: wildcard-tls
creationPolicy: Owner
template:
type: kubernetes.io/tls
data:
- secretKey: tls.crt
remoteRef:
key: secret/data/shared/tls/wildcard
property: cert
- secretKey: tls.key
remoteRef:
key: secret/data/shared/tls/wildcard
property: key
Sealed Secretsマルチクラススターマスターキー同期
# マスターキーのエクスポート
kubectl get secret -n kube-system sealed-secrets-key \
-o yaml > sealed-secrets-master-key.yaml
# ターゲットクラスターにインポート
kubectl apply -f sealed-secrets-master-key.yaml -n kube-system
# Controllerを再起動してキーをロード
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
パターン6: 監査とコンプライアンス
DevOps GitOpsシークレット管理は監査要件を満たす必要があります:誰がいつどのシークレットにアクセスしたか。2026年のコンプライアンス基準(SOC2、ISO 27001、等保2.0)はすべてシークレットアクセスのトレーサビリティを要求しています。
Vault監査ログ
# Vault監査ログの有効化
audit {
type = "file"
options = {
file_path = "/vault/audit/audit.log"
mode = "0600"
}
}
# Syslog監査の有効化
audit {
type = "syslog"
options = {
facility = "AUTH"
tag = "vault"
address = "syslog.internal:514"
}
}
ESOアクセスログ
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: audited-vault
annotations:
audit.external-secrets.io/enabled: "true"
audit.external-secrets.io/log-access: "true"
spec:
provider:
vault:
server: "https://vault.internal.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "eso-audited-role"
serviceAccountRef:
name: "external-secrets-sa"
Kubernetes監査ポリシー
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
namespaces: ["production", "staging"]
omitStages:
- RequestReceived
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
namespaces: ["default"]
omitStages:
- RequestReceived
- level: RequestResponse
resources:
- group: "external-secrets.io"
resources: ["externalsecrets", "secretstores"]
omitStages:
- RequestReceived
シークレットアクセス監視
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-secret-access
spec:
validationFailureAction: Audit
background: true
rules:
- name: block-default-sa-secret-access
match:
resources:
kinds: [Pod]
validate:
message: "default ServiceAccountでのSecretアクセスは禁止されています"
pattern:
spec:
serviceAccountName: "!default"
- name: require-secret-annotations
match:
resources:
kinds: [Secret]
names: ["db-*", "api-*", "tls-*"]
validate:
message: "本番シークレットにはownerとexpiryアノテーションが必須です"
pattern:
metadata:
annotations:
owner: "?*"
expiry: "?*"
5つのよくある落とし穴と解決策
落とし穴1: Sealed Secretsマスターキーの紛失
症状:クラスター再構築後、すべてのSealedSecretが復号不可。Controllerがfailed to unsealを報告。
原因:Sealed Secretsは非対称暗号を使用。秘密鍵はクラスタ内にのみ存在。クラスター破棄で秘密鍵が消失。
解決策:
# 1. マスターキーの定期バックアップ(自動化)
kubectl get secret -n kube-system \
sealed-secrets-key -o yaml | \
sops --encrypt --age age1abc123xyz456... \
/dev/stdin > sealed-secrets-key.enc.yaml
# 2. 暗号化されたキーを別のGitリポジトリに保存
git add sealed-secrets-key.enc.yaml
git commit -m "backup: sealed secrets master key $(date +%Y%m%d)"
# 3. リカバリプロセス
sops --decrypt sealed-secrets-key.enc.yaml | \
kubectl apply -f -
落とし穴2: ExternalSecretリフレッシュ遅延によるPod起動失敗
症状:新規デプロイされたPodがSecret不存在で起動失敗。ESOがVaultからシークレットをプルする前にPodが起動しようとする。
解決策:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
spec:
refreshInterval: 5m
target:
creationPolicy: Owner
template:
type: Opaque
dataFrom:
- extract:
key: production/app-secrets
# アプリデプロイ前にESOが同期済みであることを確認
kubectl wait --for=condition=Ready \
externalsecret/app-secrets -n production --timeout=120s
落とし穴3: SOPSキーローテーション後の旧暗号文復号不可
症状:チームがAgeキーをローテーション後、古い暗号化ファイルが復号不可に。
解決策:
# マルチキー暗号化——新旧キーの共存
sops --encrypt \
--age age1NEWKEY...,age1OLDKEY... \
--in-place secret.yaml
# .sops.yamlでキーグループを管理
sops --decrypt --age age1OLDKEY... secret.yaml | \
sops --encrypt --age age1NEWKEY... \
--filename-override secret.yaml /dev/stdin > secret_new.yaml
落とし穴4: 漏洩シークレットの迅速な無効化不可
症状:API Keyが漏洩したが、SealedSecretは再暗号化してコミットが必要で、無効化ウィンドウが長すぎる。
解決策:
# ESOアプローチ:Vaultで直接旧キーを無効化
vault kv metadata put -delete-version-after=0s \
secret/data/production/api-key
# Sealed Secretsアプローチ:Controller経由で緊急削除
kubectl delete secret api-key -n production
# 汎用アプローチ:ネットワークレベルでのブロック
kubectl apply -f - <<EOF
apiVersion: networkpolicies.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-compromised
namespace: production
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- compromised-api.example.com/32
EOF
落とし穴5: マルチチームのシークレット共有によるアクセス制御の混乱
症状:複数チームが同じVault Tokenでシークレットにアクセスし、誰が何をしたか区別できない。
解決策:
# チームごとにVaultポリシーを作成
path "secret/data/team-a/*" {
capabilities = ["read", "list"]
}
path "secret/data/team-b/*" {
capabilities = ["read", "list"]
}
# クロスアクセスを明示的に拒否
path "secret/data/team-a/*" {
capabilities = ["deny"]
}
# team-bのロールにバインド
# Vault Namespaceで分離
namespace "team-a" {
path "secret/*" {
capabilities = ["read", "list", "create", "update"]
}
}
10のよくあるエラートラブルシューティング
1. failed to unseal: no private key found
kubectl get secrets -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key
kubectl apply -f sealed-secrets-key-backup.yaml -n kube-system
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system
2. ExternalSecret "not ready": could not get secret data
kubectl describe secretstore vault-backend -n production
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
kubectl auth can-i get secret -n production \
--as=system:serviceaccount:production:external-secrets-sa
3. sops error decrypting: could not find a matching key
age-keygen -y age.key
cat .sops.yaml
sops --decrypt --age age1YOURKEY... secret.yaml
4. SealedSecret "sealed-secrets" is invalid: metadata.name
kubeseal --scope strict --namespace production \
--name db-credentials < secret.yaml > sealed.yaml
5. Vault seal status: sealed
vault status
vault operator unseal <key1>
vault operator unseal <key2>
6. refreshInterval: cannot unmarshal
spec:
refreshInterval: 15m
7. kubeseal: error: failed to get certificate
kubectl get deployment sealed-secrets-controller -n kube-system
kubectl get secret -n kube-system sealed-secrets-key
kubeseal --fetch-cert > sealed-secrets-cert.pem
kubeseal --cert sealed-secrets-cert.pem < secret.yaml
8. ExternalSecret status: SecretSyncedError
kubectl describe externalsecret db-credentials -n production
vault kv list secret/production/
vault policy read eso-role
9. SOPS: mac mismatch: file has been modified
sops --decrypt secret.yaml > secret_plain.yaml
sops --encrypt --age age1YOURKEY... secret_plain.yaml > secret.yaml
10. Kubernetes: secret "xxx" not found
kubectl get externalsecret -n production
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets --tail=100
kubectl annotate externalsecret db-credentials \
force-sync=$(date +%s) -n production --overwrite
高度な最適化テクニック
1. Git Pre-commit Hookで平文シークレットのコミットを防止
#!/bin/bash
# .git/hooks/pre-commit
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.yaml$|\.yml$|\.env$')
for FILE in $STAGED_FILES; do
if grep -qE 'kind:\s*Secret' "$FILE" 2>/dev/null; then
if ! grep -qE 'kind:\s*SealedSecret|encryptedData|sops:' "$FILE" 2>/dev/null; then
echo "ERROR: 暗号化されていないSecretファイル: $FILE"
echo "kubesealまたはsopsで暗号化してからコミットしてください"
exit 1
fi
fi
if grep -qiE '(password|secret|api.key|token)\s*[:=]\s*["\x27]?[A-Za-z0-9+/=]{16,}' "$FILE" 2>/dev/null; then
if ! grep -qE 'ENC\[|sops:|encryptedData' "$FILE" 2>/dev/null; then
echo "WARNING: 平文シークレットの疑い: $FILE"
fi
fi
done
exit 0
2. ArgoCDシークレット管理インテグレーション
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-with-secrets
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-manifests.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
3. シークレットテンプレートエンジン
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
namespace: production
spec:
refreshInterval: 15m
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: app-config
template:
type: Opaque
engineVersion: v2
data:
DATABASE_URL: "postgresql://{{ .db_user }}:{{ .db_pass }}@{{ .db_host }}:5432/{{ .db_name }}?sslmode=require"
REDIS_URL: "redis://:{{ .redis_pass }}@{{ .redis_host }}:6379/0"
JWT_SECRET: "{{ .jwt_secret }}"
CONFIG_JSON: |
{
"database": {
"host": "{{ .db_host }}",
"port": 5432,
"name": "{{ .db_name }}"
},
"redis": {
"host": "{{ .redis_host }}"
}
}
data:
- secretKey: db_user
remoteRef:
key: secret/data/production/database
property: username
- secretKey: db_pass
remoteRef:
key: secret/data/production/database
property: password
- secretKey: db_host
remoteRef:
key: secret/data/production/database
property: host
- secretKey: db_name
remoteRef:
key: secret/data/production/database
property: dbname
- secretKey: redis_pass
remoteRef:
key: secret/data/production/redis
property: password
- secretKey: redis_host
remoteRef:
key: secret/data/production/redis
property: host
- secretKey: jwt_secret
remoteRef:
key: secret/data/production/auth
property: jwt_secret
4. シークレットヘルスチェック
apiVersion: batch/v1
kind: CronJob
metadata:
name: secret-health-check
namespace: security
spec:
schedule: "0 8 * * 1"
jobTemplate:
spec:
template:
spec:
serviceAccountName: secret-checker
containers:
- name: checker
image: bitnami/kubectl:1.30
command:
- /bin/bash
- -c
- |
echo "=== シークレットヘルスチェック $(date) ==="
echo "--- 期限切れ間近のTLS証明書を確認 ---"
kubectl get secrets --all-namespaces \
-o json | jq -r '.items[] |
select(.type=="kubernetes.io/tls") |
"\(.metadata.namespace)/\(.metadata.name)"' | \
while read secret; do
ns=$(echo $secret | cut -d/ -f1)
name=$(echo $secret | cut -d/ -f2)
cert=$(kubectl get secret $name -n $ns \
-o jsonpath='{.data.tls\.crt}' | base64 -d)
expiry=$(echo "$cert" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$expiry" ]; then
days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
if [ $days -lt 30 ]; then
echo "WARNING: $secret 証明書は${days}日後に期限切れ"
fi
fi
done
echo "--- ExternalSecret同期ステータスを確認 ---"
kubectl get externalsecrets --all-namespaces \
-o json | jq -r '.items[] |
select(.status.conditions[]?.type=="Ready" and
.status.conditions[]?.status!="True") |
"\(.metadata.namespace)/\(.metadata.name): NOT READY"'
restartPolicy: OnFailure
比較分析:Sealed Secrets vs External Secrets vs SOPS
| 次元 | Sealed Secrets | External Secrets Operator | SOPS |
|---|---|---|---|
| 暗号化方式 | 非対称(クラスタ公開鍵) | Gitコンテンツを暗号化しない | 対称/非対称暗号 |
| Git保存内容 | 暗号文(SealedSecret) | 参照(ExternalSecret) | 暗号文(値のみ暗号化) |
| 外部依存 | なし(自己完結) | Vault/AWS/GCPなど | なし(自己完結) |
| ローテーション | 再暗号化が必要 | ネイティブ対応 | 再暗号化が必要 |
| マルチクラスター | マスターキー同期が必要 | クラスターごとにSecretStore | 暗号化キーを共有 |
| Git Diff対応 | 不可(暗号文は読めない) | 可能(参照は読める) | 可能(キー読める、値暗号化) |
| オフライン復号 | 不可(クラスタ秘密鍵が必要) | 不可(外部サービスが必要) | 可能(ローカルキーで可) |
| 学習曲線 | 低い | 中程度 | 中程度 |
| 運用複雑度 | 低い | 高い(Vaultの保守が必要) | 低い |
| 監査能力 | Git履歴 | Vault監査ログ | Git履歴 |
| 動的シークレット | 非対応 | 対応(Vault動的クレデンシャル) | 非対応 |
| 適用規模 | 小〜中規模チーム | 中〜大規模企業 | 任意の規模 |
| 推奨シナリオ | シンプルなGitOps | エンタープライズシークレット管理 | マルチフォーマット暗号化 |
選択意思決定ツリー
Vault/AWS Secrets Managerを既に利用しているか?
├── はい → External Secrets Operator
│ └── 動的シークレットが必要?→ ESO + Vault動的クレデンシャル
└── いいえ → Gitに暗号化された暗号文を保存する必要があるか?
├── はい → Git Diff対応が必要か?
│ ├── はい → SOPS + Age
│ └── いいえ → Sealed Secrets
└── いいえ → 動的シークレットが必要か?
├── はい → Vault + ESOをデプロイ
└── いいえ → Sealed Secrets
オンラインツール推奨
- Base64エンコード/デコード:/ja/encode/base64 — K8s SecretとSealedSecretデータのエンコード/デコード
- RSA鍵生成:/ja/encode/rsa — SOPS GPG暗号化用のRSA鍵ペア生成
- ハッシュ計算:/ja/encode/hash — シークレットフィンガープリントとチェックサムの計算
まとめ:DevOps GitOpsシークレット管理に銀の弾丸はありませんが、ベストプラクティスのパスは存在します。小規模チームはSealed Secretsから始めましょう——外部依存なしでGitOpsシークレットセキュリティを実現。中〜大規模企業はExternal Secrets Operator + Vaultを選択し、ネイティブのローテーションと動的クレデンシャルを活用。マルチフォーマット暗号化とGit Diff対応が必要な場合は、SOPS + Ageが最適です。2026年のコア原則:ゼロ平文保存、自動ローテーション、監査可能・トレーサブル。覚えておいてください——シークレット漏洩は「起きるかどうか」ではなく「いつ起きるか」の問題です。
関連記事:
- GitOpsとArgoCD本番運用 — ArgoCD完全デプロイ自動化ガイド
- GitOpsとFlux CD本番運用 — Flux CD継続デリバリー実践
- Dockerコンテナセキュリティ強化 — コンテナ8層防御体系
外部リファレンス:
ブラウザローカルツールを無料で試す →