Ruby on Rails Secrets Management

Published: Sep 4, 2023

A web application needs various kinds of values, params and etc which should not be revealed to the public, say GitHub repo. For example, API keys, tokens, passwords, and endpoints, all those should be kept secret. Another important factor is that such secrets should be shared among the team members. Additionally, all those secrets should come along with the deployment.

Previously, the dotenv file (.env) is commonly used for this purpose. When the web application is deployed, the secrets are set up as environment variables.

Rails Way of Secrets Management

Since version 5.2, Rails provides a new way of secret management (Rails 5.2.0 FINAL: Active Storage, Redis Cache Store, HTTP/2 Early Hints, CSP, Credentials). It consists of an encrypted credentials file and a key to encrypt/decrypt it. By default, those are config/credentials.yml.enc and config/master.key, which are generated by a rails new command. The credentials.yml.enc is encrypted, so it can be safely pushed to the GitHub repo. While the master.key should kept secret and never be pushed to the GitHub repo. The master.key is often shared among team members by a secure measure.

To edit the credentials file, use the rails credentials:edit command with the EDITOR environment variable. For example:

$ EDITOR=vim rails credentials:edit
$ EDITOR="code --wait" rails credentials:edit

If the master.key doesn’t exist, Rails shows the message below.

When the config/master.key is missing, the command shows the suggestion.

$ EDITOR=vim rails credentials:edit Adding config/master.key to store the encryption key: ed36bdc2…….

Save this in a password manager your team can access.

If you lose the key, no one, including you, can access anything encrypted with it.

 create  config/master.key

Couldn’t decrypt config/credentials.yml.enc. Perhaps you passed the wrong key?

Even though the new master.key is created following the message, the existing credentials.yml.enc won’t be decrypted. It needs the exact key the file is encrypted. This way, we can safely push the credentials.yml.enc file to the GitHub repo.

How to Use Credentials

As the file extension expresses, the credentials file takes YAML format. It is structured, not like a dotenv file.

When the rails new command creates the credentials file, it looks like this.

# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 3cfc7cf4......

We can add various secrets in YAML format. For example, some_api_key, client_id and client_secret pairs for OAuth authentication can be written here.

secret_key_base: 3cfc7cf4......
some_api_key: HERE_IS_MY_API_KEY
oauth:
  twitter:
    client_id: MY_TWITTER_CLIENT_ID
    client_secret: MY_TWITTER_CLIENT_SECRET
  facebook:
    client_id: MY_FACEBOOK_CLIENT_ID
    client_secret: MY_FACEBOOK_CLIENT_SECRET

To use above secrets in Rails app, Rails.application.credentials.[key1].[key2]… is the way to access those. We can confirm that on the Rails console.

$ rails c
Loading development environment (Rails 7.0.4.2)
irb(main):001:0> Rails.application.credentials.some_api_key
=> "HERE_IS_MY_API_KEY"
irb(main):002:0> Rails.application.credentials.oauth.twitter.client_id
=> "MY_TWITTER_CLIENT_ID"
irb(main):003:0> Rails.application.credentials.oauth.twitter.client_secret
=> "MY_TWITTER_CLIENT_SECRET"

Multi-environment credentials

Since Rails 6, the multi-environment credentials are supported. When secrets are not the same between development and production environment, those could’ve been differentiated by a nested YAML notation. For example,

# before Rails 6
development:
  some_api_key: API_KEY_FOR_DEV
production:
  some_api_key: API_KEY_FOR_PROD

However, in the Rails app, we should’ve written something like:

# before Rails 6
if Rails.env == "development"
  api_key = Rails.application.credentials.development.some_api_key
elsif Rails.env == "production"
  api_key = Rails.application.credentials.production.some_api_key
end

In contract, with the multi-environment credentials, the credentials file is chosen based on the Rails environment. If the Rails environment is a production, config/credentials/production.yml.enc is used. The key to encrypt/decrypt is just for the production env, config/credentials/production.key. This way, we can eliminate the environment check in our code.

Create the environment specific credentials

To create the Rails environment based credentials, use –environment option.

$ EDITOR=vim rails credentials:edit --environment development
$ EDITOR=vim rails credentials:edit --environment production

Above commands creates credential and key files under config/credentials directory.

$ tree config/credentials
config/credentials
├── development.key
├── development.yml.enc
├── production.key
└── production.yml.enc

1 directory, 4 files

The newly generated development/production credential files don’t have a secret_key_base entry. Under the development environment, missing secret_key_base looks not an immediate problem. However, under the production environment, it raises an exception.

$ RAILS_ENV=production rails c
/Users/yoko/.gem/ruby/3.2.1/gems/railties-7.0.4.2/lib/rails/application.rb:581:in `validate_secret_key_base': Missing `secret_key_base` for 'production' environment, set this string with `bin/rails credentials:edit` (ArgumentError)
...
...

We should add secret_key_base in the credentials file. To generate the value, Rails provides the command:

$ rake secret
a2b30cf4d702a........

Open the production.yml.enc file and copy/paste the generated secret as the value of secret_key_base.

secret_key_base: a2b30cf4d702a........
some_api_key: API_KEY_FOR_PROD

Now, we can check the production secrets.

$ RAILS_ENV=production rails c
Loading production environment (Rails 7.0.4.2)
irb(main):001:0> Rails.application.credentials.some_api_key
=> "API_KEY_FOR_PROD"

Deploy to Heroku

Heroku is a popular hosting service for a Rails app. However, not like AWS EC2 instance, people don’t have a direct access to the instance. The encrypt/decrypt key should be installed by a heroku command.

One thing we should know is, Rails “looks for the environment variable ENV[“RAILS_MASTER_KEY”] to encrypt the credentials file” (Securing Rails Applications). Whether the environment is development or production, Rails sees ENV[“RAILS_MASTER_KEY”].

Heroku’s staging server runs under the production environment. To deploy the config/production.key, use the Heorku CLI command below:

heroku config:set RAILS_MASTER_KEY=`cat config/credentials/production.key`

References

Latest Posts

Application Development by Rails Action Cable

The previous two blog posts introduced WebSocket and how to implement a WebSocket application on Ruby on Rails. This blog post digs deeper. It is a memo on creating a more realistic application by Action Cable.

Real-time App on Rails by Action Cable

The previous blog post, WebSocket on Rails by Action Cable, focused on WebSocket as a protocol. As in the previous post, by default, Rails app responds to WebSocket connection requests without any hassle. However, other than connecting and sending ping frames, it doesn’t do anything. This blog post focuses on an application side and explains how we can create a full-duplex, bidirectional app.

WebSocket on Rails by Action Cable

In the web application domain, we hear some protocol names. Absolutely, HTTP or HTTPS is the most famous protocol that all web developers know. Although there’s a mechanism of Keep-Alive, a single request/response sequence with a single client/server is all done by HTTP. The client initiates the HTTP request to the server. Once the client receives the HTTP response from the server, communication finishes. As far as HTTP is used, the server just waits and waits. Only when the request comes in, the server can send back some data to the client. This communication style is surprisingly capable of doing many things, so most web applications are satisfied with HTTP.