メインコンテンツまでスキップ

鍵管理と実践

このドキュメントの目的

鍵管理(Key Management) のベストプラクティスを学び、実際のシステムでの暗号化の適用方法を理解します。


鍵管理の重要性

暗号の強度 = アルゴリズムの強度 × 鍵管理の強度

最強の暗号アルゴリズムを使っても、鍵管理が甘ければ意味がない

┌─────────────────────────────────────────────────┐
│ 鍵管理の失敗例 │
├─────────────────────────────────────────────────┤
│ ・秘密鍵がGitHubに公開された │
│ ・鍵がログに出力されていた │
│ ・同じ鍵を全環境で使い回し │
│ ・退職者がアクセスできる場所に鍵があった │
│ ・鍵のバックアップがなく、紛失した │
└─────────────────────────────────────────────────┘

鍵のライフサイクル

鍵のライフサイクル管理:

┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 生成 │ → │ 配布 │ → │ 使用 │ → │ ローテーション│ → │ 破棄 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘

各フェーズでの考慮事項:

1. 生成(Generation)
- 暗号学的に安全な乱数生成器を使用
- 適切な鍵長を選択
- 生成環境のセキュリティ

2. 配布(Distribution)
- 安全なチャネルで配布
- 必要最小限の範囲に限定
- アクセス制御

3. 使用(Usage)
- 用途を限定(署名用、暗号化用を分離)
- 使用履歴の監査
- 使用期限の設定

4. ローテーション(Rotation)
- 定期的な鍵の更新
- 移行期間の確保
- 古い鍵で暗号化されたデータの再暗号化

5. 破棄(Destruction)
- 安全な削除(メモリ/ディスクからの完全消去)
- バックアップからも削除
- 破棄の記録

鍵の保存場所

環境変数

⚠️ 環境変数(開発/テスト向け)

メリット:
- 設定が簡単
- コードから分離

デメリット:
- プロセスダンプで漏洩リスク
- ログに出力されるリスク
- 共有環境では危険

使用例:
export JWT_SIGNING_KEY="..."

# アプリケーションから読み込み
String key = System.getenv("JWT_SIGNING_KEY");

⚠️ 本番環境では推奨しない

シークレット管理サービス

⭕ シークレット管理サービス(本番向け)

代表的なサービス:
┌─────────────────────────────────────────────────┐
│ HashiCorp Vault │
│ - 業界標準のシークレット管理 │
│ - 動的シークレット生成 │
│ - 監査ログ │
├─────────────────────────────────────────────────┤
│ AWS Secrets Manager │
│ - AWSネイティブ │
│ - 自動ローテーション │
│ - IAMとの統合 │
├─────────────────────────────────────────────────┤
│ Azure Key Vault │
│ - Azureネイティブ │
│ - HSMバックド │
│ - Azure ADとの統合 │
├─────────────────────────────────────────────────┤
│ Google Cloud Secret Manager │
│ - GCPネイティブ │
│ - IAMとの統合 │
│ - バージョン管理 │
└─────────────────────────────────────────────────┘

HSM(Hardware Security Module)

⭕ HSM(最高セキュリティ向け)

HSMとは:
- 鍵を物理的なハードウェア内で管理
- 鍵がHSMの外に出ることがない
- 暗号処理もHSM内で実行
- 耐タンパー性(物理的な攻撃に耐性)

クラウドHSM:
- AWS CloudHSM
- Azure Dedicated HSM
- Google Cloud HSM

使用場面:
- 認証局(CA)の秘密鍵
- 銀行・金融システム
- 政府・軍事システム
- コード署名鍵

鍵のローテーション

なぜローテーションが必要か

鍵ローテーションの理由:

1. 鍵の露出リスク軽減
- 長く使うほど漏洩リスクが増加
- 漏洩しても被害を限定

2. 暗号の陳腐化対策
- 計算能力の向上
- 新しい攻撃手法の発見

3. コンプライアンス要件
- PCI DSS: 少なくとも年1回
- NIST: 用途に応じた期限

推奨ローテーション期間:
┌─────────────────────────────────────────────────┐
│ 鍵の種類 │ 推奨期間 │
├─────────────────────────────────────────────────┤
│ JWT署名鍵 │ 90日〜1年 │
│ データ暗号化鍵 │ 1〜2年 │
│ TLS証明書 │ 1年(Let's Encryptは90日) │
│ マスター鍵 │ 2〜3年 │
│ ルートCA鍵 │ 10〜20年 │
└─────────────────────────────────────────────────┘

JWKSでの鍵ローテーション

{
"keys": [
{
"kid": "key-2024-12",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
},
{
"kid": "key-2024-09",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}

ローテーション手順:
┌─────────────────────────────────────────────────┐
│ Day 0: 新しい鍵を生成 │
│ JWKSに新しい公開鍵を追加 │
│ (まだ署名には使わない) │
├─────────────────────────────────────────────────┤
│ Day 1-7: 新しい鍵で署名を開始 │
│ 古い鍵でも検証可能(移行期間) │
├─────────────────────────────────────────────────┤
│ Day 30+: 古いトークンが全て期限切れ │
│ 古い公開鍵をJWKSから削除 │
│ 古い秘密鍵を安全に破棄 │
└─────────────────────────────────────────────────┘

実装例(Java)

import java.security.*;
import java.util.*;
import java.time.*;

public class KeyRotationManager {

private final Map<String, KeyPair> activeKeys = new ConcurrentHashMap<>();
private String currentKeyId;

// 新しい鍵を生成してアクティブにする
public void rotateKey() throws Exception {
String newKeyId = "key-" + LocalDate.now().toString();

KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(4096);
KeyPair newKeyPair = generator.generateKeyPair();

activeKeys.put(newKeyId, newKeyPair);
currentKeyId = newKeyId;

// 古い鍵を一定期間後に削除するスケジュール
scheduleKeyRemoval(newKeyId);
}

// 署名用の秘密鍵を取得(最新の鍵)
public PrivateKey getSigningKey() {
return activeKeys.get(currentKeyId).getPrivate();
}

public String getCurrentKeyId() {
return currentKeyId;
}

// 検証用の公開鍵を取得(kidで指定)
public PublicKey getVerificationKey(String keyId) {
KeyPair keyPair = activeKeys.get(keyId);
if (keyPair == null) {
throw new IllegalArgumentException("Unknown key ID: " + keyId);
}
return keyPair.getPublic();
}

// JWKSを生成
public String generateJwks() {
// 全てのアクティブな公開鍵をJWKS形式で出力
// 実装は省略
return "{ \"keys\": [...] }";
}

private void scheduleKeyRemoval(String keyId) {
// 30日後に古い鍵を削除
// 実装は省略
}
}

暗号化の実践パターン

エンベロープ暗号化

エンベロープ暗号化: 鍵で鍵を暗号化

┌─────────────────────────────────────────────────┐
│ エンベロープ暗号化 │
├─────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ マスター鍵 │ ← 極めて安全に保管(HSM等) │
│ └──────┬──────┘ │
│ │ 暗号化 │
│ ↓ │
│ ┌─────────────┐ │
│ │ データ鍵 │ ← マスター鍵で暗号化して保存 │
│ └──────┬──────┘ │
│ │ 暗号化 │
│ ↓ │
│ ┌─────────────┐ │
│ │ データ │ ← データ鍵で暗号化 │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────┘

メリット:
- マスター鍵のローテーションが容易
- データの再暗号化が不要
- マスター鍵の露出リスクを最小化

AWS KMSでのエンベロープ暗号化

import com.amazonaws.services.kms.AWSKMS;
import com.amazonaws.services.kms.model.*;

public class AwsKmsExample {

private final AWSKMS kmsClient;
private final String masterKeyId;

// データ鍵を生成
public GenerateDataKeyResult generateDataKey() {
GenerateDataKeyRequest request = new GenerateDataKeyRequest()
.withKeyId(masterKeyId)
.withKeySpec("AES_256");

return kmsClient.generateDataKey(request);
// 返り値:
// - plaintext: 平文のデータ鍵(メモリ内で使用)
// - ciphertextBlob: 暗号化されたデータ鍵(保存用)
}

// 暗号化されたデータ鍵を復号
public DecryptResult decryptDataKey(byte[] encryptedDataKey) {
DecryptRequest request = new DecryptRequest()
.withCiphertextBlob(ByteBuffer.wrap(encryptedDataKey))
.withKeyId(masterKeyId);

return kmsClient.decrypt(request);
}
}

トークンベースの暗号化

トークナイゼーション: 機密データを無意味なトークンに置換

元データ: トークン:
4111-1111-1111-1111 → tok_1234567890abcdef

┌─────────────────────────────────────────────────┐
│ トークナイゼーション │
├─────────────────────────────────────────────────┤
│ │
│ [アプリケーション] │
│ │ │
│ │ クレジットカード番号を送信 │
│ ↓ │
│ [トークンサービス] │
│ │ │
│ │ 暗号化して保存 │
│ │ トークンを返却 │
│ ↓ │
│ [アプリケーション] │
│ │ │
│ │ トークンを保存(安全) │
│ │
└─────────────────────────────────────────────────┘

メリット:
- 元データを保持しないため、漏洩リスクが低い
- PCI DSSのスコープを縮小
- 形式を維持できる(フォーマット保持トークン)

セキュリティベストプラクティス

鍵の生成

// ⭕ 良い例: 暗号学的に安全な乱数生成器
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[32]; // 256ビット
secureRandom.nextBytes(key);

// ❌ 悪い例: 予測可能な乱数生成器
Random random = new Random(); // 使用禁止!
random.nextBytes(key);

// ❌ 悪い例: 弱いシード
Random random = new Random(System.currentTimeMillis()); // 使用禁止!

鍵の保存

⭕ やるべきこと:

1. 秘密鍵は暗号化して保存
- パスフレーズで保護されたPEM
- KMSで暗号化

2. アクセス制御を設定
- ファイル権限: 600(所有者のみ読み書き)
- IAMポリシーで最小権限

3. バックアップを取る
- 別の場所に暗号化して保管
- リカバリ手順をテスト

❌ やってはいけないこと:

1. コードにハードコード
private static final String KEY = "secret123"; // ❌

2. バージョン管理にコミット
.gitignore に追加: *.pem, *.key, secrets/

3. ログに出力
logger.info("Key: " + key); // ❌

4. 環境変数をデバッグ出力
System.getenv().forEach(...); // ❌

メモリ内の鍵

// ⭕ 良い例: 使用後にメモリをクリア
byte[] key = getKey();
try {
// 鍵を使用
encrypt(data, key);
} finally {
// メモリをゼロクリア
Arrays.fill(key, (byte) 0);
}

// char[]を使用する場合
char[] password = getPassword();
try {
// パスワードを使用
} finally {
Arrays.fill(password, '\0');
}

// Stringは使用後もメモリに残る可能性がある
// ⚠️ パスワードにはString型を避ける

監査とコンプライアンス

監査ログ

鍵操作の監査ログに含めるべき情報:

┌─────────────────────────────────────────────────┐
│ タイムスタンプ: 2024-12-22T10:30:00Z │
│ 操作: KEY_ROTATION │
│ 鍵ID: key-2024-12 │
│ 実行者: admin@example.com │
│ ソースIP: 192.168.1.100 │
│ 結果: SUCCESS │
└─────────────────────────────────────────────────┘

ログに含めてはいけない情報:
- 鍵の値
- パスワード
- 復号されたデータ

コンプライアンス要件

主要なコンプライアンス要件:

┌─────────────────────────────────────────────────┐
│ PCI DSS(クレジットカード) │
├─────────────────────────────────────────────────┤
│ - 暗号化鍵は最小限のカストディアンのみがアクセス │
│ - 鍵の暗号化には別の鍵を使用(二重暗号化) │
│ - 少なくとも年1回の鍵ローテーション │
│ - 分割知識/二重管理 │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ HIPAA(医療情報) │
├─────────────────────────────────────────────────┤
│ - PHIの暗号化(推奨) │
│ - アクセス制御 │
│ - 監査証跡 │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ GDPR(個人データ) │
├─────────────────────────────────────────────────┤
│ - 適切な技術的措置(暗号化を含む) │
│ - 仮名化・匿名化 │
│ - データ侵害時の72時間以内通知 │
└─────────────────────────────────────────────────┘

アイデンティティ管理での実践

JWT署名鍵の管理

JWT署名鍵の管理ベストプラクティス:

1. 非対称鍵を使用(RS256, ES256, EdDSA)
- 秘密鍵: 認可サーバーのみ保持
- 公開鍵: JWKSで公開

2. 鍵のローテーション
- 90日ごとにローテーション
- JWKSには複数の鍵を公開
- kidで鍵を識別

3. 安全な保存
- 秘密鍵はHSMまたはシークレット管理サービス
- 起動時にのみ読み込み
- メモリ内で保持

4. バックアップ
- 秘密鍵の暗号化バックアップ
- 災害復旧計画に含める

セッション鍵の管理

セッション暗号化鍵:

┌─────────────────────────────────────────────────┐
│ セッションデータの暗号化 │
├─────────────────────────────────────────────────┤
│ │
│ 1. 暗号化鍵の生成 │
│ - アプリケーション起動時に生成 │
│ - または環境変数/シークレット管理から取得 │
│ │
│ 2. セッションデータの暗号化 │
│ - AES-GCMで暗号化 │
│ - IVはセッションごとに生成 │
│ │
│ 3. Cookieに保存 │
│ - 暗号化されたデータ + IV │
│ - HttpOnly, Secure, SameSite属性を設定 │
│ │
└─────────────────────────────────────────────────┘

データベース暗号化

保存データの暗号化レベル:

レベル1: ディスク暗号化(透過的)
- OSレベルでディスク全体を暗号化
- アプリケーションからは透過的
- 例: LUKS, BitLocker, AWS EBS暗号化

レベル2: データベース暗号化(TDE)
- データベースレベルで暗号化
- 例: PostgreSQL TDE, MySQL TDE

レベル3: カラムレベル暗号化
- 特定のカラムのみ暗号化
- アプリケーションで暗号化/復号
- 例: 個人情報、クレジットカード番号

レベル4: アプリケーションレベル暗号化
- アプリケーションで暗号化してから保存
- 最も柔軟だが実装コストが高い
- 例: エンドツーエンド暗号化

チェックリスト

開発時チェックリスト

□ 暗号学的に安全な乱数生成器を使用している
□ 秘密鍵はコードにハードコードしていない
□ 秘密鍵は.gitignoreに追加されている
□ ログに秘密情報を出力していない
□ 使用後の鍵・パスワードはメモリからクリアしている
□ 適切な鍵長を選択している(RSA 4096, AES 256等)
□ 適切なアルゴリズムを選択している(AES-GCM, Ed25519等)
□ IVは毎回新しく生成している

本番環境チェックリスト

□ シークレット管理サービスを使用している
□ 鍵へのアクセスは最小権限の原則
□ 鍵操作の監査ログが有効
□ 鍵のローテーション計画がある
□ 鍵のバックアップがある
□ 災害復旧手順がテストされている
□ コンプライアンス要件を満たしている

まとめ

鍵管理のポイント:

  1. ライフサイクル管理: 生成→配布→使用→ローテーション→破棄
  2. 安全な保存: HSM > シークレット管理サービス > 環境変数
  3. 定期的なローテーション: 鍵の露出リスクを軽減
  4. エンベロープ暗号化: 鍵で鍵を暗号化
  5. 監査とコンプライアンス: ログ記録と要件準拠

暗号化は正しく実装してこそ意味がある:

  • アルゴリズムより鍵管理が重要
  • セキュリティは最も弱いリンクで決まる
  • 定期的な見直しと改善が必要

参考資料

リソース説明
NIST SP 800-57鍵管理のガイドライン
OWASP Cryptographic Failures暗号化の失敗パターン
RFC 7517JSON Web Key (JWK)
RFC 7518JSON Web Algorithms (JWA)
AWS KMS Best PracticesAWS鍵管理のベストプラクティス