1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Support environment specific credentials file. (#33521)

For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by
`ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key.
Edit given environment credentials file by command `rails credentials:edit --environment production`.
Default behavior can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
This commit is contained in:
Wojciech Wnętrzak 2018-09-19 23:02:00 +02:00 committed by David Heinemeier Hansson
parent e184d1a94e
commit e0d3313bac
7 changed files with 163 additions and 29 deletions

View file

@ -1,3 +1,12 @@
* Support environment specific credentials file.
For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by
`ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key.
Edit given environment credentials file by command `rails credentials:edit --environment production`.
Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.
*Wojciech Wnętrzak*
* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment. * Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment.
*Michael C. Nelson* *Michael C. Nelson*

View file

@ -438,8 +438,12 @@ module Rails
# Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with # Decrypts the credentials hash as kept in +config/credentials.yml.enc+. This file is encrypted with
# the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading # the Rails master key, which is either taken from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading
# +config/master.key+. # +config/master.key+.
# If specific credentials file exists for current environment, it takes precedence, thus for +production+
# environment look first for +config/credentials/production.yml.enc+ with master key taken
# from <tt>ENV["RAILS_MASTER_KEY"]</tt> or from loading +config/credentials/production.key+.
# Default behavior can be overwritten by setting +config.credentials.content_path+ and +config.credentials.key_path+.
def credentials def credentials
@credentials ||= encrypted("config/credentials.yml.enc") @credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path)
end end
# Shorthand to decrypt any encrypted configurations or files. # Shorthand to decrypt any encrypted configurations or files.

View file

@ -17,7 +17,7 @@ module Rails
:session_options, :time_zone, :reload_classes_only_on_change, :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, :log_level, :content_security_policy_report_only, :read_encrypted_secrets, :log_level, :content_security_policy_report_only,
:content_security_policy_nonce_generator, :require_master_key :content_security_policy_nonce_generator, :require_master_key, :credentials
attr_reader :encoding, :api_only, :loaded_config_version attr_reader :encoding, :api_only, :loaded_config_version
@ -60,6 +60,9 @@ module Rails
@content_security_policy_nonce_generator = nil @content_security_policy_nonce_generator = nil
@require_master_key = false @require_master_key = false
@loaded_config_version = nil @loaded_config_version = nil
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
end end
def load_defaults(target_version) def load_defaults(target_version)
@ -273,6 +276,27 @@ module Rails
true true
end end
end end
private
def credentials_available_for_current_env?
File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc")
end
def default_credentials_content_path
if credentials_available_for_current_env?
File.join(root, "config", "credentials", "#{Rails.env}.yml.enc")
else
File.join(root, "config", "credentials.yml.enc")
end
end
def default_credentials_key_path
if credentials_available_for_current_env?
File.join(root, "config", "credentials", "#{Rails.env}.key")
else
File.join(root, "config", "master.key")
end
end
end end
end end
end end

View file

@ -38,3 +38,12 @@ the encrypted credentials.
When the temporary file is next saved the contents are encrypted and written to When the temporary file is next saved the contents are encrypted and written to
`config/credentials.yml.enc` while the file itself is destroyed to prevent credentials `config/credentials.yml.enc` while the file itself is destroyed to prevent credentials
from leaking. from leaking.
=== Environment Specific Credentials
It is possible to have credentials for each environment. If the file for current environment exists it will take
precedence over `config/credentials.yml.enc`, thus for `production` environment first look for
`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]`
or stored in `config/credentials/production.key`.
To edit given file use command `rails credentials:edit --environment production`
Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`.

View file

@ -8,6 +8,9 @@ module Rails
class CredentialsCommand < Rails::Command::Base # :nodoc: class CredentialsCommand < Rails::Command::Base # :nodoc:
include Helpers::Editor include Helpers::Editor
class_option :environment, aliases: "-e", type: :string,
desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
no_commands do no_commands do
def help def help
say "Usage:\n #{self.class.banner}" say "Usage:\n #{self.class.banner}"
@ -20,58 +23,78 @@ module Rails
require_application_and_environment! require_application_and_environment!
ensure_editor_available(command: "bin/rails credentials:edit") || (return) ensure_editor_available(command: "bin/rails credentials:edit") || (return)
ensure_master_key_has_been_added if Rails.application.credentials.key.nil?
ensure_credentials_have_been_added encrypted = Rails.application.encrypted(content_path, key_path: key_path, env_key: env_key)
ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil?
ensure_encrypted_file_has_been_added(content_path, key_path)
catch_editing_exceptions do catch_editing_exceptions do
change_credentials_in_system_editor change_encrypted_file_in_system_editor(content_path, key_path, env_key)
end end
say "New credentials encrypted and saved." say "File encrypted and saved."
rescue ActiveSupport::MessageEncryptor::InvalidMessage
say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?"
end end
def show def show
require_application_and_environment! require_application_and_environment!
say Rails.application.credentials.read.presence || missing_credentials_message encrypted = Rails.application.encrypted(content_path, key_path: key_path, env_key: env_key)
say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path)
end end
private private
def ensure_master_key_has_been_added def content_path
master_key_generator.add_master_key_file options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
master_key_generator.ignore_master_key_file
end end
def ensure_credentials_have_been_added def key_path
credentials_generator.add_credentials_file_silently options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
end end
def change_credentials_in_system_editor def env_key
Rails.application.credentials.change do |tmp_path| options[:environment] ? "RAILS_#{options[:environment].upcase}_KEY" : "RAILS_MASTER_KEY"
end
def ensure_encryption_key_has_been_added(key_path)
encryption_key_file_generator.add_key_file(key_path)
encryption_key_file_generator.ignore_key_file(key_path)
end
def ensure_encrypted_file_has_been_added(file_path, key_path)
encrypted_file_generator.add_encrypted_file_silently(file_path, key_path)
end
def change_encrypted_file_in_system_editor(file_path, key_path, env_key)
Rails.application.encrypted(file_path, key_path: key_path, env_key: env_key).change do |tmp_path|
system("#{ENV["EDITOR"]} #{tmp_path}") system("#{ENV["EDITOR"]} #{tmp_path}")
end end
end end
def master_key_generator def encryption_key_file_generator
require "rails/generators" require "rails/generators"
require "rails/generators/rails/master_key/master_key_generator" require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
Rails::Generators::MasterKeyGenerator.new Rails::Generators::EncryptionKeyFileGenerator.new
end end
def credentials_generator def encrypted_file_generator
require "rails/generators" require "rails/generators"
require "rails/generators/rails/credentials/credentials_generator" require "rails/generators/rails/encrypted_file/encrypted_file_generator"
Rails::Generators::CredentialsGenerator.new Rails::Generators::EncryptedFileGenerator.new
end end
def missing_credentials_message def missing_encrypted_message(key:, key_path:, file_path:)
if Rails.application.credentials.key.nil? if key.nil?
"Missing master key to decrypt credentials. See `rails credentials:help`" "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
else else
"No credentials have been added yet. Use `rails credentials:edit` to change that." "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that."
end end
end end
end end

View file

@ -55,6 +55,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end end
end end
test "edit command modifies file specified by environment option" do
assert_match(/access_key_id: 123/, run_edit_command(environment: "production"))
Dir.chdir(app_path) do
assert File.exist?("config/credentials/production.key")
assert File.exist?("config/credentials/production.yml.enc")
end
end
test "show credentials" do test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command) assert_match(/access_key_id: 123/, run_show_command)
end end
@ -70,17 +78,25 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
remove_file "config/master.key" remove_file "config/master.key"
add_to_config "config.require_master_key = false" add_to_config "config.require_master_key = false"
assert_match(/Missing master key to decrypt credentials/, run_show_command) assert_match(/Missing 'config\/master\.key' to decrypt credentials/, run_show_command)
end
test "show command displays content specified by environment option" do
run_edit_command(environment: "production")
assert_match(/access_key_id: 123/, run_show_command(environment: "production"))
end end
private private
def run_edit_command(editor: "cat") def run_edit_command(editor: "cat", environment: nil, **options)
switch_env("EDITOR", editor) do switch_env("EDITOR", editor) do
rails "credentials:edit" args = environment ? ["--environment", environment] : []
rails "credentials:edit", args, **options
end end
end end
def run_show_command(**options) def run_show_command(environment: nil, **options)
rails "credentials:show", **options args = environment ? ["--environment", environment] : []
rails "credentials:show", args, **options
end end
end end

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
class Rails::CredentialsTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
setup :build_app
teardown :teardown_app
test "reads credentials from environment specific path" do
with_credentials do |content, key|
Dir.chdir(app_path) do
Dir.mkdir("config/credentials")
File.write("config/credentials/production.yml.enc", content)
File.write("config/credentials/production.key", key)
end
app("production")
assert_equal "revealed", Rails.application.credentials.mystery
end
end
test "reads credentials from customized path and key" do
with_credentials do |content, key|
Dir.chdir(app_path) do
Dir.mkdir("config/credentials")
File.write("config/credentials/staging.yml.enc", content)
File.write("config/credentials/staging.key", key)
end
add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')")
add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')")
app("production")
assert_equal "revealed", Rails.application.credentials.mystery
end
end
private
def with_credentials
key = "2117e775dc2024d4f49ddf3aeb585919"
# secret_key_base: secret
# mystery: revealed
content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
yield(content, key)
end
end