Where this app keeps its secrets

Free preview 4 min to read

Every application has a handful of values that are not code and must not be public. A database password, an API key, the token that signs its cookies. This course is about where they live, and this repository is unusually interesting because it uses two mechanisms at once.

What is here

config/credentials.yml.enc     # encrypted, committed
config/master.key              # the key, ignored by git

And the contents:

Rails.application.credentials.config.keys
# => [:secret_key_base]

One value. Meanwhile, the application reads its other secrets from the environment (seven names in the secret list of config/deploy.yml, besides the master key):

# config/initializers/omniauth.rb
provider :github, ENV["GITHUB_KEY"], ENV["GITHUB_SECRET"], scope: "user:email"
# config/environments/production.rb
user_name: ENV["SMTP_USERNAME"],
password: ENV["SMTP_PASSWORD"],

So the split, in practice, is: secret_key_base in encrypted credentials, everything else in environment variables.

That is not an accident and it is not obviously wrong. It is a design that most Rails applications converge on without deciding to. And this course is about whether it is the right one, which requires understanding what each mechanism gives you.

The one thing that is definitely right

git check-ignore -v config/master.key
# .gitignore:34:/config/master.key    config/master.key

git log --all -- config/master.key
# (no output)

The key is ignored, and it has never been committed: not on any branch, not in any commit reachable from anywhere in this repository. That second check is the one people skip, and it is the one that matters. A key that is ignored today and was committed a year ago is still in the history, and still compromised.

Run that command on your own repositories. The --all is essential: a key committed to a branch that was later deleted is still reachable, and still there.

flowchart TB
  S["A secret"] --> Q{"where does it live?"}
  Q -->|"encrypted credentials"| C["credentials.yml.enc, committed"]
  C --> K["master.key, never committed"]
  Q -->|"environment"| E["ENV, set by the platform"]
  E --> P["Kamal env.secret, or the platform's secret store"]
  K -.->|"if this leaks"| B["everything in the file is public"]

The two mechanisms

Encrypted credentials Environment variables
Where the value lives In the repository, encrypted In the platform, outside the code
Where the trust lives One key, distributed separately The platform's secret store
Changing a value Edit, commit, deploy Change and restart
Who can read it Anyone with the master key Anyone who can read the process
Rotation A commit A configuration change
Audit trail Git history, of the ciphertext Whatever your platform records
Different per environment Yes, with separate key files Naturally

Two rows deserve to be read together. Changing a value is a commit with credentials, which is either a helpful audit trail or an unhelpful deploy requirement depending on the secret. And different per environment is where the two mechanisms differ most in practice. The deployment courses' staging destination needs different values, and the two approaches solve that very differently.

What is not secret

Worth stating early because it is where the effort goes wrong.

A public API key that appears in your JavaScript is not a secret, however much the vendor's dashboard calls it one. An analytics site key, a Stripe publishable key, a map tile key: they are shipped to every browser and encrypting them in credentials.yml.enc protects nothing.

Those belong in configuration, not in secrets. Putting them in credentials is not harmful, and it dilutes the file, which makes it harder for the next person to see which values matter.

The test: would an attacker who has this value be able to do anything they could not do already? If the value is already in every browser, the answer is no.

What this course covers

  1. Where this app keeps its secrets (this lesson).
  2. Encrypted credentials, precisely.
  3. Environment variables, precisely.
  4. Choosing between them.
  5. Rotation, and what to do after a leak.
  6. Secrets in the pipeline.
  7. Putting it together: a credentials plan.

Key takeaways

  • This repository holds only secret_key_base in encrypted credentials and reads every other secret from the environment.
  • config/master.key is ignored and, verified with git log --all, has never been committed.
  • Check history with --all, because a key on a deleted branch is still reachable and still compromised.
  • The mechanisms differ most in where trust lives, how a value is changed, and how environments diverge.
  • A key that ships to every browser is configuration, not a secret; encrypting it protects nothing and dilutes the file.
Lesson 1 of 7 · free

Keep going with lesson 2

Encrypted credentials, precisely

Explains how Rails encrypted credentials work, including per-environment files, key distribution, unreadable diffs and the cost of losing the key.

A free account also gives you the quizzes, your progress and notes, and the discussion.

Type to start searching...