DevOps GitOpsシークレット管理:Sealed SecretsからExternal Secretsまでの6つの本番パターン

DevOps

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年のコア原則:ゼロ平文保存、自動ローテーション、監査可能・トレーサブル。覚えておいてください——シークレット漏洩は「起きるかどうか」ではなく「いつ起きるか」の問題です。


関連記事

外部リファレンス

ブラウザローカルツールを無料で試す →

#DevOps#GitOps#密钥管理#Sealed Secrets#External Secrets#SOPS#2026#DevOps