Where this app keeps its secrets
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
- Where this app keeps its secrets (this lesson).
- Encrypted credentials, precisely.
- Environment variables, precisely.
- Choosing between them.
- Rotation, and what to do after a leak.
- Secrets in the pipeline.
- Putting it together: a credentials plan.
Key takeaways
- This repository holds only
secret_key_basein encrypted credentials and reads every other secret from the environment. config/master.keyis ignored and, verified withgit 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.
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.