🎄 科学と神々株式会社 アドベントカレンダー 2025
License System Day 17: 鍵管理とセキュリティ
📖 今日のテーマ
今日は、暗号鍵の安全な管理を学びます。
ECDSA P-256秘密鍵・公開鍵の生成、保存、ローテーション、そしてセキュリティベストプラクティスを解説します。
🎯 鍵管理の重要性
鍵漏洩のリスク
秘密鍵が漏洩すると...
❌ すべてのライセンスが偽造可能に
❌ ユーザーデータが不正アクセスされる
❌ サービス全体の信頼性が崩壊
❌ 法的責任問題に発展
→ 鍵管理は最重要のセキュリティ課題!
セキュリティ原則
✅ 最小権限の原則 (Principle of Least Privilege)
✅ 深層防御 (Defense in Depth)
✅ 秘密の分離 (Separation of Secrets)
✅ 定期ローテーション (Regular Rotation)
✅ 監査ログ (Audit Logging)
🔐 ECDSA P-256鍵ペア生成
OpenSSLによる生成
#!/bin/bash
# generate_keys.sh
# 鍵ディレクトリ作成
mkdir -p keys
chmod 700 keys # 所有者のみアクセス可能
# ECDSA P-256秘密鍵生成
openssl ecparam -name prime256v1 -genkey -noout -out keys/private_key.pem
# 秘密鍵のパーミッション設定(所有者のみ読み取り可能)
chmod 400 keys/private_key.pem
# 公開鍵の抽出
openssl ec -in keys/private_key.pem -pubout -out keys/public_key.pem
# 公開鍵は読み取り可能でOK
chmod 644 keys/public_key.pem
echo "✅ ECDSA P-256 key pair generated successfully"
echo " Private key: keys/private_key.pem (chmod 400)"
echo " Public key: keys/public_key.pem (chmod 644)"
鍵の検証
# 秘密鍵の検証
openssl ec -in keys/private_key.pem -text -noout
# 出力例:
# read EC key
# Private-Key: (256 bit)
# priv:
# 00:c9:0e:f7:3f:4c:7e:...
# pub:
# 04:b7:63:5c:0f:...
# ASN1 OID: prime256v1
# NIST CURVE: P-256
# 公開鍵の検証
openssl ec -in keys/public_key.pem -pubin -text -noout
# 出力例:
# read EC key
# Public-Key: (256 bit)
# pub:
# 04:b7:63:5c:0f:...
# ASN1 OID: prime256v1
# NIST CURVE: P-256
🗃️ 鍵の保存戦略
開発環境(ローカル)
# .gitignore に追加(必須!)
keys/
*.pem
*.key
.env
.env.local
# 環境変数ファイル(.env)
PRIVATE_KEY_PATH=./keys/private_key.pem
PUBLIC_KEY_PATH=./keys/public_key.pem
JWT_SECRET=your-super-secret-jwt-key-change-me
Nimでの読み込み:
# server/src/crypto.nim
import std/os
proc loadPrivateKey*(): string =
let keyPath = getEnv("PRIVATE_KEY_PATH", "../keys/private_key.pem")
if not fileExists(keyPath):
raise newException(IOError, "Private key not found: " & keyPath)
result = readFile(keyPath)
proc loadPublicKey*(): string =
let keyPath = getEnv("PUBLIC_KEY_PATH", "../keys/public_key.pem")
if not fileExists(keyPath):
raise newException(IOError, "Public key not found: " & keyPath)
result = readFile(keyPath)
本番環境(プロダクション)
1. 環境変数での管理
# サーバー起動時に設定
export PRIVATE_KEY="-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIAoN9z9MfrxvCqRK...
-----END EC PRIVATE KEY-----"
export PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZI...
-----END PUBLIC KEY-----"
export JWT_SECRET="production-super-secret-key-256-bits"
Nimでの読み込み:
proc loadPrivateKeyFromEnv*(): string =
result = getEnv("PRIVATE_KEY")
if result == "":
raise newException(ValueError, "PRIVATE_KEY environment variable not set")
proc loadPublicKeyFromEnv*(): string =
result = getEnv("PUBLIC_KEY")
if result == "":
raise newException(ValueError, "PUBLIC_KEY environment variable not set")
2. AWS Secrets Manager
# Nim + AWS SDK
import std/[asyncdispatch, json]
import aws_sdk/secretsmanager
proc loadPrivateKeyFromAWS*(secretName: string): Future[string] {.async.} =
let client = newSecretsManagerClient(region = "us-east-1")
try:
let response = await client.getSecretValue(secretName)
let secret = parseJson(response.secretString)
result = secret["private_key"].getStr()
except Exception as e:
raise newException(IOError, "Failed to load private key from AWS: " & e.msg)
3. HashiCorp Vault
import std/[httpclient, json, asyncdispatch]
proc loadPrivateKeyFromVault*(vaultAddr, token, path: string): Future[string] {.async.} =
let client = newAsyncHttpClient()
client.headers = newHttpHeaders({
"X-Vault-Token": token
})
try:
let response = await client.getContent(vaultAddr & "/v1/" & path)
let data = parseJson(response)
result = data["data"]["data"]["private_key"].getStr()
except Exception as e:
raise newException(IOError, "Failed to load key from Vault: " & e.msg)
4. Google Cloud Secret Manager
import std/[asyncdispatch, json]
import google_cloud/secretmanager
proc loadPrivateKeyFromGCP*(projectId, secretId: string): Future[string] {.async.} =
let client = newSecretManagerClient()
try:
let name = "projects/" & projectId & "/secrets/" & secretId & "/versions/latest"
let response = await client.accessSecretVersion(name)
result = response.payload.data.toString()
except Exception as e:
raise newException(IOError, "Failed to load key from GCP: " & e.msg)
🔄 鍵のローテーション
なぜローテーションが必要か?
理由:
1. 漏洩リスクの最小化
→ 万が一漏洩しても被害期間を限定
2. コンプライアンス要件
→ SOC2, ISO27001などで90日ローテーション推奨
3. 暗号学的強度の維持
→ 長期使用による解析リスクを軽減
ローテーション戦略
# server/src/key_rotation.nim
import std/[times, os, strutils]
type
KeyRotationConfig* = object
rotationIntervalDays*: int
keyDirectory*: string
archiveDirectory*: string
proc shouldRotate*(config: KeyRotationConfig): bool =
## 鍵ローテーションが必要かチェック
let privateKeyPath = config.keyDirectory / "private_key.pem"
if not fileExists(privateKeyPath):
return true # 鍵が存在しない場合は生成
# ファイルの最終更新日時を取得
let modTime = getLastModificationTime(privateKeyPath)
let now = now()
let daysSinceModification = (now - modTime).inDays
return daysSinceModification >= config.rotationIntervalDays
proc rotateKeys*(config: KeyRotationConfig): bool =
## 鍵ローテーション実行
try:
let now = now()
let timestamp = now.format("yyyyMMdd_HHmmss")
# 古い鍵をアーカイブ
if fileExists(config.keyDirectory / "private_key.pem"):
let archiveName = "private_key_" & timestamp & ".pem"
moveFile(
config.keyDirectory / "private_key.pem",
config.archiveDirectory / archiveName
)
if fileExists(config.keyDirectory / "public_key.pem"):
let archiveName = "public_key_" & timestamp & ".pem"
moveFile(
config.keyDirectory / "public_key.pem",
config.archiveDirectory / archiveName
)
# 新しい鍵ペアを生成
let result = execShellCmd("""
openssl ecparam -name prime256v1 -genkey -noout -out """ & config.keyDirectory & """/private_key.pem &&
chmod 400 """ & config.keyDirectory & """/private_key.pem &&
openssl ec -in """ & config.keyDirectory & """/private_key.pem -pubout -out """ & config.keyDirectory & """/public_key.pem &&
chmod 644 """ & config.keyDirectory & """/public_key.pem
""")
if result != 0:
raise newException(OSError, "Failed to generate new key pair")
echo "✅ Key rotation completed successfully"
return true
except Exception as e:
echo "❌ Key rotation failed: ", e.msg
return false
# 使用例
let config = KeyRotationConfig(
rotationIntervalDays: 90, # 90日ごとにローテーション
keyDirectory: "./keys",
archiveDirectory: "./keys/archive"
)
if shouldRotate(config):
discard rotateKeys(config)
🛡️ セキュリティベストプラクティス
1. パーミッション設定
# サーバー上での適切な権限設定
chown root:app-group /path/to/keys/
chmod 750 /path/to/keys/
chown root:app-group /path/to/keys/private_key.pem
chmod 440 /path/to/keys/private_key.pem # root + app-groupのみ読み取り可
chown root:app-group /path/to/keys/public_key.pem
chmod 644 /path/to/keys/public_key.pem # 全員読み取り可(公開鍵)
2. 暗号化保存
# 秘密鍵を暗号化して保存
import nimcrypto
proc encryptPrivateKey*(plainKey, masterPassword: string): string =
## マスターパスワードでAES-256暗号化
var ctx: CTR[aes256]
var key: array[32, byte] # AES-256 key
var iv: array[16, byte] # Initialization Vector
# パスワードからキー導出(PBKDF2)
let salt = "license-system-salt-change-me"
discard pbkdf2(sha256, masterPassword, salt, 100000, key)
# IVを生成
randomBytes(iv)
# 暗号化
ctx.init(key, iv)
let encrypted = ctx.encrypt(plainKey.toOpenArrayByte(0, plainKey.len - 1))
# IV + 暗号文をBase64エンコード
result = encode(iv & encrypted)
proc decryptPrivateKey*(encryptedKey, masterPassword: string): string =
## 暗号化された秘密鍵を復号化
let decoded = decode(encryptedKey)
let iv = decoded[0..<16]
let encrypted = decoded[16..^1]
var ctx: CTR[aes256]
var key: array[32, byte]
let salt = "license-system-salt-change-me"
discard pbkdf2(sha256, masterPassword, salt, 100000, key)
ctx.init(key, iv)
let decrypted = ctx.decrypt(encrypted)
result = decrypted.toString()
3. アクセスログ
# 秘密鍵アクセスを監査ログに記録
import std/logging
var logger = newConsoleLogger()
proc loadPrivateKeyWithAudit*(keyPath: string, userId: string): string =
logger.log(lvlInfo, "Private key accessed by user: " & userId)
try:
result = readFile(keyPath)
logger.log(lvlInfo, "Private key loaded successfully")
except Exception as e:
logger.log(lvlError, "Failed to load private key: " & e.msg)
raise
4. セキュリティチェックリスト
本番環境デプロイ前チェックリスト:
鍵管理:
- [ ] 秘密鍵は環境変数またはシークレットマネージャーに保存
- [ ] .gitignoreに鍵ファイルパスを追加
- [ ] 秘密鍵のファイルパーミッションを400に設定
- [ ] 鍵ローテーションスケジュールを設定(90日推奨)
- [ ] アーカイブ戦略を定義
アクセス制御:
- [ ] 最小権限の原則を適用
- [ ] 秘密鍵へのアクセスをロギング
- [ ] 定期的なアクセスログレビュー
暗号化:
- [ ] ECDSA P-256を使用(RSA 2048以上も可)
- [ ] JWTシークレットは256ビット以上
- [ ] パスワードはbcryptでハッシュ化
監査:
- [ ] すべての鍵操作をログに記録
- [ ] 異常アクセスの検知アラート設定
- [ ] 定期的なセキュリティ監査実施
🔍 漏洩検知と対応
漏洩検知方法
# GitHub Secret Scanningなどの自動検知ツールと連携
# 漏洩が検知された場合の自動対応
proc handleKeyLeak*(keyId: string) {.async.} =
## 鍵漏洩時の緊急対応
echo "⚠️ Key leak detected for key ID: ", keyId
# 1. 即座に鍵を無効化
await revokeKey(keyId)
# 2. 新しい鍵を生成
let newKeyId = await generateNewKey()
# 3. 全クライアントに通知
await notifyAllClients("Key rotation required. Please reactivate your license.")
# 4. インシデントレポート作成
await createIncidentReport(keyId, newKeyId)
# 5. セキュリティチームに通知
await notifySecurityTeam("Key leak incident", keyId)
echo "✅ Key leak response completed. New key ID: ", newKeyId
漏洩後の復旧手順
1. 漏洩した鍵を即座に無効化
2. すべての関連JWTトークンを失効
3. 新しい鍵ペアを生成
4. 全ユーザーに再認証を要求
5. インシデントレポート作成
6. 根本原因分析と再発防止策実施
🌟 まとめ
鍵管理とセキュリティの要点:
-
鍵生成
- ECDSA P-256
- OpenSSL使用
- 適切なパーミッション
-
保存戦略
- 開発: 環境変数 + .gitignore
- 本番: Secrets Manager
- 暗号化保存
-
ローテーション
- 90日ごと推奨
- 自動ローテーション
- アーカイブ戦略
-
セキュリティ
- 最小権限の原則
- アクセスログ
- 漏洩検知と対応
前回: Day 16: クライアントサイドの実装(WebAssembly)
次回: Day 18: デバイスフィンガープリント
Happy Learning! 🎉