More at rubyonrails.org:

Active Record Encryption exists to protect sensitive information in your application, such as personally identifiable information (PII) about your users. Active Record supports application-level encryption by allowing you to declare which attributes should be encrypted. It enables transparent encryption and decryption of attributes when saving and retrieving data.

1. Why Encrypt Data at the Application Level?

Encrypting specific attributes at the application level adds an additional security layer. For example, if someone gains access to your application logs or database backup, the encrypted data remains unreadable. It also helps avoid accidental exposure of sensitive information in your application console or logs.

Most importantly, this feature lets you explicitly define what data is sensitive in your code. This enables precise access control throughout your application and any connected services. For example, you can use tools like console1984 to restrict decrypted data access in the Rails console. You can also take advantage of automatic parameter filtering for encrypted fields.

2. Setup

To start using Active Record Encryption, you need to generate keys and declare attributes you want to encrypt in the model.

2.1. Generate Encryption Key

You can generate a random key set by running bin/rails db:encryption:init:

$ bin/rails db:encryption:init
Add this entry to the credentials of the target environment:

active_record_encryption:
  primary_key: YehXdfzxVKpoLvKseJMJIEGs2JxerkB8
  deterministic_key: uhtk2DYS80OweAPnMLtrV2FhYIXaceAy
  key_derivation_salt: g7Q66StqUQDQk9SJ81sWbYZXgiRogBwS

These values can be stored by copying and pasting the generated values into your existing Rails credentials file using bin/rails credentials:edit.

Alternatively, the encryption keys can also be configured from other sources, such as environment variables:

# 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"]

It's recommended to use Rails built-in credentials support to store keys. If you set them manually via configuration properties, make sure you don't commit them with your code (e.g. use environment variables).

The generated values are 32 bytes in length. If you generate these yourself, the recommended minimum length is 12 bytes for the primary key and 20 bytes for the salt.

Once the keys are generated and stored, you can start using Active Record Encryption by declaring attributes to be encrypted in the model.

2.2. Declare Encrypted Attributes

The encrypts method defines the attributes to be encrypted at the model level. These are regular Active Record attributes backed by a column with the same name.

class Article < ApplicationRecord
  encrypts :title
end

Active Record Encryption will transparently encrypt these attributes before saving them to the database and will decrypt them upon retrieval. For example:

article = Article.create(title: "Encrypt it all!")
article.title # => "Encrypt it all!"

However, in the Rails console, the executed SQL looks like this:

INSERT INTO "articles" ("title", "created_at", "updated_at")
VALUES ('{"p":"oq+RFYW8CucALxnJ6ccx","h":{"iv":"3nrJAIYcN1+YcGMQ","at":"JBsw7uB90yAyWbQ8E3krjg=="}}', ...) RETURNING "id"

The value inserted is a JSON object that contains the encrypted value for the title attribute. More specifically, the JSON object stores two keys: p for payload and h for headers. The ciphertext, which is compressed and encoded in Base64, is stored as the payload. The h key stores metadata needed to decrypt the value. The iv value is the initialization vector and at is authentication tag (used to ensure the ciphertext has not been tampered with).

When looking at the Article in the Rails console, the encrypted attribute title will also be filtered:

my-app(dev)> Article.first
  Article Load (0.1ms)  SELECT "articles".* FROM "articles" ORDER BY "articles"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Article:0x00007f83fd9533b8
    id: 1,
    title: "[FILTERED]",
    created_at: Fri, 12 Sep 2025 16:57:45.753372000 UTC +00:00,
    updated_at: Fri, 12 Sep 2025 16:57:45.753372000 UTC +00:00>

Encryption requires extra storage space because the encrypted value will be larger than the original value. This overhead is negligible at larger sizes. Active Record Encryption has compression enabled by default, which can offer up to 30% storage savings over the unencrypted version for larger payloads.

2.3. Important: Storage Considerations

Encrypted data takes more storage because Active Record Encryption stores additional metadata alongside the encrypted payload, and the payload itself is Base64-encoded so it can fit safely in text-based columns.

When using the built-in envelope encryption key provider, you can estimate the worst-case overhead to be around 255 bytes. This overhead is negligible for larger sizes. Encryption also uses compression by default, which can offer up to 30% storage savings over the unencrypted version for larger payloads.

When using string columns, it’s important to know that modern databases define the column size in terms of number of characters, not bytes. With encodings like UTF-8, a single character can take up to four bytes. This means that a column defined to hold N characters may actually consume up to 4 × N bytes in storage.

Since an encrypted payload is binary data serialized with Base64, it can be stored in regular a string column. Because it's a sequence of ASCII bytes, an encrypted column can take up to four times its clear version size. So, even if the bytes stored in the database are the same, the column must be four times bigger.

In practice, this means:

  • When encrypting short texts written in Western alphabets (mostly ASCII characters), you should account for that 255 additional overhead when defining the column size.
  • When encrypting short texts written in non-Western alphabets, such as Cyrillic, you should multiply the column size by 4. Notice that the storage overhead is 255 bytes at most.
  • When encrypting long texts, you can ignore column size concerns.

For example:

Content to encrypt Original column size Recommended encrypted column size Storage overhead (worst case)
Email addresses string(255) string(510) 255 bytes
Short sequence of emojis string(255) string(1020) 255 bytes
Summary of texts written in non-western alphabets string(500) string(2000) 255 bytes
Arbitrary long text text text negligible

3. Basic Usage

3.1. Querying Encrypted Data: Deterministic vs. Non-deterministic Encryption

By default, Active Record Encryption is non-deterministic, which means that encrypting the same value with the same key twice will result in different encrypted values (aka ciphertexts). The non-deterministic approach improves security by making crypto-analysis of ciphertexts harder. However, it also means that queries (such as WHERE title = "Encrypt it all!") on encrypted values are not possible, since the same plaintext value can result in a different encrypted value that does not match the encrypted value previously stored in the JSON document.

You can use deterministic encryption if you need to query using encrypted values. For example, the email field on the Author model below:

class Author < ApplicationRecord
  encrypts :email, deterministic: true
end

# You can only query by email if using deterministic encryption.
Author.find_by_email("tolkien@email.com")

The :deterministic option generates initialization vectors in a deterministic way, meaning it will produce the same encrypted output given the same plaintext input value. This makes querying encrypted attributes possible by string equality comparison. For example, notice that the p and iv key in the JSON document have the same value when we create and when we query an Author's email:

my-app(dev)> author = Author.create(name: "J.R.R. Tolkien", email: "tolkien@email.com")
  TRANSACTION (0.1ms)  begin transaction
  Author Create (0.4ms)  INSERT INTO "authors" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) RETURNING "id"  [["name", "J.R.R. Tolkien"], ["email", "{\"p\":\"8BAc8dGXqxksThLNmKmbWG8=\",\"h\":{\"iv\":\"NgqthINGlvoN+fhP\",\"at\":\"1uVTEDmQmPfpi1ULT9Nznw==\"}}"], ["created_at", "2025-09-19 18:08:40.104634"], ["updated_at", "2025-09-19 18:08:40.104634"]]
  TRANSACTION (0.1ms)  commit transaction

my-app(dev)> Author.find_by_email("tolkien@email.com")
  Author Load (0.1ms)  SELECT "authors".* FROM "authors" WHERE "authors"."email" = ? LIMIT ?  [["email", "{\"p\":\"8BAc8dGXqxksThLNmKmbWG8=\",\"h\":{\"iv\":\"NgqthINGlvoN+fhP\",\"at\":\"1uVTEDmQmPfpi1ULT9Nznw==\"}}"], ["LIMIT", 1]]
=> #<Author:0x00007f8a396289d0
    id: 3,
    name: "J.R.R. Tolkien",
    email: "[FILTERED]",
    created_at: Fri, 19 Sep 2025 18:08:40.104634000 UTC +00:00,
    updated_at: Fri, 19 Sep 2025 18:08:40.104634000 UTC +00:00>

In the above example, the initialization vector, iv, has the value "NgqthINGlvoN+fhP" for the same string. Even if you use the same email string in a different model instance (or different attribute with deterministic encryption), it will map to the same p and iv values:

my-app(dev)> author2 = Author.create(name: "Different Author", email: "tolkien@email.com")
  TRANSACTION (0.1ms)  begin transaction
  Author Create (0.4ms)  INSERT INTO "authors" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) RETURNING "id"  [["name", "Different Author"], ["email", "{\"p\":\"8BAc8dGXqxksThLNmKmbWG8=\",\"h\":{\"iv\":\"NgqthINGlvoN+fhP\",\"at\":\"1uVTEDmQmPfpi1ULT9Nznw==\"}}"], ["created_at", "2025-09-19 18:20:11.291969"], ["updated_at", "2025-09-19 18:20:11.291969"]]
  TRANSACTION (0.1ms)  commit transaction

The :deterministic option allows for querying by trading off lesser security. The data is still encrypted but the determinism makes crypto-analysis easier. For this reason, non-deterministic encryption is recommended for all data unless you need to query by the encrypted attribute.

In non-deterministic mode, Active Record uses AES-GCM with a 256-bits key and a random initialization vector. In deterministic mode, it also uses AES-GCM, but the initialization vector is not random. It is generated as a function of the key and the plaintext content (HMAC-SHA-256 digest of the two).

If you do not define a deterministic_key, then you have effectively disabled deterministic encryption.

3.2. Ignoring Case

You might want to ignore the case when querying deterministically encrypted data. There are two options for achieving this - :downcase and :ignore_case.

When you use the :downcase option when declaring the encrypted attribute, it converts the data to downcase before encryption occurs. This allows to effectively ignore case when querying data.

class Person
  encrypts :email_address, deterministic: true, downcase: true
end

When using :downcase, the original case is lost.

You can use the :ignore_case option when you want to preserve the original case for displaying but ignore the case when querying data:

class Label
  encrypts :name, deterministic: true, ignore_case: true # the encrypted content with the original case will be stored in the column `original_name`
end

With the :ignore_case option, you need to add a new column named original_<column_name> to store the encrypted content with the case unchanged. When reading the name attribute, Rails will serve the version with the original case. When querying name, it will ignore case.

3.3. Serialized Attributes

By default, Active Record Encryption will serialize values using the underlying type before encrypting them as long as the value is serializable as Strings. If the underlying type is not serializable as a String, you can use a custom message_serializer:

class Article < ApplicationRecord
  encrypts :metadata, message_serializer: SomeCustomMessageSerializer.new
end

Attributes with structured types using the serialized method can be encrypted as well. The serialized method is used when you have an attribute that needs to be saved to the database as a serialized object (using YAML, JSON or such), and retrieved by deserializing into the same object.

When using serialized attributes for custom types, the declaration of the serialized attribute should go before the encryption declaration:

# CORRECT
class Article < ApplicationRecord
  serialize :title, type: Title
  encrypts :title
end

# INCORRECT
class Article < ApplicationRecord
  encrypts :title
  serialize :title, type: Title
end

3.4. Ensuring Uniqueness with Encrypted Data

Checking for uniqueness is only supported with deterministically encrypted data.

3.4.1. Unique Validations

If an attribute is deterministically encrypted, a uniqueness validation can be specified normally, along with encryption:

class Person
  validates :email_address, uniqueness: true
  encrypts :email_address, deterministic: true, downcase: true
end

If you want to ignore the case for uniqueness, make sure to use the :downcase or :ignore_case option in the encrypts declaration. Using the :case_sensitive option in the validation won't work.

If you have a mix of unencrypted and encrypted data or if you have data that is encrypted using two different sets of keys/schemes, you'll need to enable extended queries with config.active_record.encryption.extend_queries = true in order to support unique validations.

3.4.2. Unique Indexes

In order to support unique indexes on deterministically encrypted attributes, it’s important to ensure that a given plaintext always produces the same ciphertext. This consistency is what makes indexing and querying possible.

class Person
  encrypts :email_address, deterministic: true
end

In order for unique indexes to work, you will have to ensure that the encryption properties for the underlying attributes don't change.

3.5. Filtering Params Named as Encrypted Attributes

Encrypted attributes are configured to be automatically filtered out of the Rails logs. So sensitive information, like encrypted emails or credit card numbers, isn't stored in your logs. For example, if you are filtering the email field, you will see something like this in the logs: Parameters: {"email"=>"[FILTERED]", ...}.

In case you need to disable filtering of encrypted parameters, you can use the following configuration:

# config/application.rb
config.active_record.encryption.add_to_filter_parameters = false

When filtering is enabled, if you want to exclude specific attributes from automatic filtering, you can use this configuration:

config.active_record.encryption.excluded_from_filter_parameters = [:catchphrase]

When generating the filter parameter, Rails will use the model name as a prefix. E.g: For User#email, the filter parameter will be user.email.

3.6. Action Text

You can encrypt Action Text attributes by passing encrypted: true in their declaration.

class Message < ApplicationRecord
  has_rich_text :content, encrypted: true
end

Passing individual encryption options to Action Text attributes is not supported. It will use non-deterministic encryption with the global encryption options configured.

3.7. Fixtures

To allow your tests can use plain text values in the YAML fixture files for encrypted attributes, you can configure fixtures to be automatically encrypted by adding this configuration to your config/environments/test.rb file:

Rails.application.configure do
  config.active_record.encryption.encrypt_fixtures = true
  # ...
end

Without this setting, Rails would load fixture values as is. This wouldn't work for encrypted attributes and Active Record Encryption expects a JSON value in that column. However, when encrypt_fixtures is enabled, all the encryptable attributes will be automatically encrypted and also seamlessly decrypted, according to the encryption settings defined in the model.

3.7.1. Action Text Fixtures

To encrypt Action Text fixtures, you can place them in fixtures/action_text/encrypted_rich_texts.yml.

3.8. Encoding

When encrypting strings non-deterministically, their original encoding is preserved automatically.

For deterministic encryption, Rails stores the string encoding alongside the ciphertext. However, to ensure consistent encryption output, especially for querying or enforcing uniqueness, the library forces UTF-8 encoding by default. This avoids producing different ciphertexts for identical strings with different encodings.

You can customize this behavior. To change the default forced encoding:

config.active_record.encryption.forced_encoding_for_deterministic_encryption = Encoding::US_ASCII

To disable forced encoding and preserve the original encoding in all cases:

config.active_record.encryption.forced_encoding_for_deterministic_encryption = nil

3.9. Compression

Active Record Encryption enables compression of encrypted payloads by default. This can save up to 30% of the storage space for larger payloads.

Compression is enabled by default but not applied to all payloads. It is based on a size threshold (such as 140 bytes), which is used as a heuristic to determine if compression is "worth it".

You can disable compression by setting the compress option to false when encrypting attributes:

class Article < ApplicationRecord
  encrypts :content, compress: false
end

You can also configure the algorithm used for the compression. The default compressor is Zlib. You can implement your own compressor by creating a class or module that responds to deflate and inflate methods. For example:

require "zstd-ruby"

module ZstdCompressor
  def self.deflate(data)
    Zstd.compress(data)
  end

  def self.inflate(data)
    Zstd.decompress(data)
  end
end

class User
  encrypts :name, compressor: ZstdCompressor
end

You can also configure the desired compression method globally:

config.active_record.encryption.compressor = ZstdCompressor

3.10. Using the API

Active Record Encryption is meant to be used declaratively, but there is also an API for debugging or advanced use cases.

You can encrypt and decrypt all relevant attributes of an article model like this:

article.encrypt # encrypt or re-encrypt all the encryptable attributes
article.decrypt # decrypt all the encryptable attributes

You can check whether a given attribute is encrypted:

article.encrypted_attribute?(:title)

You can read the ciphertext for an attribute:

article.ciphertext_for(:title)

4. Migrating Existing Data

4.1. Support for Unencrypted Data

To ease the transition from unencrypted to encrypted attributes in your Rails application, you can enable support for unencrypted data with:

config.active_record.encryption.support_unencrypted_data = true

When enabled:

  • Reading attributes that are still unencrypted will succeed without raising errors.

  • Queries on deterministically encrypted attributes can match both encrypted and cleartext values, if you also enable extended_queries:

config.active_record.encryption.extend_queries = true

This setup is intended only for migration periods during which both encrypted and unencrypted data need to coexist in your application. Both options default to false, which is the recommended long-term configuration to ensure data is fully encrypted and enforced.

4.2. Support for Previous Encryption Schemes

Changing encryption properties of attributes can break existing data. For example, imagine you want to make a deterministic attribute non-deterministic. If you change the declaration in the model, reading existing ciphertexts will fail because the encryption method is different now.

To support these situations, you can specify previous encryption schemes to be used globally or on a per-attribute basis.

Once you configure the previous scheme, the following will be supported:

  • When reading encrypted data, Active Record Encryption will try previous encryption schemes if the current scheme doesn't work.

  • When querying deterministic data, it will add ciphertexts using previous schemes so that queries work seamlessly with data encrypted with different schemes.

You need to enable extended_queries configuration for this to work:

config.active_record.encryption.extend_queries = true

Next, let's see how to configure previous encryption schemes.

4.2.1. Global Previous Encryption Schemes

You can add previous encryption schemes by adding them as a list of properties using the previous config property in your config/application.rb:

config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]

4.2.2. Per-attribute Previous Encryption Schemes

Use the previous option when declaring the encrypted attribute:

class Article
  encrypts :title, deterministic: true, previous: { deterministic: false }
end

4.2.3. Encryption Schemes and Determinism

With deterministic encryption, you typically want ciphertexts to remain constant. So when changing encryption schemes, non-deterministic and deterministic encryption behave differently.

  • With non-deterministic encryption, new information will always be encrypted with the newest (current) encryption scheme.

  • With deterministic encryption, new information will be encrypted with the oldest encryption scheme by default.

It is possible to change this behavior for deterministic encryption to use the newest encryption scheme for encrypting new data like this:

class Article
  encrypts :title, deterministic: { fixed: false }
end

5. Encryption Contexts

An encryption context defines the encryption components that are used at a given moment. There is a default encryption context based on your global configuration, but you can also configure a custom context for a given attribute or when running a specific block of code.

Encryption contexts are a flexible but advanced configuration mechanism. Most users would not need to use them.

The main components of encryption contexts are:

  • encryptor: exposes the internal API for encrypting and decrypting data. It interacts with a key_provider to build encrypted messages and deal with their serialization. The encryption/decryption itself is done by the cipher and the serialization by message_serializer.
  • cipher: the encryption algorithm itself (AES 256 GCM).
  • key_provider: serves encryption and decryption keys.
  • message_serializer: serializes and deserializes encrypted payloads.

If you decide to build your own message_serializer, it's important to use safe mechanisms that can't deserialize arbitrary objects. A commonly supported scenario is encrypting existing unencrypted data. An attacker can leverage this to enter a tampered payload before encryption takes place and perform RCE attacks. This means custom serializers should avoid Marshal, YAML.load (use YAML.safe_load instead), or JSON.load (use JSON.parse instead).

5.1. Built-In Encryption Context

The global encryption context is the one used by default and is configured with other configuration properties in your config/application.rb or environment config files.

config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new
config.active_record.encryption.encryptor = MyEncryptor.new

You can use with_encryption_context to override any of the properties of the encryption context.

5.2. Encryption Context with a Block of Code

You can set an encryption context for a given block of code using with_encryption_context:

ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
  # ...
end

5.3. Per-attribute Encryption Contexts

You can override encryption context configuration by passing options in the attribute declaration:

class Attribute
  encrypts :title, encryptor: MyAttributeEncryptor.new
end

5.4. Encryption Context to Disable Encryption

You can run code without encryption:

ActiveRecord::Encryption.without_encryption do
  # ...
end

This means that reading encrypted text will return the ciphertext, and saved content will be stored unencrypted.

5.5. Encryption Context to Protect Encrypted Data

You can run code in a block without encryption but prevent overwriting encrypted content:

ActiveRecord::Encryption.protecting_encrypted_data do
  # ...
end

This can be handy if you want to protect encrypted data while running arbitrary code against it (e.g. in a Rails console).

6. Key Management

Key providers implement key management strategies. You can configure key providers globally or on a per-attribute basis.

6.1. Built-in Key Providers

6.1.1. DerivedSecretKeyProvider

The DerivedSecretKeyProvider serves keys derived from the provided passwords using PBKDF2. This is the key provider configured by default.

config.active_record.encryption.key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(["some passwords", "to derive keys from. ", "These should be in", "credentials"])

By default, active_record.encryption configures a DerivedSecretKeyProvider with the keys defined in active_record.encryption.primary_key.

6.1.2. EnvelopeEncryptionKeyProvider

The EnvelopeEncryptionKeyProvider implements a simple envelope encryption strategy, where the data is encrypted with a key, which in turn is also encrypted.

The EnvelopeEncryptionKeyProvider generates a random key for each data encryption operation. It stores the data-key with the data itself. Then, the data-key is also encrypted with a primary key defined in the credential active_record.encryption.primary_key.

You can configure Active Record to use this key provider by adding this to your config/application.rb:

config.active_record.encryption.key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new

As with other built-in key providers, you can provide a list of primary keys in active_record.encryption.primary_key to implement key-rotation schemes.

6.2. Custom Key Providers

For more advanced key-management schemes, you can configure a custom key provider in an initializer:

ActiveRecord::Encryption.key_provider = MyKeyProvider.new

A key provider must implement this interface:

class MyKeyProvider
  def encryption_key
  end

  def decryption_keys(encrypted_message)
  end
end

Both methods return ActiveRecord::Encryption::Key objects:

  • encryption_key returns the key used for encrypting some content
  • decryption_keys returns a list of potential keys for decrypting a given ciphertext.

A key can include arbitrary tags that will be stored unencrypted with the message. You can use ActiveRecord::Encryption::Message#headers to examine those values when decrypting.

6.3. Attribute-specific Key Providers

You can configure a key provider on a per-attribute basis with the key_provider option. For example, assuming you have defined a custom key provider called ArticleKeyProvider:

class Article < ApplicationRecord
  encrypts :summary, key_provider: ArticleKeyProvider.new
end

6.4. Attribute-specific Keys

You can configure a specific key for a given attribute using the key option:

class Article < ApplicationRecord
  encrypts :summary, key: ENV["SOME_SECRET_KEY_FOR_ARTICLE_SUMMARIES"]
end

Active Record will use the key passed to encrypts to encrypt and decrypt the summary attribute above.

6.5. Rotating Keys

Active Record Encryption can work with lists of keys to support implementing key rotation schemes. The reason to rotate keys may be as part of your organization's security policy or if you suspect a key may be compromised.

In the example below, the last key is used for encrypting new content and all keys are tried when decrypting content until one works.

active_record_encryption:
  primary_key:
    - a1cc4d7b9f420e40a337b9e68c5ecec6 # Previous keys can still decrypt existing content
    - bc17e7b413fd4720716a7633027f8cc4 # Active, encrypts new content
  key_derivation_salt: a3226b97b3b2f8372d1fc6d497a0c0d3

This enables key rotation workflow where you keep a short list of keys by adding new keys, re-encrypting content, and deleting old keys.

Rotating keys is not supported for deterministic encryption.

6.6. Storing Key References

You can store a reference to the encryption key in the encrypted message itself. The advantage of doing this is that decryption can be more performant as the system does not have to try a list of keys to find one that works. The tradeoff is that the encryption data will be a bit larger.

In order to store a key reference, you need to enable this configuration:

config.active_record.encryption.store_key_references = true


Back to top