Storing sensitive user data like social security numbers, API keys, or personal identification documents requires more than just HTTPS and secure passwords. If an attacker gains database access, unencrypted sensitive fields become an immediate liability. Rails 8 provides built-in Active Record Encryption that makes protecting data at rest straightforward and maintainable.
The Problem: Sensitive Data in Plain Text
Consider a typical user profile that stores a government ID number or payment account details. Without encryption, a database dump exposes this information directly:
# Direct database query shows plain text
mysql> SELECT government_id FROM users WHERE id = 1;
+----------------+
| government_id |
+----------------+
| 123-45-6789 |
+----------------+Active Record Encryption solves this by transparently encrypting data before it reaches the database and decrypting it when loaded into models. The application code remains clean, while the database stores only ciphertext.
Setting Up Encryption Keys
Rails 8 requires three keys for encryption: a primary key, a deterministic key, and a key derivation salt. Generate these using the built-in Rails task:
$ bin/rails db:encryption:initThis outputs credentials to add to your encrypted credentials file:
# Run: EDITOR="code --wait" bin/rails credentials:edit
# config/credentials.yml.enc
active_record_encryption:
primary_key: aGVsbG93b3JsZHRoaXNpc2Fwcmlt
deterministic_key: ZGV0ZXJtaW5pc3RpY2tleWV4YW1wbGU=
key_derivation_salt: c2FsdHlzYWx0eXNhbHR5c2FsdHk=For production environments with separate key management, configure environment variables in the initializer:
# config/initializers/active_record_encryption.rb
Rails.application.configure do
config.active_record.encryption.primary_key = ENV["AR_ENCRYPTION_PRIMARY_KEY"]
config.active_record.encryption.deterministic_key = ENV["AR_ENCRYPTION_DETERMINISTIC_KEY"]
config.active_record.encryption.key_derivation_salt = ENV["AR_ENCRYPTION_KEY_DERIVATION_SALT"]
endEncrypting Model Attributes
With keys configured, declare encrypted attributes in models using the encrypts macro. The simplest form uses non-deterministic encryption, which produces different ciphertext each time—even for identical values:
# app/models/user.rb
class User < ApplicationRecord
encrypts :government_id
encrypts :medical_notes
endCreate a migration to add these columns as text or string types. Rails handles the encryption transparently:
# db/migrate/20251231120000_add_encrypted_fields_to_users.rb
class AddEncryptedFieldsToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :government_id, :text
add_column :users, :medical_notes, :text
end
endNow the application code works exactly as before, but database contents are encrypted:
# Usage remains transparent
user = User.create!(government_id: "123-45-6789", medical_notes: "Annual checkup complete")
user.government_id # => "123-45-6789"
# But the database stores ciphertext
mysql> SELECT government_id FROM users WHERE id = 1;
+----------------------------------------------------------+
| government_id |
+----------------------------------------------------------+
| {"p":"aWdIc2R...encoded_ciphertext...","h":{"iv":"..."}} |
+----------------------------------------------------------+Deterministic vs Non-Deterministic Encryption
Non-deterministic encryption provides stronger security because identical values produce different ciphertext, preventing pattern analysis. However, this makes querying encrypted columns impossible.
When queries are necessary—such as finding a user by encrypted email—use deterministic encryption:
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true
encrypts :government_id # non-deterministic, more secure
encrypts :medical_notes # non-deterministic, more secure
endDeterministic encryption allows find_by and where queries:
# Works with deterministic encryption
User.find_by(email: "[email protected]")
User.where(email: ["[email protected]", "[email protected]"])
# Does NOT work with non-deterministic encryption
User.find_by(government_id: "123-45-6789") # Always returns nilChoose deterministic encryption only for fields that require querying. Prefer non-deterministic encryption for maximum security on fields that are only read, not searched.
Encrypting Existing Data
For applications with existing unencrypted data, Rails provides a migration path. First, add the encrypts declaration with support for unencrypted reads:
# app/models/user.rb
class User < ApplicationRecord
encrypts :government_id, support_unencrypted_data: true
endCreate a migration task to encrypt existing records:
# lib/tasks/encryption.rake
namespace :encryption do
desc "Encrypt existing user government IDs"
task encrypt_government_ids: :environment do
User.find_each do |user|
# Reading and saving triggers encryption
user.update!(government_id: user.government_id)
end
puts "Encrypted #{User.count} user records"
end
endRun the task, then remove the support_unencrypted_data option once migration completes:
$ bin/rails encryption:encrypt_government_idsHandling Key Rotation
Security best practices recommend periodic key rotation. Rails 8 supports multiple keys, allowing decryption with old keys while encrypting new data with the current key:
# config/credentials.yml.enc
active_record_encryption:
primary_key:
- new_primary_key_2025 # Current key (first position)
- old_primary_key_2024 # Previous key for decryption
deterministic_key: current_deterministic_key
key_derivation_salt: consistent_salt_valueAfter adding the new key, re-encrypt existing data to use the new key:
# lib/tasks/encryption.rake
namespace :encryption do
desc "Re-encrypt all user data with current key"
task rotate_keys: :environment do
User.find_each do |user|
user.encrypt
end
end
endCommon Mistakes to Avoid
Several pitfalls can undermine encryption efforts:
- Logging encrypted values: Ensure encrypted attributes are filtered from logs by adding them to
config.filter_parameters - Serialization leaks: Be careful with
to_jsonor API responses that might expose decrypted values unintentionally - Backup encryption: Database backups contain encrypted data, but ensure backup systems cannot access decryption keys
- Search functionality: Full-text search on encrypted fields requires additional architecture like blind indexes
# config/initializers/filter_parameters.rb
Rails.application.config.filter_parameters += [
:government_id,
:medical_notes,
:password,
:ssn
]Testing Encrypted Attributes
Testing encrypted models requires no special setup—encryption works transparently in the test environment:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe "encrypted attributes" do
it "encrypts government_id transparently" do
user = User.create!(government_id: "123-45-6789")
# Reload from database
user.reload
expect(user.government_id).to eq("123-45-6789")
end
it "allows querying deterministic encrypted email" do
user = User.create!(email: "[email protected]")
found = User.find_by(email: "[email protected]")
expect(found).to eq(user)
end
end
endSummary
Active Record Encryption in Rails 8 provides a robust foundation for protecting sensitive data at rest. The key decisions when implementing encryption are:
- Use non-deterministic encryption by default for maximum security
- Use deterministic encryption only when querying is required
- Store encryption keys in Rails credentials or environment variables, never in source control
- Plan for key rotation from the start
- Filter encrypted attributes from logs and be mindful of API serialization
With these patterns in place, applications can store sensitive user data with confidence that a database breach does not automatically mean a data breach.