Skip to content

Instantly share code, notes, and snippets.

@palkan

palkan/01_Readme.md

Last active Sep 30, 2020
Embed
What would you like to do?
Backport Rails 6 per-environment credentials

Backport Rails 6 per-environment credentials to Rails 5.2

Rails PR: https://github.com/rails/rails/pull/33521

This patch makes it possible to use per-environment credentials (i.e., config/credentials/staging.yml.enc) in Rails 5.2.

Installation

  • Drop backport_rails_six_credentials.rb and backport_rails_six_credentials_command.rb somewhere, for example, into the lib/ folder
  • Add this line to config/application.rb:
# Right after `require "rails"`

require_relative "../lib/backport_rails_six_credentials"
  • Add this line to config/boot.rb:
# Right after `require 'bundler/setup'`
require_relative "../lib/backport_rails_six_credentials_command"

Usage

Now you can call:

$ bundle exec rails credentials:edit -e staging

create  config/credentials/staging.key
...

And Rails.application.credentials now uses env-specific credentials if they're present and master/root credentials otherwise.

# frozen_string_literal: true
Rails::Application.class_eval do
def credentials
@credentials ||= encrypted(config.credentials.content_path, key_path: config.credentials.key_path)
end
end
Rails::Application::Configuration.prepend(Module.new do
attr_accessor :credentials
def initialize(*)
super
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
end
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)
# frozen_string_literal: true
tracer = TracePoint.new(:class) { |event|
next unless event.self.name == "Rails::Command::CredentialsCommand"
tracer.disable
Rails::Command::CredentialsCommand.class_eval do
class_option :environment, aliases: "-e", type: :string,
desc: "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
end
Rails::Command::CredentialsCommand.prepend(Module.new do
def edit
require_application_and_environment!
ensure_editor_available(command: "bin/rails credentials:edit") || (return)
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
change_encrypted_file_in_system_editor(content_path, key_path, env_key)
end
say "File encrypted and saved."
rescue ActiveSupport::MessageEncryptor::InvalidMessage
say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?"
end
def show
require_application_and_environment!
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
private
def content_path
options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
end
def key_path
options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
end
def env_key
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}")
end
end
def encryption_key_file_generator
require "rails/generators"
require "rails/generators/rails/encryption_key_file/encryption_key_file_generator"
Rails::Generators::EncryptionKeyFileGenerator.new
end
def encrypted_file_generator
require "rails/generators"
require "rails/generators/rails/encrypted_file/encrypted_file_generator"
Rails::Generators::EncryptedFileGenerator.new
end
def missing_encrypted_message(key:, key_path:, file_path:)
if key.nil?
"Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
else
"File '#{file_path}' does not exist. Use `rails credentials:edit` to change that."
end
end
end)
}
tracer.enable
@ldthorne

This comment has been minimized.

Copy link

@ldthorne ldthorne commented Feb 5, 2020

Hi @palkan! We ran into an issue in which the RAILS_MASTER_KEY environment variable was being used to decrypt the environment-specific credentials. We modified backport_rails_six_credentials.rb to do something similar to what backport_rails_six_credentials_command.rb uses the env_key method for.

backport_rails_six_credentials.rb

Rails::Application.class_eval do
  def credentials 
    @credentials ||= encrypted(
      config.credentials.content_path,
      key_path: config.credentials.key_path,
      env_key: config.credentials.env_key
    )
  end
end

...

Rails::Application::Configuration.prepend(Module.new do
  attr_accessor :credentials

  def initialize(*)
    super
    @credentials = ActiveSupport::OrderedOptions.new
    @credentials.content_path = default_credentials_content_path
    @credentials.key_path = default_credentials_key_path
    @credentials.env_key = env_key
  end

  def env_key
    "RAILS_#{Rails.env.upcase}_KEY"
  end
end

Was there any intentional reason not to do it this way?

@palkan

This comment has been minimized.

Copy link
Owner Author

@palkan palkan commented Feb 5, 2020

Hi Daniel,

Was there any intentional reason not to do it this way?

The idea is that you only set RAILS_MASTER_KEY in production-like environments, where it's impossible to run the app in some other env.

Thus, this approach assumes that for local development you use config/environments/credentials/<env>.key or config/master.key and not env vars.

@ldthorne

This comment has been minimized.

Copy link

@ldthorne ldthorne commented Feb 5, 2020

Ah gotcha. That makes sense. Thanks for the clarification!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.