active-record-encryption について
Active Record Encryptionで機密属性をアプリケーション層で暗号化するための、Rails開発者向けスキルです。鍵の設定、検索時の選択、データ移行、テスト時の注意点を扱います。
主な機能
- 01鍵と暗号化属性の設定
- 02決定的・非決定的暗号化の選択
- 03既存の平文データの移行
- 04カラム容量と大文字小文字の処理
- 05鍵ローテーションとテストの設定
活用シーン
- 01モデル属性の暗号化
- 02未暗号化レコードの移行
- 03決定的暗号化フィールドの検索
- 04復号や検索エラーの調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill active-record-encryptionインストール後、Claude Code で/active-record-encryptionを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
Active Record Encryption エキスパート
Rails 組み込みの Active Record Encryption を使用して、アプリケーションレベルで機密データを暗号化します。
基本方針
基本原則:
- 必要なものだけを暗号化する — 暗号化により、複雑さとストレージのオーバーヘッドが増します。慎重に判断してください。
- クエリが必要な場合に限り決定論的暗号化を使う — 非決定論的暗号化のほうが安全です。デフォルトでは非決定論的暗号化を使用してください。
- キーはコードではなく credentials に保存する — 暗号化キーをコミットしてはいけません。絶対にです。
- 暗号化する前に移行を計画する — 既存の未暗号化データには、移行戦略が必要です。
- 暗号化を有効にしてテストする —
encrypt_fixtures: trueが必要です。設定しないとテストが失敗します。
判断基準:
Need to query/find_by this field?
YES → deterministic: true
NO → default (non-deterministic)
Need uniqueness validation?
YES → deterministic: true (+ downcase/ignore_case if case-insensitive)
Need to preserve case but query case-insensitively?
YES → deterministic: true, ignore_case: true (adds original_<column> column)
Just protecting data at rest (logs, backups)?
YES → default (non-deterministic) is perfect
このスキルを使う場面
- モデル属性に
encrypts宣言を追加する場合 - 初めて暗号化キーを設定する場合
- 既存の未暗号化データを暗号化するために移行する場合
- 暗号化キーをローテーションする場合
- 決定論的に暗号化された属性をクエリする場合
- Action Text のコンテンツを暗号化する場合
- 暗号化関連のエラー(復号失敗、クエリの不一致)をデバッグする場合
- 決定論的暗号化と非決定論的暗号化のトレードオフを理解する場合
避けるべき重大な間違い
🚨 エージェントが最もよく犯す間違いです。コードを書く前にお読みください。
-
非決定論的フィールドをクエリする —
Model.find_by(field: value)が機能するのは、deterministic: trueの場合のみです。非決定論的暗号化では、暗号文が毎回異なるため、データベースで一致させられません。 -
キーの生成や設定を忘れる —
rails db:encryption:initを実行してキーを credentials に保存しないと、encrypts :emailは何の役にも立ちません。ActiveRecord::Encryption::Errors::Configurationエラーが発生します。 -
決定論的モードを使わず、インデックスが必要なフィールドを暗号化する — 非決定論的カラムのデータベースインデックスは役に立ちません。一意インデックスが必要な場合は、
deterministic: trueを使用してください。 -
既存データへの対応を計画しない — 既存の行があるモデルに
encryptsを追加すると、Rails が平文の値を復号しようとするため、読み取りに失敗します。移行中はsupport_unencrypted_dataを有効にしてください。 -
カラムを追加せずに
ignore_case: trueを使用する — このオプションには、データベース内のoriginal_<column_name>カラムが必要です。これがないとクラッシュします。 -
serializeをencryptsの後に宣言する — シリアライズされた属性では、モデル内でserializeをencryptsより前に記述する必要があります。Rails はこれらの宣言を順番に処理するため、暗号化でシリアライズ済みの値をラップする必要があります。 -
決定論的暗号化のキーをローテーションしようとする — 同じ平文からは常に同じ暗号文が生成される必要があるため(これによってクエリが機能します)、決定論的暗号化ではキーのローテーションはサポートされていません。キーを変更すると、すべての検索がひそかに機能しなくなります。
-
文字列カラムのサイズを小さくしすぎる — 暗号化されたデータはサイズが大きくなります。
string(255)のメールカラムを暗号化する場合、少なくともstring(510)が必要です。サイズのガイドについては reference.md を参照してください。
手順
ステップ 1:キーを生成して保存する
まずここから始めてください。 キーを設定しないと、encrypts 宣言によって実行時に設定エラーが発生します。
bin/rails db:encryption:init
3 つの値が出力されます。これらを credentials に保存します。
bin/rails credentials:edit
active_record_encryption:
primary_key: YehXdfzxVKpoLvKseJMJIEGs2JxerkB8
deterministic_key: uhtk2DYS80OweAPnMLtrV2FhYIXaceAy
key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS
代替方法:環境変数(12-factor アプリ):
# config/application.rb
config.active_record.encryption.primary_key = ENV["ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT"]
キーが設定されていることを確認する:
bin/rails runner "puts ActiveRecord::Encryption.config.primary_key.present?"
# Should print: true
ステップ 2:既存のパターンを確認する
まずコードベースを検索し、既存の暗号化パターンを確認します。
# Find existing encryption declarations
rg "encrypts :" app/models/
# Check for existing encryption config
rg "active_record.encryption" config/
rg "active_record_encryption" config/credentials/
# Check column sizes for fields you plan to encrypt
bin/rails runner "ActiveRecord::Base.connection.columns('users').each { |c| puts \"#{c.name}: #{c.type}(#{c.limit})\" }"
既存のプロジェクト規約に合わせてください — 環境変数を使用している場合は環境変数を使います。credentials を使用している場合は credentials を使います。
ステップ 3:暗号化する属性を宣言する
非決定論的(デフォルト — 最も安全):
class User < ApplicationRecord
encrypts :ssn
encrypts :medical_notes
end
決定論的(クエリ可能):
class User < ApplicationRecord
encrypts :email, deterministic: true
encrypts :phone, deterministic: true, downcase: true
end
Action Text:
class Message < ApplicationRecord
has_rich_text :content, encrypted: true
end
シリアライズされた属性を使う場合(順序が重要です!):
class Article < ApplicationRecord
serialize :metadata, type: Hash # FIRST
encrypts :metadata # SECOND
end
ステップ 4:カラムサイズを調整する
暗号化する前に、必要に応じてカラムを確認し、サイズを変更してください。
| 元のカラム | 暗号化に必要なサイズ |
|---|---|
string(255) ASCII | string(510) |
string(255) Unicode | string(1275) |
string(500) 非西洋文字 | string(2255) |
text | text(変更不要) |
カラムサイズが小さすぎる場合は、マイグレーションを生成してください。
class ResizeEmailForEncryption < ActiveRecord::Migration[7.1]
def change
change_column :users, :email, :string, limit: 510
end
end
ステップ 5:既存の未暗号化データを移行する
テーブルにすでにデータがある場合は、移行を計画してください。 そうしないと、Rails は平文の行を復号しようとして Decryption エラーを発生させます。
フェーズ 1:共存を有効にする
# config/application.rb
config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true
フェーズ 2:既存のレコードを暗号化する(タスクまたはマイグレーションとして実行)
# lib/tasks/encryption.rake
namespace :encryption do
desc "Encrypt existing user data"
task encrypt_users: :environment do
User.find_each do |user|
user.encrypt
rescue => e
Rails.logger.error "Failed to encrypt user #{user.id}: #{e.message}"
end
end
end
bin/rails encryption:encrypt_users
フェーズ 3:確認して共存を無効にする
# Verify all records are encrypted
User.find_each do |user|
unless user.encrypted_attribute?(:email)
puts "User #{user.id} still unencrypted"
end
end
すべてのデータが暗号化されたら、共存設定を削除してください。
# Remove these lines:
# config.active_record.encryption.support_unencrypted_data = true
# config.active_record.encryption.extend_queries = true
ステップ 6:キーのローテーションを設定する(非決定的暗号化のみ)
キーのローテーションは非決定的暗号化でのみ機能します。 決定的フィールドは元のキーを維持する必要があります。キーを変更すると暗号文が一致しなくなり、すべてのクエリが機能しなくなります。
新しいキーをリストに追加します。最後のキーで暗号化し、すべてのキーで復号します。
# credentials
active_record_encryption:
primary_key:
- old_key_abc123 # Can still decrypt
- new_key_xyz789 # Active — encrypts new data
key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS
新しいキーで既存のデータを再暗号化します。
User.find_each(&:encrypt)
再暗号化後、古いキーを削除します。
ステップ 7:テスト環境を設定する
config/environments/test.rb に追加します。
Rails.application.configure do
config.active_record.encryption.encrypt_fixtures = true
end
これがないと、フィクスチャの値が暗号化されず、読み取りに失敗します。
データを移行する場合は、テスト環境でも共存を有効にします。
config.active_record.encryption.support_unencrypted_data = true
config.active_record.encryption.extend_queries = true
ステップ 8:クエリを適切に処理する
決定的暗号化 — クエリは通常どおり機能します。
# These work with deterministic: true
User.find_by(email: "[email protected]")
User.where(email: "[email protected]")
非決定的暗号化 — クエリは実行できません。
# These can't work with non-deterministic encryption — each encryption
# produces a different ciphertext, so the DB can't match against it
User.find_by(ssn: "123-45-6789") # => nil (always)
User.where(ssn: "123-45-6789") # => empty (always)
非決定的フィールドで検索する必要がある場合は、次の 2 つの方法があります。
deterministic: trueに切り替える(セキュリティは低下します)- 検索用に別の決定的なダイジェスト/ハッシュカラムを追加する
ステップ 9:一意性と大文字・小文字の区別
一意性のバリデーションには決定的暗号化が必要です。
class User < ApplicationRecord
validates :email, uniqueness: true
encrypts :email, deterministic: true, downcase: true
end
バリデーションで case_sensitive: false を使用しないでください — データベースには平文ではなく暗号文が見えるため、暗号化されたフィールドでは機能しません。代わりに encrypts の downcase: true または ignore_case: true を使用してください。暗号化前に大文字・小文字を正規化できます。
downcase: true — 元の大文字・小文字は失われます。シンプルです。
ignore_case: true — 元の大文字・小文字を保持しますが、追加のカラムが必要です。
# Migration needed:
add_column :labels, :original_name, :string
# Model:
class Label < ApplicationRecord
encrypts :name, deterministic: true, ignore_case: true
end
クイックリファレンス
encrypts のオプション
| オプション | デフォルト | 目的 |
|---|---|---|
deterministic: | false | クエリ可能な暗号化を有効にする |
downcase: | false | 暗号化前に小文字にする(決定的暗号化のみ) |
ignore_case: | false | 大文字・小文字を区別しないクエリを可能にし、元の値を保持する(original_ カラムが必要) |
key_provider: | global | この属性用のカスタムキー・プロバイダー |
key: | nil | この属性用の特定の暗号化キー |
previous: | nil | 移行用の以前の暗号化方式 |
compress: | true | ペイロードの圧縮を有効にする |
compressor: | Zlib | カスタム圧縮アルゴリズム |
message_serializer: | default | 暗号化ペイロード用のカスタムシリアライザー |
設定オプション
# config/application.rb
config.active_record.encryption.primary_key = "..."
config.active_record.encryption.deterministic_key = "..."
config.active_record.encryption.key_derivation_salt = "..."
config.active_record.encryption.support_unencrypted_data = false # true during migration
config.active_record.encryption.extend_queries = false # true during migration
config.active_record.encryption.encrypt_fixtures = false # true in test env
config.active_record.encryption.add_to_filter_parameters = true # auto-filter in logs
config.active_record.encryption.store_key_references = false # faster decryption, larger payload
config.active_record.encryption.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
プログラムから使用する API
record.encrypt # Encrypt/re-encrypt all encryptable attributes
record.decrypt # Decrypt all encryptable attributes
record.encrypted_attribute?(:field) # Check if attribute is currently encrypted
record.ciphertext_for(:field) # Read raw ciphertext
# Run code without encryption (reads return ciphertext, writes store plaintext)
ActiveRecord::Encryption.without_encryption { ... }
# Run code that can read encrypted data but can't overwrite it
ActiveRecord::Encryption.protecting_encrypted_data { ... }
よくあるエラー → 解決方法
| エラー | 原因 | 解決方法 |
|---|---|---|
ActiveRecord::Encryption::Errors::Configuration | キーが設定されていない | rails db:encryption:init を実行し、認証情報に保存する |
ActiveRecord::Encryption::Errors::Decryption | キーが間違っている、またはデータが破損している | キー設定を確認する。移行中は support_unencrypted_data を有効にする |
既存データのクエリ結果が nil | 非決定的フィールドをクエリしている | encrypts の宣言に deterministic: true を追加する |
undefined column 'original_X' | マイグレーションを行わずに ignore_case: true を使用している | マイグレーションで original_<column> カラムを追加する |
| フィクスチャのテストが失敗する | フィクスチャが暗号化されていない | テスト設定で encrypt_fixtures = true を設定する |
| シリアライズされた属性が気付かないうちに壊れている | 宣言の順序が間違っている | serialize を encrypts より前に記述する |
デバッグのヒント
# Check if encryption is properly configured
bin/rails runner "pp ActiveRecord::Encryption.config"
# Check if a specific record's attribute is encrypted
bin/rails runner "puts User.first.encrypted_attribute?(:email)"
# Read raw ciphertext
bin/rails runner "puts User.first.ciphertext_for(:email)"
# Temporarily disable encryption to see raw database values
ActiveRecord::Encryption.without_encryption do
puts User.first.read_attribute(:email)
end
# Check what's actually stored in the database
bin/rails runner "puts ActiveRecord::Base.connection.select_value(\"SELECT email FROM users LIMIT 1\")"
避けるべきアンチパターン
- すべてを暗号化する — 本当に機密性の高いフィールド(PII、金融データ、健康情報)のみを暗号化します。
nameやcreated_atを暗号化しても、セキュリティ上のメリットはなく、オーバーヘッドが増えるだけです。 - 非決定的な暗号化で十分なのに、決定的な暗号化を使う — 決定的な暗号化は安全性が低くなります。そのフィールドで検索する必要がある場合にのみ使用してください。
- 移行計画を忘れる — 既存のデータがあるモデルに
support_unencrypted_dataなしでencryptsを追加すると、読み取り時にエラーが発生します。 - キーをリポジトリに保存する — 設定ファイルにも、シードにも、コード内の ENV のデフォルト値にも保存しないでください。認証情報または実際の環境変数のみを使用してください。
- カラムのサイズ変更を省略する — 暗号化されたデータはサイズが大きくなります。カラムのサイズが不足すると、切り詰められてデータが失われます。
encrypt_fixturesなしでテストする — フィクスチャは未加工の YAML 値を読み込みます。テスト時に暗号化しないと、比較が失敗します。- 決定的な暗号化のキーをローテーションする — すべてのクエリが警告なしに機能しなくなります。決定的に暗号化するフィールドでは、元のキーを維持する必要があります。
- バリデーションで
case_sensitive: falseを使う — 暗号化されたフィールドでは機能しません。encryptsにdowncase: trueまたはignore_case: trueを使用してください。