Encrypting Sensitive Rails Attributes with Active Record Encryption
Rails 7's Active Record Encryption lets you protect PII and secrets at rest while keeping queries practical. This post walks through key setup, deterministic vs. non‑deterministic modes, a concrete User model example, and the trade‑offs you must accept.
15 Aug 2026, 05:04 UTC

The problem: plaintext PII in the database
Storing email addresses, social‑security numbers, or API tokens in clear text makes a data breach trivial. Rails 7 introduced ActiveRecord::Encryption, an application‑level layer that encrypts designated model attributes before they hit the database and decrypts them transparently on read. The feature works with existing migrations, fixtures, and dirty‑tracking, but it forces a few architectural decisions you should understand before you enable it.
How the encryption layer works
Key hierarchy
Three secrets drive the system (all stored in config/credentials.yml.enc or a secret manager):
- primary_key – encrypts non‑deterministic attributes.
- deterministic_key – encrypts attributes that must be queryable.
- key_derivation_salt – derives per‑attribute keys from the two keys above.
Rotation is supported via a custom KeyProvider that returns an array of decryption keys; new records are always encrypted with the first key.
Deterministic vs. non‑deterministic
Non‑deterministic encryption (the default) uses a fresh IV per write, so ciphertexts never repeat. This gives strong indistinguishability but prevents WHERE clauses on the column. Deterministic encryption reuses a derived IV, enabling equality lookups (find_by, where) at the cost of leaking equality patterns — acceptable for low‑cardinality lookup fields like an email used for login, but unsafe for high‑entropy secrets such as API tokens.
Worked example: a User model with email and SSN
Assume a fresh Rails 7.1+ app. Run the following commands from the project root (requires write access to the repo and the ability to edit credentials):
# 1. Generate model and migration
rails g model User email:string ssn:string
rails db:migrate
# 2. Declare encryption in the model
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # queryable
encrypts :ssn # non‑deterministic, not queryable
end
# 3. Add the three keys to credentials (run once per environment)
rails credentials:edit
# Insert:
# active_record_encryption:
# primary_key: ""
# deterministic_key: ""
# key_derivation_salt: ""
# 4. Verify in console (requires Rails console permission)
rails console
> u = User.create!(email: '[contact removed]', ssn: '123-45-6789')
> User.find_by(email: '[contact removed]') # => returns u
> User.find_by(ssn: '123-45-6789') # raises ActiveRecord::Encryption::Errors::ForbiddenQuery
> User.connection.select_value("SELECT ssn FROM users")
# => binary gibberish, not plaintext
The encrypts macro handles casting; the schema shows email and ssn as binary (or text depending on DB). db/schema.rb does not record the encryption metadata — the model is the source of truth.
Trade‑offs and limitations
- Key loss = data loss. If the primary key (or all keys in a provider) disappears, encrypted columns become unrecoverable. Store keys in a secret manager (AWS KMS, HashiCorp Vault) and back them up separately from the database.
- Deterministic encryption leaks equality. Two users with the same email produce identical ciphertexts. Do not use it for high‑cardinality secrets.
- Indexes on deterministic columns store ciphertext. Unique constraints enforce ciphertext uniqueness, which matches plaintext uniqueness only while the same key/salt pair is used.
- In‑memory, log, and network exposure remain. Combine with TLS,
config.filter_parameters, and application‑level access controls. - Rails 7.0–7.1 bug with encrypted enums. Fixed in 7.1.3+. Verify your version if you combine
enumandencrypts.
Actionable next steps
- Identify every column that holds PII, tokens, or secrets.
- Classify each as queryable (deterministic) or non‑queryable (default).
- Generate strong base64 keys (32 bytes each) and add them to your secret store.
- Add
encryptsdeclarations to the corresponding models. - Run the console verification steps above; confirm ciphertext appears in the DB and deterministic lookups work.
- Implement a
KeyProviderif you anticipate rotation; test by deploying a provider that returns[old_key, new_key]and ensure existing records still decrypt. - Document the key‑management procedure for the team and add a run‑book for key loss scenarios.
With these steps you get encryption at rest without sacrificing the ability to query the fields you actually need to search.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.