シークレットローテーション:システムを壊さずに実践する理由、タイミング、方法

シークレットを安全にボールトに保管し、パイプラインがデプロイ時にそれらを注入している。一見、盤石に見える。しかし、見落としがちな問題がある。それは、同じシークレットを何ヶ月も、何年も使い続けることが、時限爆弾になり得るということだ。

開発者が会社のプレゼンテーション中に、スクリーンショットに誤ってAPIキーを含めてしまうケースを想像してほしい。あるいは、6ヶ月前のログファイルにデータベースパスワードがまだ残っていて、不正アクセス権を持つ誰かに見つかるケースだ。シークレットの寿命が長ければ長いほど、知らぬ間に漏洩するリスクは高まる。ローテーションはセーフティネットとなる。もしシークレットが漏洩しても、漏洩が悪用される前に、その有効期限がすでに切れているようにできるのだ。

なぜシークレットをローテーションするのか

ローテーションの最も基本的な理由は、脆弱性の露出期間(ウィンドウ・オブ・ヴァルネラビリティ)を短縮することだ。APIキーの有効期間が1年で、3ヶ月目に漏洩した場合、攻撃者は9ヶ月間アクセスできる。しかし、そのキーを毎週ローテーションしていれば、最大の被害期間は7日間で済む。

ローテーションはコンプライアンスの観点からも重要だ。PCI DSSやSOC 2などの標準規格では、定期的なシークレットローテーションが求められる。しかし、規制上のプレッシャーがなくても、ローテーションは実用的な防御メカニズムとなる。ブラスト半径(被害範囲)を制限し、シークレット管理パイプラインが実際に機能することを確認させ、インシデント発生時ではなく、平常時にクレデンシャル変更を扱うための運用上の筋肉記憶を構築する。

いつローテーションすべきか

ローテーションをトリガーする主なシナリオは3つある。

スケジュールローテーション。 これは定期的で予測可能なサイクルだ。シークレットの機密性に応じて、30日、60日、または90日ごとに実施する。本番システムのデータベースパスワードは30日ごとにローテーションするかもしれない。重要度の低い内部APIキーは90日ごとでよい。スケジュールは、シークレットが保護するもののリスクプロファイルに一致させるべきだ。

インシデント駆動型ローテーション。 何かが発生した場合だ。監査ログで不審なアクセスを発見した、公開GitHubリポジトリにシークレットが表示された、従業員のラップトップが侵害された、といったケースだ。このような場合、即座にローテーションする。次のスケジュールされたタイミングを待ってはいけない。手順よりもスピードが重要だ。

人事異動。 シークレットにアクセス権を持っていた人物がチームを離れる、役割が変わる、または解雇される場合、その人物がアクセスできた可能性のあるすべてのシークレットをローテーションする。これは信頼の問題ではない。彼らが記憶したり、ローカルに保存したり、どこかに書き留めたりしたクレデンシャルが、アクセス権が失効した後も有効であり続ける可能性を排除するためだ。

アプリケーションを壊さずにローテーションする方法

ここが難しいところだ。データベースパスワードをローテーションした結果、すべてのサービスが突然接続を失ったら、事態は悪化する。目標は、ダウンタイムなしでローテーションすることだ。最も信頼性の高い戦略は、デュアルシークレットローテーション、つまり移行期間を伴うローテーションである。

デュアルシークレットローテーション

アイデアはシンプルだ。移行期間中、アプリケーションは2つの有効なシークレットを同時に受け入れる。以下がステップバイステップの手順だ。

以下のシーケンス図は、ボールト、設定サービス、および複数のアプリケーションインスタンスにわたるデュアルシークレットローテーションプロセスを示しています。

以下は、HashiCorp VaultとJSON設定ファイルを使用してこれを実装する方法の例です。

# Step 1: 新しいシークレットバージョンを生成(古いものは保持)
vault kv put secret/db-password \
  old_password="$(vault kv get -field=password secret/db-password)" \
  new_password="$(openssl rand -base64 32)"

# Step 2: 両方のシークレットを含むアプリケーション設定を更新
cat > /etc/myapp/config.json <<EOF
{
  "db": {
    "old_password": "$(vault kv get -field=old_password secret/db-password)",
    "new_password": "$(vault kv get -field=new_password secret/db-password)"
  }
}
EOF

# Step 3: 新しい設定を反映するためにアプリケーションをリロード
systemctl reload myapp

# Step 4: すべてのインスタンスが新しいシークレットを使用するようになったら、古いものを削除
vault kv patch secret/db-password old_password=""

# Step 5: 新しいシークレットのみを使用するように設定を更新
cat > /etc/myapp/config.json <<EOF
{
  "db": {
    "password": "$(vault kv get -field=new_password secret/db-password)"
  }
}
EOF
systemctl reload myapp
sequenceDiagram participant Vault participant Config participant ServiceA participant ServiceB Note over Vault,ServiceB: Step 1: 新しいシークレットを生成 Vault->>Vault: 新しいシークレットを生成(古いものは保持) Note over Vault,ServiceB: Step 2: 新しいシークレットを全サービスにデプロイ Vault->>Config: 古いシークレットと新しいシークレットを提供 Config->>ServiceA: 設定を更新(両方のシークレットが有効) Config->>ServiceB: 設定を更新(両方のシークレットが有効) Note over Vault,ServiceB: Step 3: 全サービスが新しいシークレットを使用していることを確認 ServiceA->>Vault: 新しいシークレットで接続 ServiceB->>Vault: 新しいシークレットで接続 Note over Vault,ServiceB: Step 4: 古いシークレットを無効化 Vault->>Config: 古いシークレットを無効としてマーク Config->>ServiceA: 設定から古いシークレットを削除 Config->>ServiceB: 設定から古いシークレットを削除 Note over Vault,ServiceB: Step 5: ボールトから古いシークレットを削除 Vault->>Vault: 古いシークレットを削除
  1. ボールトまたはシークレットストアで新しいシークレットを生成する。古いものは削除しない。
  2. 古いシークレットと新しいシークレットの両方が有効であることをアプリケーション設定に認識させるため、設定を更新する。
  3. 更新された設定をデプロイする。実行中のすべてのインスタンスが両方のシークレットを受け入れるようになる。
  4. すべてのインスタンスが新しい設定を取得し、新しいシークレットを使用するまで待機する。
  5. 設定から古いシークレットを削除し、再度デプロイする。

このプロセス中、アプリケーションの接続が失われることは決してない。新しい設定を受け取っていないインスタンスは、古いシークレットで引き続き動作する。更新を受け取ったインスタンスは、どちらかのシークレットを使用できる。古いシークレットが削除された後は、新しいシークレットのみが機能する。

このアプローチは、アプリケーションによって直接消費されるシークレット(データベースパスワード、APIキー、サービス間トークンなど)に適している。重要な要件は、アプリケーションコードまたはミドルウェアが複数の有効なクレデンシャルを同時にサポートしていることだ。最近のデータベースドライバやHTTPクライアントのほとんどはこれを処理できる。

複数サービスにわたるローテーションの調整

単一のシークレットが多くのサービスで共有されている場合、ローテーションはより複雑になる。10のマイクロサービスで使用されているデータベースパスワードを想像してほしい。調整なしに、サービスごとにローテーションすることはできない。サービスAが新しいパスワードに切り替えたが、サービスBがまだ古いパスワードを使用しており、データベースが新しいパスワードのみを受け入れる場合、サービスBは停止する。

1つの解決策は、データベース接続を集中的に管理するサービスメッシュまたはサイドカープロキシを使用することだ。サイドカーがデータベースへの認証を処理する。サービスはデータベースに直接接続するのではなく、サイドカーに接続する。データベースパスワードをローテーションするときは、サイドカーの設定のみを更新すればよい。サービスはローテーションが発生したことすら知らない。

別のアプローチは、動的シークレットシステムを使用することだ。これについては後述する。しかし、多くのコンシューマー間で共有される静的シークレットの場合、サービスメッシュまたは専用のコネクションプーラーが最も実用的なパターンである。

ローテーションで他に重要なこと

ローテーションは単に値を変更することだけではない。それを支えるプラクティスが必要なプロセスである。

監査ログ。 すべてのローテーションを記録すべきだ。誰がトリガーしたか、いつ、どのシークレットがローテーションされたか、結果はどうだったか。これはインシデント調査とコンプライアンス監査に不可欠である。

ステージング環境で事前テスト。 本番シークレットをローテーションする前に、ステージング環境でプロセスを検証することなしに行ってはならない。ステージング環境は、本番環境のシークレット消費パターンをミラーリングすべきだ。ローテーションで何かが壊れるなら、本番ではなくステージングで壊れるようにしたい。

ロールバック計画を用意する。 ローテーションが予期せぬ問題を引き起こすことがある。新しいシークレットが正しく伝播しない、サービスが設定変更を取得できない、といったケースだ。ローテーション手順には、古いシークレットに迅速に戻す方法を含めるべきだ。これは、新しいシークレットがどこでも機能する確信が得られるまで、古いシークレットを有効にしておくことを意味する。

シークレットローテーションの実践的チェックリスト

  • ローテーションが必要なシークレットとそのリスクレベルを特定する
  • リスクに基づいてローテーションスケジュールを定義する(30/60/90日)
  • アプリケーションコードまたはミドルウェアにデュアルシークレットサポートを実装する
  • ステージング環境でローテーション手順をテストする
  • 本番シークレットをローテーションする前にロールバック手順を文書化する
  • すべてのローテーションをタイムスタンプ、オペレーター、結果とともに記録する
  • 可能な場合はスケジュールローテーションを自動化する
  • 緊急ローテーションのためのインシデント対応プロセスを確立する

まとめ

変更されないシークレットは、悪用されるのを待っている脆弱性である。ローテーションは被害期間を制限し、コンプライアンス要件を満たし、シークレット管理のプラクティスを常に最新に保つことを強制する。デュアルシークレットパターンは、ダウンタイムなしで安全にローテーションする方法を提供する。しかし、ローテーションは全体像の一部に過ぎない。次の課題は、静的シークレットから完全に脱却し、使用後に自動的に期限切れとなるシークレットに移行できるかどうかである。ここで動的シークレットが登場する。