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:
parent
e184d1a94e
commit
e0d3313bac
7 changed files with 163 additions and 29 deletions
|
@ -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*
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
49
railties/test/credentials_test.rb
Normal file
49
railties/test/credentials_test.rb
Normal 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
|
Loading…
Reference in a new issue