EJSON::Rails

Build Status

Automatically injects ejson decrypted secrets into your Rails.application.secrets.

Installation

Add this line to your application's Gemfile:

gem 'ejson-rails'

And then execute:

$ bundle

Or install it yourself as:

$ gem install ejson-rails

Usage

Decrypted secrets and credentials from project/config/secrets.json (or project/config/secrets.{current_rails_environment}.json if that doesn't exist) will be accessible via Rails.application.secrets. For example:

# project/config/secrets.json

{ "some_secret": "key" }

will be accessible via Rails.application.secrets.some_secret or Rails.application.secrets[:some_secret] upon booting. JSON files are loaded once and contents are deep_merge'd into your app's existing rails secrets.

Secrets will also be accessible via Rails.application.credentials, e.g. Rails.application.credentials.some_secret or Rails.application.credentials[:some_secret]. To avoid subtle compatibility issues, if a credential already exists, an error will occur.

If you set the EJSON_RAILS_DELETE_SECRETS environment variable to true the gem will automatically delete the secrets from the filesystem after loading them into Rails. It will delete both paths (project/config/secrets.json and project/config/secrets.{current_rails_environment}.json) if the files exist and are writable.

NOTE: This gem does not decrypt ejson for you. You will need to configure this as part of your deployment pipeline.

Migrating to credentials

Rails 7.1 has deprecated application secrets in favor of credentials. ejson-rails can migrate secrets to application credentials.

Even before running Rails 7.1, you can migrate your secrets in several steps:

  1. Convert secrets from YAML to JSON
  2. Move any ERB embedded within the YAML to the corresponding environment file
  3. Use Rails.application.credentials in place of Rails secrets

1. Convert secrets from config/secrets.yml to config/secrets.json

Typically, secrets share the same structure across different environments. While test secrets are often placeholders, development secrets may sometimes use environment variables to communicate with external services. In that case, the easiest way to migrate is to use the test secrets in all local environments, and override for development as needed:

# Recommended
bin/rails runner -e test 'Rails.root.join("config/secrets.json").write(JSON.pretty_generate(Rails.application.secrets.to_h.without(:secret_key_base)))'

[!NOTE] Alternatively, if its necessary to configure distinct values between the development/test environment, you can use separate JSON files for the development/test environments:

bin/rails runner 'Rails.root.join("config/secrets.#{Rails.env}.json").write(JSON.pretty_generate(Rails.application.secrets.to_h.without(:secret_key_base)))'
bin/rails runner -e test 'Rails.root.join("config/secrets.#{Rails.env}.json").write(JSON.pretty_generate(Rails.application.secrets.to_h.without(:secret_key_base)))'

2. Move any ERB into the corresponding environment files

YAML supports ERB while JSON secrets do not. If your secrets contain ERB, you will need to move that logic to the corresponding environment file:

Before:

config/secrets.yml

development:
  some_external_service:
    api_token: <%= ENV.fetch(SOME_EXTERNAL_SERVICE_API_TOKEN, "12345") %>

After:

config/secrets.json as generated by the recommended command above.

{
  "some_external_service": {
    "api_token": "12345"
  },
  "something_else_entirely": "abc"
}

config/environments/development.rb

Rails.application.configure do
  # elided

  credentials.some_external_service.api_token = ENV.fetch("SOME_EXTERNAL_SERVICE_API_TOKEN", "12345")
  credentials.something_else_entirely = ENV.fetch("SOMETHING_ELSE_ENTIRELY", "abc")
end

Rails 7.0 Note

[!NOTE] In Rails 7.0, credentials are accessed as a Hash with [] and []=.. This is important because the dynamic accessor methods will set values in a different object, and credentials will behave inconsistently after that:

Rails.application.credentials.some_external_service.api_token = "foo"
Rails.application.credentials[:some_external_service][:api_token] # => "12345"

Also note the code sets top-level values through credentials.config, because credentials#[]=(key, value) sets values in a different object.

Rails.application.credentials[:something_else_entirely] = "foo"
Rails.application.credentials[:something_else_entirely] # => "abc"

Make sure there's no code using the dynamic accessors before setting the configuration in the Hash, or the values won't be accessible from the dynamic accessor:

Rails.application.credentials.something_else_entirely # just accessing is enough to cause the issue
Rails.application.credentials[:some_external_service][:api_token] = "foo"
Rails.application.credentials.some_external_service.api_token # => "12345"

3. Use Rails.application.credentials

You are now ready to replace Rails secrets with Rails credentials:

git ls-files | xargs ruby -pi -e 'gsub("Rails.application.secrets", "Rails.application.credentials")' --

To avoid the deprecation warning from the use of secrets in ejson-rails once you're running Rails 7.1, require another file from your Gemfile:

gem 'ejson-rails', require: 'ejson/rails/skip_secrets'

With this require, ejson-rails will no longer merge secrets from JSON into Rails.application.secrets. This will be the default in the next major version.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/ejson-rails.

License

The gem is available as open source under the terms of the MIT License.