データの暗号化 — 保存データを守る
「顧客の在庫データが漏洩した可能性がある」
あの夜からずっと、リョウの頭を離れなかった言葉だ。攻撃者がデータベースに直接アクセスできた場合、パスワードはBCryptでハッシュ化されていたので読めない。しかし、在庫数量、仕入れ価格、サプライヤー情報は平文で保存されていた。
「暗号化されていれば、たとえデータを取得されても読めなかった」
リョウはデータ暗号化の実装に取り掛かった。
データ分類の重要性
すべてのデータを同じように扱うのではなく、機密度に応じて分類することが重要だ。
| 分類 | データ例 | 保護方法 |
|---|---|---|
| 公開 | 商品名、カテゴリ | 特別な保護不要 |
| 内部 | 在庫数量、注文履歴 | アクセス制限 |
| 機密 | 仕入れ価格、利益率 | 暗号化必須 |
| 最高機密 | 個人情報、決済情報 | KMS + 暗号化 |
AWS KMSで鍵を管理する
AWS Key Management Service(KMS)は、暗号化キーの作成・管理・ローテーションを安全に行うサービスだ。
# KMSキーを作成
aws kms create-key \
--description "StockFlow data encryption key" \
--key-usage ENCRYPT_DECRYPT \
--key-spec SYMMETRIC_DEFAULT \
--tags TagKey=Project,TagValue=StockFlow
# キーの自動ローテーションを有効化(1年ごと)
aws kms enable-key-rotation \
--key-id "arn:aws:kms:ap-northeast-1:123456789012:key/xxx"
# キーエイリアスを設定(人が読みやすい名前)
aws kms create-alias \
--alias-name "alias/stockflow-data-key" \
--target-key-id "arn:aws:kms:ap-northeast-1:123456789012:key/xxx"# terraform/kms.tf
resource "aws_kms_key" "data_encryption" {
description = "StockFlow data encryption key"
key_usage = "ENCRYPT_DECRYPT"
customer_master_key_spec = "SYMMETRIC_DEFAULT"
# 自動ローテーション(年次)
enable_key_rotation = true
# 削除保護(誤削除防止)
deletion_window_in_days = 30
# キーポリシー
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Enable IAM User Permissions"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
{
Sid = "Allow API Service"
Effect = "Allow"
Principal = { AWS = aws_iam_role.api_service.arn }
Action = [
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
]
Resource = "*"
}
]
})
}attr_encryptedでカラム暗号化
attr_encrypted gemを使えば、モデルの特定のカラムを透過的に暗号化できる。
# Gemfile
gem 'attr_encrypted', '~> 4.0'# db/migrate/20240115000004_add_encrypted_fields_to_inventory_items.rb
class AddEncryptedFieldsToInventoryItems < ActiveRecord::Migration[7.1]
def change
# 暗号化カラム(平文カラムを削除して暗号化版に移行)
add_column :inventory_items, :encrypted_purchase_price, :string
add_column :inventory_items, :encrypted_purchase_price_iv, :string
add_column :inventory_items, :encrypted_supplier_code, :string
add_column :inventory_items, :encrypted_supplier_code_iv, :string
add_column :inventory_items, :encrypted_internal_notes, :text
add_column :inventory_items, :encrypted_internal_notes_iv, :string
end
end# app/models/inventory_item.rb
class InventoryItem < ApplicationRecord
# KMSを使ってデータ暗号化鍵(DEK)を管理する
# エンベロープ暗号化パターン:
# 1. AES-256でデータを暗号化(DEK)
# 2. DEKをKMSのCMKで暗号化
# 3. 暗号化されたDEKをDBに保存
encrypts :purchase_price,
key: :encryption_key,
encode: true,
type: :decimal
encrypts :supplier_code,
key: :encryption_key,
encode: true
encrypts :internal_notes,
key: :encryption_key,
encode: true
private
def encryption_key
# KMSからデータ暗号化鍵を取得(Railsの暗号化機能ではなくattr_encryptedの場合)
@encryption_key ||= fetch_encryption_key
end
def fetch_encryption_key
Rails.cache.fetch("kms_data_key:#{company_id}", expires_in: 5.minutes) do
kms = Aws::KMS::Client.new
response = kms.generate_data_key(
key_id: ENV['KMS_KEY_ARN'],
key_spec: 'AES_256'
)
response.plaintext
end
end
endActive RecordのEncrypts(Rails 7.1+)
Rails 7.1からはネイティブの暗号化機能が使える。
# config/application.rb
module Stockflow
class Application < Rails::Application
# 暗号化の設定
config.active_record.encryption.primary_key = Rails.application.credentials.dig(:active_record_encryption, :primary_key)
config.active_record.encryption.deterministic_key = Rails.application.credentials.dig(:active_record_encryption, :deterministic_key)
config.active_record.encryption.key_derivation_salt = Rails.application.credentials.dig(:active_record_encryption, :key_derivation_salt)
end
end# 暗号化キーを生成してcredentialsに保存
rails db:encryption:init
# 出力例:
# active_record_encryption:
# primary_key: abc123...
# deterministic_key: def456...
# key_derivation_salt: ghi789...# app/models/customer.rb
class Customer < ApplicationRecord
# 機密フィールドを暗号化
encrypts :tax_number, deterministic: true # 検索可能な暗号化
encrypts :phone_number
encrypts :address, downcase: false
# 個人情報の特別な保護
encrypts :national_id, key_provider: :customer_specific_key
private
def customer_specific_key
# 顧客ごとに異なる鍵を使用(最高機密データ用)
Aws::KMS::Client.new.generate_data_key(
key_id: ENV['KMS_CUSTOMER_KEY_ARN'],
key_spec: 'AES_256'
).plaintext
end
endINFO
deterministic: trueは同じ値が常に同じ暗号文になるため、WHERE句での検索が可能です。ただし頻度分析攻撃に弱くなるため、一意性が高い値(メールアドレス、電話番号等)に限定して使用します。
パスワードの適切なハッシュ化
パスワードは「暗号化」ではなく「ハッシュ化」する。違いは不可逆性だ。
# app/models/user.rb
class User < ApplicationRecord
has_secure_password # BCryptを使ったパスワードハッシュ化
# BCryptのコスト(ラウンド数)を設定
# コストが高いほど安全だが、ログイン時間も増加
# cost: 12は約0.3秒(デフォルトは10)
BCrypt::Engine.cost = 12
# パスワード再ハッシュ(コスト変更時の移行)
def authenticate_with_upgrade(password)
return false unless authenticate(password)
# コストが変わった場合、ログイン時に自動アップグレード
if BCrypt::Password.new(password_digest).cost != BCrypt::Engine.cost
update!(password: password)
end
true
end
endRDS暗号化
データベース自体も暗号化する。
# terraform/rds.tf
resource "aws_db_instance" "main" {
identifier = "stockflow-postgres"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.medium"
allocated_storage = 100
# 保存データの暗号化(必須)
storage_encrypted = true
kms_key_id = aws_kms_key.rds.arn
# 自動バックアップも暗号化される
backup_retention_period = 7
backup_window = "03:00-04:00"
# 削除保護
deletion_protection = true
# パブリックアクセス禁止
publicly_accessible = false
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
}S3バケットの暗号化
ファイルストレージも暗号化する。
# terraform/s3.tf
resource "aws_s3_bucket" "exports" {
bucket = "stockflow-exports-${data.aws_caller_identity.current.account_id}"
}
# KMSによる暗号化(SSE-KMS)
resource "aws_s3_bucket_server_side_encryption_configuration" "exports" {
bucket = aws_s3_bucket.exports.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.s3.arn
}
bucket_key_enabled = true # コスト削減
}
}
# パブリックアクセスのブロック
resource "aws_s3_bucket_public_access_block" "exports" {
bucket = aws_s3_bucket.exports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}データマスキング(センシティブデータの表示制限)
APIレスポンスでもデータをマスキングする。
# app/serializers/customer_serializer.rb
class CustomerSerializer < ActiveModel::Serializer
attributes :id, :name, :email, :phone, :tax_number
def email
return object.email if scope&.admin?
# 管理者以外はメールをマスキング
mask_email(object.email)
end
def phone
return object.phone if scope&.admin?
# 電話番号の最後4桁のみ表示
"***-***-#{object.phone&.last(4)}"
end
def tax_number
return object.tax_number if scope&.admin?
# 法人番号は非表示
"[REDACTED]"
end
private
def mask_email(email)
return unless email
local, domain = email.split('@')
"#{local.first}***@#{domain}"
end
end暗号化データの検索
暗号化されたデータを検索するために、別途インデックス用のハッシュを保存する手法がある。
# app/models/concerns/searchable_encrypted.rb
module SearchableEncrypted
extend ActiveSupport::Concern
# 検索用に決定論的なハッシュを生成
def self.search_hash(value)
OpenSSL::HMAC.hexdigest(
'SHA256',
Rails.application.credentials.search_key!,
value.to_s.downcase.strip
)
end
end
# app/models/customer.rb
class Customer < ApplicationRecord
encrypts :email, deterministic: true # Railsの暗号化で検索可能にする
# または、検索ハッシュを別カラムに保存する方法
before_save :compute_email_search_hash
private
def compute_email_search_hash
self.email_search_hash = SearchableEncrypted.search_hash(email) if email_changed?
end
end
# app/models/concerns/searchable_encrypted.rb に追加
scope :find_by_email, ->(email) {
where(email_search_hash: SearchableEncrypted.search_hash(email))
}鍵のローテーション手順
KMSキーをローテーションした後、既存の暗号化データを再暗号化する必要がある。
# app/jobs/reencrypt_data_job.rb
class ReencryptDataJob < ApplicationJob
def perform(model_class, batch_size: 100)
klass = model_class.constantize
klass.find_in_batches(batch_size: batch_size) do |batch|
batch.each do |record|
# 古い鍵で復号化し、新しい鍵で再暗号化
record.reencrypt!
end
end
Rails.logger.info("Reencryption complete for #{model_class}")
end
endWARNING
KMSの自動ローテーションは新しいキーマテリアルを追加しますが、古いデータの再暗号化は行いません。AWS KMSは古いキーマテリアルを保持するため、古いデータも復号できます。手動でデータを再暗号化する場合は、移行期間を設けて段階的に行ってください。
チェックリスト
- データを機密度別に分類し、暗号化ポリシーを定めている
- パスワードはBCryptでハッシュ化している(cost: 12以上)
- 機密フィールドはActive RecordのEncryptsまたはattr_encryptedで暗号化している
- AWS KMSで暗号化キーを管理し、自動ローテーションを有効にしている
- RDSの保存暗号化が有効になっている
- S3バケットがSSE-KMSで暗号化されている
- APIレスポンスで機密データをマスキングしている
- 暗号化データの検索方法を適切に設計している
- 鍵ローテーション手順が文書化されている