Using Rails 4.1 Secrets for Configuration

I previously wrote about how I handle environment configuration in Rails. Along with solutions like the dotenv gem, it relies on entirely on environment variables.

One of the highlighted features of Rails 4.1 was the config/secrets.yml file. By default, this file contains the secret_key_base and defers to the ENV variable of the same name in the production environment. Even though secret_key_base isn’t typically referenced explicitly in an application, I was curious if I could use the config/secrets.yml file in place of previously documented configuration solution.

After a little digging, it turns out that it works perfectly. A valid question is whether the variables are better referenced through the Rails.application hash, but that’s probably more a preference and use-case dependent decision. Either way, we’ll explore the solution below.

The Question

Below is a default config/secrets.yml file generated from a Rails 4.1 app. As you can see, both the development and test environments rely on statically set values, where the production environment relies on environment variables being set on the system. The latter is perfect for platforms like Heroku, and just as easy if you manage your own systems on EC2 or similar infrastructure.

development:
  secret_key_base: 9ac2d0ad8ebcc312090e99d745006d3cf8

test:
  secret_key_base: a1580ad61ccb6ac60f9f256948cf63d6e20

# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

Notice how ERB is processed in the file. This gives us the opportunity to use Ruby to generate random strings, dates, or anything else that can be expressed through code.

So can we use it for other configuration values in the application?

The Solution

In order to figure out where the output of the parsed secrets file was stored, I pulled the latest Rails changes and went code diving.

The first thing I did was search “secrets”. The first group of results were mostly comments related to the processing of secret_key_base and where it could be found. After combing through a few more results, I came across the Rails::Application class.

A static array at the top of the file seemed to hold some values for the application as shown below:

INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders,
                        :routes, :helpers, :app_env_config, :secrets] # :nodoc:

Looks like we’re on the right track. Going further down the file leads us to the getter:

def secrets #:nodoc:
  @secrets ||= begin
    secrets = ActiveSupport::OrderedOptions.new
    yaml = config.paths["config/secrets"].first

    if File.exist?(yaml)
      require "erb"
      all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}
      env_secrets = all_secrets[Rails.env]
      secrets.merge!(env_secrets.symbolize_keys) if env_secrets
    end

    # Fallback to config.secret_key_base if secrets.secret_key_base isn't set
    secrets.secret_key_base ||= config.secret_key_base

    secrets
  end
end

As you can see, the file path config/secrets is referenced as the yaml source:

yaml = config.paths["config/secrets"].first

and the result of reading the file is sent through ERB and YAML:

all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}

The environment group is parsed:

env_secrets = all_secrets[Rails.env]

The result of the output is returned, leaving us with a hash of options based on the environment group. With a little luck we should be able to query the application from the console and get the configuration values.

irb(main):001:0> Rails.application.secrets.class
=> ActiveSupport::OrderedOptions
irb(main):002:0> Rails.application.secrets
=> {:secret_key_base=>"9ac2d0ad8ebcc312090e99d745006d3cf8"}
irb(main):003:0> Rails.application.secrets.secret_key_base
=> "a1580ad61ccb6ac60f9f256948cf63d6e20"

That’s great news because it means we can put other values in this file and reference them throughout our application using the parent hash Rails.application.secrets.

For example, let’s assume we need to configuration Pusher URL again. We could add it to the secrets.yml file like so:

development:
  secret_key_base: 9ac2d0ad8ebcc312090e99d745006d3cf8
  pusher_url: http://asdfa@api.pusherapp.com

test:
  secret_key_base: a1580ad61ccb6ac60f9f256948cf63d6e20

# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
  pusher_url: <%= ENV["PUSHER_URL"] %>

Now within our application we can set the Pusher URL within the initializer using the secret value:

# config/initializers/pusher.rb
Pusher.url = Rails.application.secrets.pusher_url

Summary

I feel like my previous solution has the potential to be replaced with the secrets file. I plan to try it out in an upcoming application and see if it’s as easy to manage as it seems.

Note that by default, the secrets.yml file is NOT ignored by git. If you plan to include passwords or other sensitive data in the file, be sure to add it to your .gitignore.

Comments