EJSON::Rails
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:
- Convert secrets from YAML to JSON
- Move any ERB embedded within the YAML to the corresponding environment file
- 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.