Rails can encrypt keys for you. It encrypts and decrypts them using other keys. Once your keys have been decrypted using the other keys, you can look up your keys by their keys. If that sounds confusing, this post may be the key to getting a better understanding of Rails Credentials.
Learning 1: RAILS_ENV=production rails credentials:edit
, RAILS_ENV=development rails credentials:edit
and rails credentials:edit
all do exactly the same thing. The way to edit per-environment credentials is by adding the --environment
flag; rails credentials:edit --environment production
.
Learning 2: Regardless of which file they’re defined in, credentials are accessed again using Rails.application.credentials.name
or Rails.application.credentials.name[:child][:grandchild][:great_grandchild]
if you nest credentials.
Learning 3: If an environment-specific credentials file is present, Rails will only look in that file for credentials. The default/shared credentials file will not be used, even if the secret is missing from the environment-specific file.
Bonus Tip: ActiveSupport::OrderedOptions
is a handy sub-class of hash that gives you dynamic method based on hash key names and the option of raising an error instead of retuning nil if a requested hash key doesn’t have a value assigned.
That’s the short version. Read on if you’d like some additional context, a bit information about how the Credentials “magic” actually works and some practical implications. If you’re super bored/interested, read on beyond that for some mildly interesting trivia or edge cases.
A note on terminology As noted earlier, the term keys is very overloaded. Credentials, passwords or secrets are often referred to as keys, as in API key. Additionally, Rails Credentials uses a key to encrypt each credential file. Finally, hashes, the data type Rails Credentials uses to store decrypted credentials in memory, use keys to retrieve values. Which is why we can accurately but unhelpfully say, keys are encrypted by other keys and looked up by yet different keys. In an effort to avoid confusion I have used the following naming convention throughout this post:
- The term Credentials (upper case C) is shorthand for Rails Credentials, the overall Rails facility for storing secrets needed by your application.
- The terms credentials or credential (lower case C) refer to the actual application secret(s) you store using Credentials.
- The term “name” is used to refer to the hash key (or YAML key) of a credential.
- The term “file encryption key” is used to refer to the main secret that Credentials uses to encrypt a set of your credentials on disk.
- Any other unavoiable use of the word “key” will be preceded by a descriptor such as hash key, YAML key or API key.
I’m using Rails’ built in Credentials feature to store Google Workspace API credentials. After seeing how easy it was to delete real Google Workspace users from the directory, I decided I really should be using the test domain Google generously allows education customers to set up. So after adding a Service Account to our test domain, it was time to separate the production credentials from the development/staging credentials.
My first thought was to run RAILS_ENV=production rails credentials:edit
but when I did, the existing credentials file opened up. I then tried to specify the development environment to see if maybe I had it backwards but once again the same credentials file opened up.
There’s nothing in the Rails Guide on security about it but eventually I found a reference to the original PR for this feature which explains the need to specify the environment using the --environment
flag.
Here are some of the things I learned while exploring this corner of Rails.
RAILS_ENV
has no effect on the rails credentials
commandsThe command rails credentials:edit
, regardless of the value of RAILS_ENV
, will always attempt to open and decrypt the default credentials file for editing; config/credentials.yml.enc
. The way to change which environment you would like to edit credntials for is to use the --environment
flag.
When dealing with the default credentials file, the encryption key is obtained from the RAILS_MASTER_KEY
environment variable if it is set, otherwise the contents of config/master.key
is tried. When you close the decrypted file, it is re-encrypted with the encryption key.
If you specify an environment using (for example) rails credentials:edit --environment production
, then a different credentials file will be opened (or created) at config/credentials/production.yml.enc
. This one might use the same encryption key or it might not. If the same RAILS_MASTER_KEY
environment variable is set, it will use that to encrypt the file. If it isn’t set, it will use (or create on first edit) a different key stored in a correspondingly named file, config/credentials/production.key
in our example.
Here’s a table showing 4 different credential commands, the files they maintain, and the location of the encryption keys used for each file:
Command | Credentials File | Encryption key Environment Variable | Encryption Key File (if ENV VAR not set) |
---|---|---|---|
rails credentials:edit |
config/credentials.yml.enc |
RAILS_MASTER_KEY |
/config/master.key |
rails credentials:edit --environment development |
/config/credentials/development.yml.enc |
RAILS_MASTER_KEY |
/config/credentials/development.key |
rails credentials:edit --environment test |
/config/credentials/test.yml.enc |
RAILS_MASTER_KEY |
/config/credentials/test.key |
rails credentials:edit --environment production |
/config/credentials/production.yml.enc |
RAILS_MASTER_KEY |
/config/credentials/production.key |
With your credentials successfully stored, they can all be accessed within your Rails app (or in the console) via the Rails.application.credentials
hash. The credential names in the YAML are symbolized so credential_name: my secret password
can be accessed via Rails.application.credentials[:credential_name]
. For your convenience, first level credentials are also made available as methods so you can access them using Rails.application.credentials.name
.
If you nest additional credentials, they form a hash of hashes and can be accessed using standard hash notation. I can’t imagine why you’d want more than 2, maybe 3, levels in a real application but if you had six levels of nesting the way to access the deepest item would be Rails.application.credentials.name[:child][:grandchild][:gen_4][:gen_5][:gen_6]
. Child credentials can’t be accessed as methods, you must use the hash syntax to access them: Rails.application.credentials.name[:child]
.
If you want an exception to be raised if a top level credential can’t be found, use the bang !
version of the method name: Rails.application.credentials.name!
. Without this you’ll just get back nil
. You will need to manually guard against missing child credentials yourself though. One way to do this would be Rails.application.credentials.name![:child].presence || raise(KeyError.new(":child is blank"))
If an environment-specific credentials file is present, Rails will only look in that file for credentials. The default credentials file will not be used, even if the requested credential is missing from the environment-specific file and set in the default file.
One implication of this is that, if you use environment specific files, you will need to duplicate any shared keys between files and keep them in sync when they change. I would love to see Credentials improved to first load the default credentials file, if present, with all its values and then load an environment-specific file, if present, with its values. Shared credentials could then be stored in the default file and be overridden (or supplemented) in certain environments.
Choosing to adopt environment-specific files means choosing to keep common credentials synchronised between files. Small teams may be better off sticking with namespaced credentials in the default file. To my mind, the neatest option is simply adding an environment YAML key where necessary:
# credentials.yml.enc
aws_key: qazwsxedcrfv # same in all environments
google:
development: &google_defaults
project_id: 12345678 # shared between all environments
private_key: ABC123
test:
<<: *google_defaults # exactly the same as development
production:
<<: *google_defaults
private_key: DEF456 # override just the values that are different
# Application
Rails.application.credentials.aws_key
Rails.application.credentials.google[Rails.env.to_sym][:project_id]
Rails.application.credentials.google[Rails.env.to_sym][:private_key]
If separate files are needed, I think the next best option would be to try to limit yourself to two files; one shared between dev, test and staging, and another one for production. However this will get messy the moment you need to access any production credentials in staging (for example). You’ll then need to either keep all 3 files fully populated or start splitting the contents of one or both of the files using the [Rails.env.to_sym]
trick.
If you lose your file encryption Key, the contents of the encrypted file will also be lost. Individuals could store this file in local backups or store the contents of the file in their password manager.
If multiple team members need access, the file encryption key should be stored in a shared vault. I’m a big fan of 1password.com myself.
One way to rotate your File Encryption Keys is to:
1. Run rails credentials:edit
to decrypt your current credentials
2. Copy the contents of that file before closing it
3. Delete credentials.yml.enc
and master.key
(or other file pairs as necessary)
4. Re-run rails credentials:edit
to create a new master.key
5. Paste the original contents in, then save and close. This will create a new credentials.yml.enc
file
6. Update the copy in your password manager
7. Clear your clipboard history if applicable
If RAILS_MASTER_KEY
is not set and the encryption key file (see table above) does not exist, a new encryption key will be generated and saved to the relevant file. The encryption key file will also be added to .gitignore
. The new encyption key will not be able to decrypt existing credential files.
Whilst RAILS_MASTER_KEY
lives up to it’s “master key” name and is used by all environment files, config/master.key
does not and is not.
The --environment
flag accepts either a space, --environment production
, or an equals sign, --environment=production
.
If you specify a credential name (YAML key) with a hyphen in it, the .name
syntax won’t work. Similarly if you name a child credential with a hyphen, you will need to access it with a strange (to me) string/symbol hybrid. The required syntax is Rails.application.credentials[:'hyphen-name']
and Rails.application.credentials.name[:'child-hyphen-name']
respectively.
You can’t change the file encryption key by running credentials:edit
and then changing the file encryption key whlie the credentials file is still open. The original file encryption key is kept in memory and used to re-encrypt the contents when the credentials file is closed.
Even though you don’t use RAILS_ENV
to set the environment, the environment name you pass to the --environment
flag should match a real envrionment name. If you run rails credentials:edit --environment foo
, Rails will happily generate foo.yml.enc
and foo.key
but unless you have a Rails environment named foo
the credentials will never be (automatically) loaded.
Some YAML files in Rails are parsed with ERB first. Credentials files are not so you can’t include Ruby code in your credentials file.
YAML does allow you to inherit settings from one section to another. By appending &foo
to the end of a parent key you can “copy” all the child keys to another YAML node with <<: *foo
. See the example in Takeaway 1 above for a fuller example.
In development, Rails.application.credentials
will not have any credentials loaded (in @config
) until after you first access one explicitly or access them all with Rails.application.credentials.config
.
This may also be theoretically true in production, but in practice the production environment tries to validate secret_key_base
at startup, thereby loading all the credentials straight away.
Whilst technically the credentials live in the Rails.application.credentials.config
hash, Credentials delegates calls to the :[]
and :fetch
methods to :config
. This allows us to drop the .config
part of the method call.
Missing methods on Rails.application.credentials
get delegated to :options
. The options
method simply converts theconfig
hash into ActiveSupport::OrderedOptions
, a sub-class of Hash. OrderedOptions
is what provides the .name
shortcuts and the .name!
alternatives. I can think of a few other use cases where OrderedOptions
would be handy! If you already have a hash you need to use ActiveSupport::InheritableOptions
to convert it into an OrderedOptions
collection.