KubernetesでExternal Secrets Operatorをデプロイ:Vaultとクラウドシークレットマネージャーを同期する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

現場の課題:シークレットの散乱とGitOpsのジレンマ

昨年のステージングクラスタでの障害対応訓練中、私たちのチームは深刻な情報漏洩を発見しました。開発者が誤って暗号化されていないKubernetesのSecretマニフェストを公開GitHubミラーにプッシュしてしまっていたのです。そのファイルには、ステージング環境のPostgreSQLデータベースの認証情報が含まれていました。データは一見暗号化されているように見えますが、Base64は単なるエンコード方式にすぎません。暗号化機能は一切なく、シェルが使える人なら誰でも2秒足らずでデコードできてしまいます。

クラスタが成長してマイクロサービスが十数個を超えると、認証情報の管理は急速に煩雑化します。

  • GitOpsとの摩擦:ArgoCDやFluxのようなツールは、すべての状態変更がGitで管理されているときに最大の効果を発揮します。しかし、平文のKubernetes Secretマニフェストをプッシュすると、リポジトリの読み取り権限を持つ全員に機密性の高いパスワードが漏洩してしまいます。
  • シークレットローテーションの苦労:RDSのマスターパスワードやStripeのAPIキーのローテーションに午前中を丸々費やすべきではありません。30個ものネームスペーススコープのSecretを手動でパッチ適用するのは時間の無駄であり、稼働中の接続が切断される原因にもなりがちです。
  • 監査証跡の死角:HashiCorp VaultやAWS Secrets Managerは、すべての読み取りリクエストをログに記録します。しかし、シークレットが一度Kubernetesのetcdに入ってしまうと、どのPodがそれを読み取ったのかを外部の監査ログで追跡できなくなります。

新しいルート認証情報を中央のVaultにプッシュする前に、エントロピーの高いランダム文字列を生成してください。サーバーのパスワードには、ブラウザベースのツールtoolcraft.app/ja/tools/security/password-generatorを活用しています。クライアントサイドで実行されるため、機密トークンがネットワーク上に流出することはありません。強力な認証情報を準備できたら、次の課題はそれらをPodへクリーンに注入することです。

根本原因:Kubernetes標準のSecretが不十分な理由

本番環境の負荷下でこの構成が破綻する理由を理解するために、Kubernetesが内部でシークレットをどのように保存しているかを振り返ってみましょう。

1. Base64には機密性が一切ない

Kubernetesマニフェストは、dataフィールドの下にBase64文字列としてシークレットを保存します。echo "<string>" | base64 -dを実行すれば、本番環境のトークンが端末画面に平文で表示されます。ローカルデバッグ中に未加工のトークンを確認する必要がある場合は、ToolCraftのBase64 Encoder/Decoderを使用してください。ただし、デコードされたペイロードが誤ってバージョン管理にコミットされないよう細心の注意を払いましょう。

2. ライフサイクルの断絶

HashiCorp Vault、AWS Secrets Manager、GCP Secret Managerなどの外部マネージャーは、自動ローテーション、リース、厳格なIAM境界を処理します。しかし、Kubernetesはそれらの変更を感知しません。外部データベースのパスワードが深夜2時にローテーションされても、CIパイプラインやエンジニアが更新をプッシュするまで、etcdは古い値を配信し続けます。

3. クラスタ全体への広範な読み取り権限

エンジニアは本番以外のネームスペースに対して読み取り専用権限を持っていることがよくあります。クラスタが専用のKMSエンベロープ暗号化や厳格なRBACガードで保護されていない限り、kubectl get secret -o yamlを実行するだけで、そのネームスペース内のあらゆる認証情報が即座に取得できてしまいます。

選択肢の比較検討

過去4年間にわたり、私たちのインフラチームは外部VaultとKubernetes Podを連携させるための代表的な4つのパターンを評価してきました。

アプローチ 仕組み メリット デメリット
Sealed Secrets (Bitnami) クラスタ内コントローラーの鍵による非対称暗号化。 GitOpsに対応。暗号化されたマニフェストをパブリックリポジトリに安全に配置可能。 エンタープライズVaultとの統合なし。パスワードローテーションのたびに手動で再暗号化が必要。
Vault Agent Sidecar Mutating Webhookが各Podにサイドカーコンテナを注入し、共有メモリボリュームにシークレットを展開。 動的リースに直接対応。ネイティブなKubernetes Secretを一切生成しない。 Podあたり40〜80MBのRAMオーバーヘッドが発生。アプリは標準の環境変数ではなくファイルパスから読み取る必要がある。
Secrets Store CSI Driver 外部プロバイダーのシークレットをローカルコンテナボリュームとして直接マウント。 Kubernetes公式SIGプロジェクト。幅広いクラウドプロバイダーをサポート。 ネイティブなSecretオブジェクトへの同期には追加のCRDやフラグが必要。同期をトリガーするにはPodがボリュームをマウントする必要がある。
External Secrets Operator (ESO) 単一のコントローラーが外部APIを標準のKubernetes Secretオブジェクトへ同期。 Vault、AWS、GCP、Azureに対応。標準の環境変数と連携可能。標準でドリフト検出を処理。 ネームスペース内に標準のKubernetes Secretオブジェクトが残る。

ステップ・バイ・ステップ:External Secrets Operator(ESO)のデプロイ

GitOpsパイプラインを運用する多くのエンジニアリングチームにとって、External Secrets Operator(ESO)が最もバランスの取れた選択肢です。アプリケーションは従来どおり標準の環境変数を読み取りつつ、HashiCorp VaultやCloud Secret Managerを単一の信頼できる情報源(Single Source of Truth)として維持できます。

ステップ1:HelmでESOをインストールする

まず、公式Helmチャートを取得し、専用のネームスペースにオペレーターをインストールします:

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets \
  external-secrets/external-secrets \
  -n external-secrets \
  --create-namespace \
  --set installCRDs=true

コントローラー、Webhook、cert-controllerの3つのコアPodがすべて正常に稼働していることを確認します:

kubectl get pods -n external-secrets

ステップ2:アップストリームストアへの認証を設定する

ESOはアップストリーム接続用に2つのカスタムリソースを提供しています。単一ネームスペース用のSecretStoreと、クラスタ全体用のバックエンドであるClusterSecretStoreです。以下は、専用のKubernetes ServiceAccountを使用してHashiCorp Vaultに接続する、本番環境で検証済みの設定例です:

# Vault認証専用のサービスアカウントを作成
kubectl create serviceaccount vault-auth-sa -n production

このマニフェストをvault-secretstore.yamlとして保存します:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal.mycompany.com:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-app-role"
          serviceAccountRef:
            name: vault-auth-sa

ファイルを適用します:

kubectl apply -f vault-secretstore.yaml

AWS EKSで運用している場合は、IAM Roles for Service Accounts(IRSA)を使用してESOをAWS Secrets Managerに向けます:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-backend
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: aws-irsa-sa

ステップ3:ExternalSecretカスタムリソースを定義する

認証を確立したら、外部キーとネイティブなKubernetes Secretの間のマッピングを定義します。外部シークレットにネストされたJSONが含まれている場合は、事前にToolCraftのJSON Formatter & Validatorを使用してペイロード構造を検証してください。Vault内の括弧が1箇所崩れているだけでも、ネームスペース全体のオペレーターリコンサイルがブロックされる可能性があります。

app-externalsecret.yamlを作成します:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: payment-service-secret-sync
  namespace: production
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: payment-service-db-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: production/payment-service
        property: password
    - secretKey: DB_USER
      remoteRef:
        key: production/payment-service
        property: username

マニフェストを適用します:

kubectl apply -f app-externalsecret.yaml

ステップ4:同期ステータスを確認する

カスタムリソースのステータスフラグを確認します:

kubectl get externalsecret payment-service-secret-sync -n production

STATUSカラムにSecretSyncedが表示されていることを確認します:

NAME                          STORE           REFRESH INTERVAL   STATUS         READY
payment-service-secret-sync   vault-backend   1h                 SecretSynced   True

生成されたKubernetes Secretを確認します:

kubectl get secret payment-service-db-credentials -n production -o yaml

ステップ5:DeploymentにSecretをバインドする

アプリケーションPodには、独自のクライアントSDKやサイドカーは一切不要です。標準の環境変数を介して同期されたキーを注入します:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: app
          image: payment-service:v1.2.0
          env:
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: payment-service-db-credentials
                  key: DB_PASSWORD
            - name: DATABASE_USER
              valueFrom:
                secretKeyRef:
                  name: payment-service-db-credentials
                  key: DB_USER

本番環境運用のベストプラクティス

マルチテナントの本番クラスタでESOを運用して得られた貴重な知見をいくつか紹介します:

  • ポーリングコストに注意する:refreshIntervalを5sのような過密な間隔に設定するのは避けてください。AWS Secrets Managerは10,000回のAPIリクエストごとに0.05ドル課金されます。50個のシークレットを5秒ごとにポーリングすると、月間で2,500万回以上のリクエストが発生し、130ドルもの想定外のAPIコストがかかります。静的なアプリケーション認証情報であれば、1hから4hの間隔で十分に機能します。
  • Podの再起動を自動化する:基盤となるSecretが更新されても、KubernetesはPodを自動的に再起動しません。ESOと併せてStakater Reloaderを導入してください。Deploymentにreloader.stakater.com/auto: "true"アノテーションを追加することで、ESOがシークレットを更新した瞬間に安全なローリング再起動がトリガーされます。
  • Gitをクリーンに保つ:GitにコミットするのはExternalSecretとSecretStoreの宣言のみに限定してください。機密性の高い認証情報は中央のキーVault内に隔離されたまま安全に保護されます。
Share: