Skip to content

Commit

Permalink
Merge pull request rails#28128 from rails/revert-28127-revert-28038-e…
Browse files Browse the repository at this point in the history
…ncrypted-secrets

Revert "Revert "Add encrypted secrets""
  • Loading branch information
kaspth authored Feb 23, 2017
2 parents 4734d23 + 9fdf326 commit 8f59a1d
Show file tree
Hide file tree
Showing 18 changed files with 485 additions and 39 deletions.
14 changes: 2 additions & 12 deletions railties/lib/rails/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "active_support/key_generator"
require "active_support/message_verifier"
require "rails/engine"
require "rails/secrets"

module Rails
# An Engine with the responsibility of coordinating the whole boot process.
Expand Down Expand Up @@ -385,18 +386,7 @@ def config=(configuration) #:nodoc:
def secrets
@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) || {}
shared_secrets = all_secrets["shared"]
env_secrets = all_secrets[Rails.env]

secrets.merge!(shared_secrets.deep_symbolize_keys) if shared_secrets
secrets.merge!(env_secrets.deep_symbolize_keys) if env_secrets
end
secrets.merge! Rails::Secrets.parse(config.paths["config/secrets"].existent, env: Rails.env)

# Fallback to config.secret_key_base if secrets.secret_key_base isn't set
secrets.secret_key_base ||= config.secret_key_base
Expand Down
6 changes: 6 additions & 0 deletions railties/lib/rails/application/bootstrap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "active_support/notifications"
require "active_support/dependencies"
require "active_support/descendants_tracker"
require "rails/secrets"

module Rails
class Application
Expand Down Expand Up @@ -77,6 +78,11 @@ module Bootstrap
initializer :bootstrap_hook, group: :all do |app|
ActiveSupport.run_load_hooks(:before_initialize, app)
end

initializer :set_secrets_root, group: :all do
Rails::Secrets.root = root
Rails::Secrets.read_encrypted_secrets = config.read_encrypted_secrets
end
end
end
end
6 changes: 4 additions & 2 deletions railties/lib/rails/application/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Configuration < ::Rails::Engine::Configuration
:railties_order, :relative_url_root, :secret_key_base, :secret_token,
:ssl_options, :public_file_server,
:session_options, :time_zone, :reload_classes_only_on_change,
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
:read_encrypted_secrets

attr_writer :log_level
attr_reader :encoding, :api_only
Expand Down Expand Up @@ -51,6 +52,7 @@ def initialize(*)
@debug_exception_response_format = nil
@x = Custom.new
@enable_dependency_loading = false
@read_encrypted_secrets = false
end

def encoding=(value)
Expand Down Expand Up @@ -80,7 +82,7 @@ def paths
@paths ||= begin
paths = super
paths.add "config/database", with: "config/database.yml"
paths.add "config/secrets", with: "config/secrets.yml"
paths.add "config/secrets", with: "config", glob: "secrets.yml{,.enc}"
paths.add "config/environment", with: "config/environment.rb"
paths.add "lib/templates"
paths.add "log", with: "log/#{Rails.env}.log"
Expand Down
28 changes: 19 additions & 9 deletions railties/lib/rails/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,23 @@ def environment # :nodoc:
end

# Receives a namespace, arguments and the behavior to invoke the command.
def invoke(namespace, args = [], **config)
namespace = namespace.to_s
namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace)
namespace = "version" if %w( -v --version ).include? namespace
def invoke(full_namespace, args = [], **config)
namespace = full_namespace = full_namespace.to_s

if command = find_by_namespace(namespace)
command.perform(namespace, args, config)
if char = namespace =~ /:(\w+)$/
command_name, namespace = $1, namespace.slice(0, char)
else
find_by_namespace("rake").perform(namespace, args, config)
command_name = namespace
end

command_name = "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
namespace = "version" if %w( -v --version ).include?(command_name)

command = find_by_namespace(namespace, command_name)
if command && command.all_commands[command_name]
command.perform(command_name, args, config)
else
find_by_namespace("rake").perform(full_namespace, args, config)
end
end

Expand All @@ -52,8 +60,10 @@ def invoke(namespace, args = [], **config)
#
# Notice that "rails:commands:webrat" could be loaded as well, what
# Rails looks for is the first and last parts of the namespace.
def find_by_namespace(name) # :nodoc:
lookups = [ name, "rails:#{name}" ]
def find_by_namespace(namespace, command_name = nil) # :nodoc:
lookups = [ namespace ]
lookups << "#{namespace}:#{command_name}" if command_name
lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

lookup(lookups)

Expand Down
10 changes: 8 additions & 2 deletions railties/lib/rails/command/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def inherited(base) #:nodoc:
end

def perform(command, args, config) # :nodoc:
command = nil if Rails::Command::HELP_MAPPINGS.include?(args.first)
if Rails::Command::HELP_MAPPINGS.include?(args.first)
command, args = "help", []
end

dispatch(command, args.dup, nil, config)
end
Expand Down Expand Up @@ -111,7 +113,7 @@ def usage_path
# For a `Rails::Command::TestCommand` placed in `rails/command/test_command.rb`
# would return `rails/test`.
def default_command_root
path = File.expand_path(File.join("../commands", command_name), __dir__)
path = File.expand_path(File.join("../commands", command_root_namespace), __dir__)
path if File.exist?(path)
end

Expand All @@ -129,6 +131,10 @@ def create_command(meth)
super
end
end

def command_root_namespace
(namespace.split(":") - %w( rails )).first
end
end

def help
Expand Down
52 changes: 52 additions & 0 deletions railties/lib/rails/commands/secrets/USAGE
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
=== Storing Encrypted Secrets in Source Control

The Rails `secrets` commands helps encrypting secrets to slim a production
environment's `ENV` hash. It's also useful for atomic deploys: no need to
coordinate key changes to get everything working as the keys are shipped
with the code.

=== Setup

Run `bin/rails secrets:setup` to opt in and generate the `config/secrets.yml.key`
and `config/secrets.yml.enc` files.

The latter contains all the keys to be encrypted while the former holds the
encryption key.

Don't lose the key! Put it in a password manager your team can access.
Should you lose it no one, including you, will be able to access any encrypted
secrets.
Don't commit the key! Add `config/secrets.yml.key` to your source control's
ignore file. If you use Git, Rails handles this for you.

Rails also looks for the key in `ENV["RAILS_MASTER_KEY"]` if that's easier to
manage.

You could prepend that to your server's start command like this:

RAILS_MASTER_KEY="im-the-master-now-hahaha" server.start


The `config/secrets.yml.enc` has much the same format as `config/secrets.yml`:

production:
secret_key_base: so-secret-very-hidden-wow
payment_processing_gateway_key: much-safe-very-gaedwey-wow

But that's where the similarities between `secrets.yml` and `secrets.yml.enc`
end, e.g. no keys from `secrets.yml` will be moved to `secrets.yml.enc` and
be encrypted.

A `shared:` top level key is also supported such that any keys there is merged
into the other environments.

=== Editing Secrets

After `bin/rails secrets:setup`, run `bin/rails secrets:edit`.

That command opens a temporary file in `$EDITOR` with the decrypted contents of
`config/secrets.yml.enc` to edit the encrypted secrets.

When the temporary file is next saved the contents are encrypted and written to
`config/secrets.yml.enc` while the file itself is destroyed to prevent secrets
from leaking.
36 changes: 36 additions & 0 deletions railties/lib/rails/commands/secrets/secrets_command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
require "active_support"
require "rails/secrets"

module Rails
module Command
class SecretsCommand < Rails::Command::Base # :nodoc:
def help
say "Usage:\n #{self.class.banner}"
say ""
say self.class.desc
end

def setup
require "rails/generators"
require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator"

Rails::Generators::EncryptedSecretsGenerator.start
end

def edit
require_application_and_environment!

Rails::Secrets.read_for_editing do |tmp_path|
puts "Waiting for secrets file to be saved. Abort with Ctrl-C."
system("\$EDITOR #{tmp_path}")
end

puts "New secrets encrypted and saved."
rescue Interrupt
puts "Aborted changing encrypted secrets: nothing saved."
rescue Rails::Secrets::MissingKeyError => error
say error.message
end
end
end
end
1 change: 1 addition & 0 deletions railties/lib/rails/generators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ def sorted_groups
rails.map! { |n| n.sub(/^rails:/, "") }
rails.delete("app")
rails.delete("plugin")
rails.delete("encrypted_secrets")

hidden_namespaces.each { |n| groups.delete(n.to_s) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ Rails.application.configure do
config.consider_all_requests_local = false
config.action_controller.perform_caching = true

# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true

# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ development:
test:
secret_key_base: <%= app_secret %>

# Do not keep production secrets in the repository,
# instead read values from the environment.
# Do not keep production secrets in the unencrypted secrets file.
# Instead, either read values from the environment.
# Or, use `bin/rails secrets:setup` to configure encrypted secrets
# and move the `production:` environment over there.

production:
secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require "rails/generators/base"
require "rails/secrets"

module Rails
module Generators
class EncryptedSecretsGenerator < Base
def add_secrets_key_file
unless File.exist?("config/secrets.yml.key") || File.exist?("config/secrets.yml.enc")
key = Rails::Secrets.generate_key

say "Adding config/secrets.yml.key to store the encryption key: #{key}"
say ""
say "Save this in a password manager your team can access."
say ""
say "If you lose the key, no one, including you, can access any encrypted secrets."

say ""
create_file "config/secrets.yml.key", key
say ""
end
end

def ignore_key_file
if File.exist?(".gitignore")
unless File.read(".gitignore").include?(key_ignore)
say "Ignoring config/secrets.yml.key so it won't end up in Git history:"
say ""
append_to_file ".gitignore", key_ignore
say ""
end
else
say "IMPORTANT: Don't commit config/secrets.yml.key. Add this to your ignore file:"
say key_ignore, :on_green
say ""
end
end

def add_encrypted_secrets_file
unless File.exist?("config/secrets.yml.enc")
say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted."
say ""

template "config/secrets.yml.enc" do |prefill|
say ""
say "For now the file contains this but it's been encrypted with the generated key:"
say ""
say prefill, :on_green
say ""

Secrets.encrypt(prefill)
end

say "You can edit encrypted secrets with `bin/rails secrets:edit`."

say "Add this to your config/environments/production.rb:"
say "config.read_encrypted_secrets = true"
end
end

private
def key_ignore
[ "", "# Ignore encrypted secrets key file.", "config/secrets.yml.key", "" ].join("\n")
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# See `secrets.yml` for tips on generating suitable keys.
# production:
# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289…
Loading

0 comments on commit 8f59a1d

Please sign in to comment.