Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Ruby on Rails  Module 10 – Securing Rails applications

Credentials, secrets and Active Record encryption

Keep Rails 8 credentials and secret_key_base safe, rotate keys without logging anyone out, and encrypt sensitive columns you can still query by.

  • Intermediate
  • 15 minutes
  • Examples run with Ruby 4.0.7 and rails 8.1.4
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: Where a Rails 8 app keeps its secrets · What signs cookies and what the log filter hides. With a pass: 2 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Manage encrypted credentials per environment and keep the credentials key out of git and container images
  • Read the Rails 8.1 parameter filter and tell what a log hides from the rest
  • Rotate secret_key_base and encryption keys without logging anyone out
  • Pick deterministic or non-deterministic encryption per column and defend the choice
  • Encrypt sensitive columns and still query the ones that need it

Before you start

What you’ll learn · Before you start

What you will learn

  • Manage encrypted credentials per environment and keep the credentials key out of git and container images
  • Read the Rails 8.1 parameter filter and tell what a log hides from the rest
  • Rotate secret_key_base and encryption keys without logging anyone out
  • Pick deterministic or non-deterministic encryption per column and defend the choice
  • Encrypt sensitive columns and still query the ones that need it

Before you start

On this page

A Kaksha engineer who commits the app’s credentials key file to a public repository has leaked every secret the app holds: the key that signs and encrypts every cookie, the payment token, the SMTP password. A different leak is quieter: an attacker who copies 50,000 rows of the guardians table from a backup walks away with 50,000 plain phone numbers, and the column cannot be queried if it was encrypted the wrong way.

A Rails 8.1 app ships three defences for them. Rails credentials keep the API keys and passwords the app needs in one encrypted file. secret_key_base signs and encrypts what the running app hands to a browser. Active Record encryption encrypts chosen columns, so a copied backup gives nothing away. Each has a failure mode and a fix.

Where a Rails 8 app keeps its secrets

Rails credentials are one encrypted file and one key file. bin/rails credentials:edit writes config/credentials.yml.enc, which is encrypted and checked into git, and config/master.key, which holds the key that decrypts it and is listed in .gitignore. The encrypted credentials: The config/credentials.yml.enc file Rails ships with: a YAML file encrypted with a key read from a git-ignored key file or the RAILS_MASTER_KEY variable, holding secret_key_base and the app's API keys. file is useless on its own, and the credentials key: The key that unlocks a Rails app's encrypted credentials: kept in a git-ignored key file on a developer laptop, or in the RAILS_MASTER_KEY environment variable on a server. file is useless on its own; the pair is what matters. A deploy almost never puts the key file on the server: it sets RAILS_MASTER_KEY as an environment variable instead, so a container image can be identical for development, staging and production.

Interactive · message by message How Rails reads credentials.yml.enc at boot Step through what happens when the app starts on a laptop and on a server, and watch where the key comes from.
How Rails reads credentials.yml.enc at boot, step 4 of 4. Messages so far: The host to Rails boot: two files; The host to Rails boot: RAILS_MASTER_KEY; Rails boot to Your code: decrypted values; Your code to Rails boot: credentials.stripe.The hosttwo filesRails bootdecryptedYour codeone stringtwo filesRAILS_MASTER_KEYdecrypted valuescredentials.stripe

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two files on disk. config/credentials.yml.enc is encrypted and checked into git. config/master.key holds the key that decrypts it and is listed in .gitignore. Either half is useless without the other.
  2. A server reads a variable. A deployed app rarely has the key file. The deploy sets RAILS_MASTER_KEY as an environment variable instead, so one container image serves every environment and carries no key.
  3. Rails decrypts the file. When the app starts, Rails picks the file of the current environment, falling back to the shared one, decrypts it with the key it found and keeps the values in memory. No database or network is involved.
  4. Code reads one value. Your code asks for one value, such as Rails.application.credentials.stripe[:secret_key]. Nothing on disk changes, and the decrypted file is never written back.

Per-environment credentials go in config/credentials/production.yml.enc next to its own production.key, and when that file exists production reads it in place of the shared one. The 8.1 command bin/rails credentials:fetch stripe.secret_key prints one value for a deploy script, so a shell command never opens the whole file; the generated .kamal/secrets shows it in a commented example.

Checking the key file in by accident

git add . picks up config/master.key unless the generated .gitignore is intact. If it has already landed in a commit, rotate every secret the file held and the key itself, then remove the file in a new commit; a history rewrite does not withdraw copies that have left the server.

What signs cookies and what the log filter hides

secret_key_base is one credential, and the app derives its keys from it with ActiveSupport::KeyGenerator and a salt per use: one for signed cookies, one for encrypted cookies (the session cookie is one of them), and one for each named message verifier, such as the one behind generates_token_for tokens. Changing secret_key_base: The credential Rails derives cookie-signing, cookie-encryption and message-signing keys from, with ActiveSupport::KeyGenerator; one value for the whole app, held in encrypted credentials. therefore signs every user out, and the security guide adds that encrypted cookies, signed cookies and Active Storage files may be affected too, unless the old value is registered as a rotation.

The second place a secret shows up is a smaller file. config/initializers/filter_parameter_logging.rb carries the filter_parameters: The Rails config list whose entries are substring-matched against parameter keys so matching values become [FILTERED] in logs; the generator writes a list that starts with passw, email, secret, token. list:

What the generated filter list hides Ruby · filter_parameters_demo.rb
# Shows what the generated config/initializers/filter_parameter_logging.rb hides from Rails logs.
# The filter list below is the one Rails 8.1.4 writes into a new app (checked by reading the generator template
# in the fixture app). filter_parameters is a key filter, not a value filter: a secret pasted into a comment field
# called notes is not masked.
require "active_support"
require "active_support/parameter_filter"

GENERATED = %i[passw email secret token _key crypt salt certificate otp ssn cvv cvc]
filter = ActiveSupport::ParameterFilter.new(GENERATED)

payload = {
  "user" => {
    "email_address" => "asha@example.com",
    "password" => "the-password",            # fictional, matches passw
    "password_confirmation" => "the-password",
    "pan_card" => "ABCDE1234F",              # NOT masked: no key matches
    "address" => "12 Rajpur Road, Dehradun",
  },
  "api_token" => "the-token",                # fictional, matches token
  "notes" => "wrote the password down in a notebook", # NOT masked: filter_parameters reads KEYS, not values
  "amount" => 500,
}
puts "filter list: #{GENERATED.inspect}"
puts "filtered:    #{filter.filter(payload).inspect}"

Output

filter list: [:passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc]
filtered:    {"user" => {"email_address" => "[FILTERED]", "password" => "[FILTERED]", "password_confirmation" => "[FILTERED]", "pan_card" => "ABCDE1234F", "address" => "12 Rajpur Road, Dehradun"}, "api_token" => "[FILTERED]", "notes" => "wrote the password down in a notebook", "amount" => 500}

Recorded with ruby 4.0.7 (revision 229531a6cf) +PRISM [arm64-darwin23] on macOS 26 arm64. To run it yourself: ruby filter_parameters_demo.rb

The run shows two facts. The list is a substring match on keys, so password, password_confirmation, email_address and api_token all become [FILTERED]. And it is a key filter, not a value filter: a password written into a notes field reaches the log unchanged. Add any key your app writes that holds a secret, and keep secrets out of free-text fields.

Two production questions follow: rotating secret_key_base without logging everyone out, and encrypting a column you still need to query. Active Record encryption gives each column one of two shapes. A deterministic column stores the same ciphertext for the same value, so find_by can still find a row by it. A non-deterministic column, the default, stores a new ciphertext on every write. Before a push, a secret scanner checks pasted code or logs for keys and tokens, in the browser.

Key idea

Every cookie key comes from secret_key_base, so changing it signs everyone out unless the old value stays registered, and the log filter reads key names, so a secret typed into a free-text field still reaches the log.

Checkpoint

Check: Encrypted credentials and the credentials key · The parameter filter in logs

  1. Question 1 of 2 config/credentials.yml.enc lives in git and config/master.key does not. Which single statement is true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The pair is what matters: the encrypted file is useless without the key, and the key is useless without the file

    Rails credentials are a two-file design on purpose: the encrypted file can live in git, and the key is read from an environment variable on the server. Lose the key and the file cannot be read again; leak the key and every version of the file that git holds can be read.

  2. Question 2 of 2 In the recorded run of filter_parameters_demo.rb, the notes field held "wrote the password down in a notebook" and reached the output unchanged. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: filter_parameters matches keys, not values; the key "notes" is not in the list

    The generated filter list is a list of key patterns. Add every key your app uses for a secret, and keep secrets out of free-text fields: no key pattern can catch a password typed into notes.

Rotate secret_key_base and encryption keys with no sign-out Pro: opens with a Pro, Premium or Ultimate pass

A rotation of secret_key_base has a sharp edge: every session cookie, signed cookie, encrypted cookie and signed message in flight was made with the old key, and the new app no longer holds it. Without a rotation registered, every one of them reads as nil the moment the new build is deployed, which signs every browser out at once. The fix is to teach the new app the old key, let the two coexist for a window, and remove the old one when it has emptied.

In this part:

  • What rotating the signing key invalidates
  • Where the rotation goes in an app, and the boot order that hides a mistake
  • The rotation runbook

1 diagram · 2 runnable examples (Ruby) · about 9 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Encrypt columns you still need to find rows by Pro: opens with a Pro, Premium or Ultimate pass

An encrypted column protects the rows a database leak would hand over in plain text. Rails’ Active Record Encryption writes a small JSON envelope into the column instead of the plain value, and the running app decrypts it on read. The decision per column is between a shape that lets find_by keep working, and a shape that hides whether two rows hold the same value.

In this part:

  • What encrypts :phone actually writes
  • Which shape to pick, column by column
  • Rotation of the encryption keys themselves

3 runnable examples (Ruby, YAML) · about 11 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: read the ciphertext before writing more code

The questions replay the credentials layout and the filter run: which two files a deploy needs, which file production reads when there are two, and which keys the generated filter hides and which it lets through.

Practice that adapts to you

Practise this lesson

6 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 Which two files does bin/rails credentials:edit write for a new Rails 8.1 app?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: config/credentials.yml.enc and config/master.key

    The pair is config/credentials.yml.enc, in git, and the key file beside it, kept out of git. Per-environment credentials add config/credentials/production.yml.enc and its production.key in a folder of their own.

  2. Question 2 of 6 Why does a deploy almost always use RAILS_MASTER_KEY rather than shipping the key file in the container image?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: One image can serve every environment, and the key stays out of it

    Reading the key from RAILS_MASTER_KEY keeps the key out of the image and out of every registry the image is pushed to, and lets one image run in every environment.

  3. Question 3 of 6 A production server has config/credentials/production.yml.enc with its production.key, and the shared config/credentials.yml.enc. Which file does production read?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: production.yml.enc, because the per-environment file is used whenever it exists

    With a per-environment file present, production reads only that file, so every value production needs has to be in it.

  4. Question 4 of 6 Which of these entries does the Rails 8.1 generator write into config/initializers/filter_parameter_logging.rb?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: :passw

    The generator writes passw, email, secret, token, _key, crypt, salt, certificate, otp, ssn, cvv and cvc. ParameterFilter compares each key with every entry as a substring.

  5. Question 5 of 6 The generated filter list contains :passw. Which of these keys does that entry match? Choose all that apply.

    Choose every answer that is right.

    Show the answer: Question 5 of 6

    Answer:

    • password
    • password_confirmation

    An entry matches a key that contains it as one unbroken substring, in any case. In the run, password and password_confirmation matched passw; email_address and api_token were masked by their own entries.

  6. Question 6 of 6 The run printed pan_card: "ABCDE1234F" in full. Which change keeps a PAN out of the log from now on?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: Add :pan_card to filter_parameters in config/initializers/filter_parameter_logging.rb

    Every key that carries a secret or a personal identifier goes on the list. The generated entries are only a start.

Interview questions

Warm-up: a Rails log shows password: "[FILTERED]" but also a notes field with the plain text wrote the password down. Why does the filter hide one and not the other?

Because filter_parameters matches keys, not values. The generated list holds key patterns such as :passw, which catches password, password_confirmation and any other key that contains those letters. notes matches no entry, so its whole value reaches the log, whatever it says. Add every key your app uses for a secret to the list, and keep secrets out of free-text fields: a filter that guessed at values would still miss a password that looks like an ordinary word.

  1. Mid to senior

    An alert at Kaksha says the production RAILS_MASTER_KEY may have been read by someone who should not have it. secret_key_base is one of the credentials. Rotate it in a way that fits the alert, and keep the next scheduled quarterly rotation of the same key running under its own clock.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    Kaksha stores a guardian's phone, address, PAN card number and a free-text note. The team is adding Active Record Encryption. Decide which columns to encrypt and which shape to pick for each, and defend the choices against the shape you did not take.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A Kaksha developer pushes a branch to the public mirror of the app repo and realises after fifteen minutes that the credentials key file is in the diff. The branch has not been merged. Lay out the response for the next 24 hours.

    Model answer, Pro. First step and follow-ups

Flashcards · 6 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which two files make up Rails credentials, and which of them goes into git?

    The encrypted config/credentials.yml.enc goes into git; the key file that decrypts it is git-ignored. Neither is any use without the other, and a server normally gets the key from an environment variable instead of the file.

  2. What changes once config/credentials/production.yml.enc exists, and how does a deploy script get a single value?

    Production reads that file, with its own key, in place of the shared one. bin/rails credentials:fetch with the value's name prints just that value, so the script never opens the whole file.

  3. The credentials key file has landed in a commit. What do you do?

    Rotate every secret the credentials held and the key itself, then remove the file in a new commit. Rewriting history does not withdraw copies that have already left the server.

  4. Why does changing secret_key_base sign every user out?

    The app derives a key from it for each use, with ActiveSupport::KeyGenerator and a salt per use: signed cookies, encrypted cookies such as the session, and each named message verifier. Unless the old value stays registered as a rotation, everything signed or encrypted with it stops reading.

  5. The log shows password as [FILTERED] but a notes field holding a password in plain text. Why?

    filter_parameters matches key names, as substrings, and never looks at values. notes matches no entry on the list, so its whole value reaches the log.

  6. What two shapes can an Active Record encrypted column take?

    A deterministic column stores the same ciphertext for the same value, so find_by still finds a row by it. A non-deterministic column, the default, stores a new ciphertext on every write.

Key takeaways

  • Encrypted credentials and the credentials key are a pair: the encrypted YAML goes in git, and the key comes from RAILS_MASTER_KEY on a server and from the git-ignored key file on a developer laptop.
  • A per-environment file such as config/credentials/production.yml.enc replaces the shared file when it exists, and bin/rails credentials:fetch prints one value for a deploy script.
  • Every cookie key is derived from secret_key_base, so changing it signs every user out unless the old value stays registered as a rotation.
  • filter_parameters masks values by key name, so a secret typed into a free-text field such as notes reaches the log.
  • Active Record encryption stores a column either deterministically, so find_by still works on it, or with a new ciphertext on every write, the default.

Terms in this lesson

credentials key
The key that unlocks a Rails app's encrypted credentials: kept in a git-ignored key file on a developer laptop, or in the RAILS_MASTER_KEY environment variable on a server.
deterministic encryption
A column-encryption shape in Active Record Encryption where the same plaintext always produces the same ciphertext bytes, so find_by on the column hits an index; non-deterministic encryption uses a fresh IV per write.
encrypted credentials
The config/credentials.yml.enc file Rails ships with: a YAML file encrypted with a key read from a git-ignored key file or the RAILS_MASTER_KEY variable, holding secret_key_base and the app's API keys.
filter_parameters
The Rails config list whose entries are substring-matched against parameter keys so matching values become [FILTERED] in logs; the generator writes a list that starts with passw, email, secret, token.
key rotation
Replacing a signing or encryption key with a new one on a schedule or in response to a leak, usually with the old key kept as a fallback verifier for a window so pre-rotation tokens still work.
secret_key_base
The credential Rails derives cookie-signing, cookie-encryption and message-signing keys from, with ActiveSupport::KeyGenerator; one value for the whole app, held in encrypted credentials.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.