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.
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
rails credentials:edit --environment production.
Learning 2: Regardless of which file they’re defined in, credentials are accessed again using
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.
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
Here are some of the things I learned while exploring this corner of Rails.
RAILS_ENVhas no effect on the
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
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)|
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
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:
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
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:
rails credentials:edit to decrypt your current credentials
2. Copy the contents of that file before closing it
master.key (or other file pairs as necessary)
rails credentials:edit to create a new
5. Paste the original contents in, then save and close. This will create a new
6. Update the copy in your password manager
7. Clear your clipboard history if applicable
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.
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.
--environment flag accepts either a space,
--environment production, or an equals sign,
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
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.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.
Rails.application.credentials will not have any credentials loaded (in
@config) until after you first access one explicitly or access them all with
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
: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 method simply converts the
config 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