0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Store Sensitive Data in Rails

    Software Engineer
    Software Engineer
    Ruby on Rails
    Ruby on Rails
    published 2026-06-08 20:05:25 +0700 ·
    5 mins read
    attr_encrypted used to be the go-to solution for encryption in Rails applications. However, Rails 7 introduced Active Record Encryption, making application-level encryption a first-class feature. Adding encryption is now as simple as:

    // language: ruby
    class User < ApplicationRecord
      encrypts :email
    end

    But adding encryption is rarely the hard part. The hard part is understanding what problem you're solving, what trade-offs you're accepting, and what information still leaks after encryption.

    This post covers what I've learned about storing sensitive data in Rails, and some of the trade-offs that aren't obvious at first glance.

    What Is Sensitive Data?

    When people hear "sensitive data", they usually think of passwords. Passwords are actually the easy case because we don't encrypt them - we hash them (with hashing, we cannot recover the original data, whereas encryption allows that)

    The harder question is:
    What data would hurt users or the business if a database dump leaked tomorrow?
    Some obvious examples:
    • Email addresses
    • Phone numbers
    • Government IDs
    • Bank accounts
    • Access tokens
    • API credentials
    But the most sensitive data is often hiding in places nobody thinks about. Examples include:
    • Internal notes
    • Customer support conversations
    • Audit logs
    Support notes can contain more personal information than the dedicated profile fields. It might look like this:
    >>> Customer called from +84..., recently moved to District 7, updated bank account information...

    None of those details belongs to a dedicated field, yet together they can reveal far more than an email address ever could. To conclude, before discussing encryption strategies, spend some time identifying what data is actually sensitive.

    Storage-level and Application-level encryption

    🔓 Our database is already encrypted 
    If you’re using AWS RDS, that’s probably true. Most cloud providers enable encryption at rest with just a few clicks. But encryption at rest only protects the physical storage. For example, it helps if:
    • Someone steals a hard drive from the data center
    • A cloud provider retires or replaces storage hardware
    • Physical infrastructure is compromised
    What it doesn’t protect against is how data is actually leaked most of the time.
    Imagine a developer accidentally uploads a production backup to the wrong S3 bucket users.sql.gz.The database is encrypted at rest while running in RDS, but the backup now contains every email address and phone number in plaintext.

    Or imagine an engineer running this query using a read-only production account:
    // language: sql
    SELECT email, phone
    FROM users;

    Encryption at rest won’t stop that either. Application-level encryption solves a different problem: even if someone gets access to the database itself, they only see ciphertext instead of the original values.

    Active Record Encryption

    Rails ships with Active Record Encryption. Setup is simple.

    Generate keys:
    // language: bash
    bin/rails db:encryption:init

    Then declare encrypted attributes:
    // language: ruby
    class User < ApplicationRecord
      encrypts :email
    end

    Rails transparently encrypts data before persisting it and decrypts it when reading it back. From the application's perspective:
    // language: ruby
    user.email
    # => "[email protected]"

    From PostgreSQL's perspective:
    // language: javascript
    {"p":"oq+RFYW8CucALxnJ6ccx","h":{"iv":"3nrJAIYcN1+YcGMQ","at":"JBsw7uB90yAyWbQ8E3krjg=="}}

    Rails uses AES-GCM under the hood. One nice touch is that encrypted fields are automatically filtered in console output and logs

    The Search Problem

    The moment you encrypt data, somebody asks: Can I still search by email?
    The answer is: It depends

    Non-deterministic Encryption

    By default, Rails uses non-deterministic encryption. Encrypting the same email twice produces different ciphertexts.  This is great for security because attackers cannot infer patterns from encrypted values. However, this query no longer works.
    // language: ruby
    User.find_by(email: "[email protected]")

    The database has no stable encrypted value to compare against.

    Deterministic Encryption

    Rails provides deterministic encryption for searchable fields.
    // language: ruby
    encrypts :email, deterministic: true

    Now the same plaintext always generates the same ciphertext.

    Problem solved? Not exactly.

    Searchable Encryption Always Leaks Something

    Imagine a table with deterministic encryption
    // language: ruby
    gmail.com => A
    gmail.com => A
    gmail.com => A
    apple.me => B
    apple.me => B
    company.com => C

    The actual values are hidden, but the patterns are not. An attacker can still see: duplicates, frequency, ...
    In many real-world datasets, frequency analysis is enough to make educated guesses about the underlying values.

    This isn't a Rails problem. This is a fundamental challenge in searchable encryption research. Multiple academic attacks have demonstrated how search patterns and access patterns can reveal information even when the underlying data remains encrypted.

    The trade-off is unavoidable:
    Queryability costs privacy.
    The moment you want efficient searches, you start leaking information. The only question is how much.

    Blind Indexes

    This is where Blind Index becomes interesting. Instead of making ciphertext searchable, we store a keyed hash alongside the encrypted value.

    A popular gem is blind_index by Ankane
    // language: ruby
    class User < ApplicationRecord
      encrypts :email
      blind_index :email
    end

    Internally:
    // language: text
    email
    ├── encrypted value
    └── blind index

    Searches happen against the blind index. Rails computes the same blind index for the search term.
    // language: ruby
    User.find_by(email: "[email protected]")

    Advantages

    • Encrypted data remains randomized: the same email encrypted twice produces different ciphertexts, making it harder to infer patterns.
    • Exact-match lookups remain fast: find_by(email: "...") can still use the blind index.
    • Database indexes still work: the blind index column can be indexed like any other database column.

    Trade-offs

    • Equality still leaks: if two users have the same email, they’ll have the same blind index.
    • Frequency analysis remains possible: attackers can still observe which values appear more often, even if they don’t know the actual values. In practice, this is less useful for high-entropy data such as email addresses, API tokens, or UUIDs, where values are typically unique or nearly unique.
    • Partial matching is hard: queries like LIKE, prefix search, or fuzzy search generally won’t work on encrypted fields.
    In production, blind indexes are generally preferred over deterministic encryption for searchable fields because they allow the underlying data to remain encrypted with randomized (non-deterministic) encryption while providing efficient exact-match lookups through a separate index. This reduces ciphertext leakage, simplifies key rotation, and separates search concerns from data encryption.

    Cryptography as a Service

    As organizations mature, encryption often moves outside the application. Examples include:
    • AWS KMS
    • Google Cloud KMS
    • HashiCorp Vault
    Instead of managing encryption directly inside Rails, the application delegates cryptographic operations to specialized systems.

    For example, Rails might ask KMS to encrypt a value:
    // language: ruby
    ciphertext = kms_client.encrypt(
      key_id: "customer-data-key",
      plaintext: email
    )

    and decrypt it when needed:
    // language: ruby
    email = kms_client.decrypt(ciphertext)

    The exact API varies by provider, but the idea is the same: the application no longer owns the encryption keys.

    Benefits

    • Centralized auditing: know who used which key and when.
    • Key rotation: rotate keys without every application managing the process.
    • Access control: restrict which services can decrypt specific data.
    • Compliance support: often required in regulated environments.

    Costs

    • Latency: encryption may require network calls.
    • Operational complexity: another system to manage and monitor.
    • Additional dependencies: your application now depends on KMS or Vault availability.
    For most Rails applications, Active Record Encryption provides an excellent balance between simplicity and security.

    Final Thoughts

    Active Record Encryption makes encryption feel simple. The API is simple, but the trade-offs are not.

    Understanding what you’re protecting, what information still leaks, and what operational costs you’re introducing is still our responsibility as engineers. Encryption isn’t a checkbox. It’s a balance between security, usability, performance, and operational complexity.