Configure Active Record Encryption in Rails 7+ for Transparent Attribute Encryption
A step-by-step guide to configuring Active Record Encryption in Rails 7+: generate keys, add encrypted columns, declare deterministic or non-deterministic attributes, backfill existing data, rotate keys, and verify the implementation.
19 Aug 2025, 04:27 UTC

Problem and Takeaway
You need to encrypt sensitive model attributes—SSNs, API tokens, PII—at rest in the database without changing application code that reads or writes those fields. Rails 7.0+ includes Active Record Encryption, which handles encryption and decryption transparently when you declare encrypts :attribute on a model. The practical takeaway: generate three keys, add an _encrypted column via migration, declare the attribute with the appropriate determinism, backfill existing data, and verify the raw stored value differs from the plaintext.
Prerequisites
- Rails 7.0 or newer (7.1+ adds
db:encryption:statusandextend_queries). - Application uses
config/credentials.yml.encor environment variables for secret storage; plaintext keys must never be committed. - Database supports
TEXTorBLOBcolumns (PostgreSQL, MySQL, SQLite all work). - Downtime window or deployment strategy for the backfill step if the table is large.
Generate Encryption Keys
Run the built-in generator to produce a primary key, a deterministic key, and a key-derivation salt. Execute this locally or in a secure CI environment:
bin/rails db:encryption:initOutput example (do not copy these values; generate your own):
primary_key: a1b2c3d4e5f6... (64 hex chars)
deterministic_key: f6e5d4c3b2a1... (64 hex chars)
key_derivation_salt: 1a2b3c4d5e6f... (64 hex chars)Store the three values in config/credentials.yml.enc under active_record_encryption or set them as environment variables ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY, ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY, ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT. Rails reads both locations automatically.
Add the Encrypted Column
Create a migration that adds a TEXT column named #{attribute}_encrypted. For a User model with an ssn attribute:
bin/rails generate migration AddEncryptedSsnToUsers ssn_encrypted:textThe generated migration:
class AddEncryptedSsnToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :ssn_encrypted, :text
end
endRun bin/rails db:migrate. The plaintext ssn column can remain temporarily; you will drop it after backfill.
Declare Encryption on the Model
In app/models/user.rb, add the encrypts macro. Choose determinism based on query needs:
- Deterministic (
deterministic: true): same plaintext always produces same ciphertext. AllowsWHERE ssn = '123-45-6789'and equality indexes, but reveals which rows share a value. Suitable for low-entropy identifiers like SSN, email, or status codes. - Non-deterministic (
deterministic: false, default): each encryption yields a unique ciphertext. Stronger confidentiality; prevents equality queries and indexing on the encrypted column. Use for high-entropy secrets (API tokens, encryption keys).
Example for SSN (deterministic):
class User < ApplicationRecord
encrypts :ssn, deterministic: true
endExample for an API token (non-deterministic):
class User < ApplicationRecord
encrypts :api_token
endBackfill Existing Plaintext Data
After deploying the model change, existing rows still have plaintext in ssn and NULL in ssn_encrypted. Run a one-time script to encrypt them. Use find_each to avoid memory pressure:
User.find_each do |user|
user.update!(ssn: user.ssn) # triggers encryption write to ssn_encrypted
endFor large tables, run this in a background job (Sidekiq, GoodJob) with batching. After backfill completes and you verify decryption works, drop the plaintext column in a follow-up migration:
bin/rails generate migration RemovePlaintextSsnFromUsers ssn:string
# edit migration to remove_column :users, :ssn
bin/rails db:migrateKey Rotation
If keys are compromised or you rotate periodically, Rails supports reading with old keys while writing with new ones. Add the previous key set to config/application.rb or an initializer:
config.active_record.encryption.previous_keys = [
{
primary: ENV['OLD_PRIMARY_KEY'],
deterministic: ENV['OLD_DETERMINISTIC_KEY'],
key_derivation_salt: ENV['OLD_KEY_DERIVATION_SALT']
}
]Deploy with both old and new keys present. Existing records decrypt with the old key; new writes use the new primary key. To re-encrypt all data with the new key, run the same backfill script again after rotation.
Testing Configuration
In config/environments/test.rb, allow fixtures and factories to write unencrypted data so tests don't need encryption keys:
config.active_record.encryption.support_unencrypted_data = trueUse the built-in assertion helper in model specs:
require 'test_helper'
class UserTest < ActiveSupport::TestCase
test 'ssn is encrypted at rest' do
user = User.create!(ssn: '123-45-6789')
assert_encrypted :ssn
assert_equal '123-45-6789', user.ssn
end
endThe helper reads the raw ssn_encrypted column and confirms it differs from the plaintext.
Verification Steps
- Console check: After deployment, run
bin/rails runner "puts User.first.ssn"in production. The output should be the plaintext value, confirming decryption works. - Schema inspection: Open
db/schema.rband verifyt.text "ssn_encrypted"exists and the plaintextssncolumn is absent. - Key status (Rails 7.1+): Run
bin/rails db:encryption:status. It reports whether the three keys are configured and match credentials. - Raw value test: In a console, execute
User.first.attributes['ssn_encrypted']. The returned string should be a base64-encoded ciphertext, not the plaintext SSN.
Limitations and Operational Considerations
- Key loss is catastrophic. Losing all three keys (primary, deterministic, key-derivation salt) makes every encrypted value irrecoverable. Back up credentials with the same rigor as database backups—offline, encrypted, access-controlled.
- Deterministic encryption leaks equality. Two users with the same SSN will have identical
ssn_encryptedvalues. Do not use deterministic mode for high-entropy secrets (passwords, tokens). Usehas_secure_passwordfor passwords. - Changing determinism requires re-encryption. Switching
deterministic: truetofalse(or vice versa) after data exists means every row must be rewritten. Plan a maintenance window or use a dual-write migration pattern. - Database-level backups and logs are not encrypted by this feature. Enable infrastructure encryption-at-rest (EBS encryption, Cloud SQL encryption) and configure log filtering (e.g.,
config.filter_parameters += [:ssn]) for defense in depth. - LIKE queries on deterministic columns require Rails 7.1+ with
config.active_record.encryption.extend_queries = true. Verify your Rails version before relying on partial matches. - Performance encryption occurs in Ruby, not the database. Benchmark your workload; typical overhead is sub-millisecond per attribute. Indexes on deterministic columns increase storage proportionally to the ciphertext size.
Recovery Options
If decryption fails in production (e.g., wrong key deployed), the application will raise ActiveRecord::Encryption::Errors::Decryption. Immediate steps:
- Revert the deployment to the previous version that had the correct keys.
- Confirm the key values in credentials or environment variables match the ones used to encrypt the data.
- If keys were rotated and old keys removed, restore the previous key set in
previous_keysand redeploy.
No automated rollback exists for encrypted data itself; the only recovery is having the correct keys available.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.