Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active April 30, 2023 00:03
Show Gist options
  • Save palkan/e27e4885535ff25753aefce45378e0cb to your computer and use it in GitHub Desktop.
Save palkan/e27e4885535ff25753aefce45378e0cb to your computer and use it in GitHub Desktop.
Backport Rails 6 per-environment credentials

Backport Rails 6 per-environment credentials to Rails 5.2

Rails PR: rails/rails#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
Copy link

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
Copy link
Author

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
Copy link

ldthorne commented Feb 5, 2020

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

@Fcukit
Copy link

Fcukit commented Mar 1, 2022

Hi!
My project has got Rails 5.2.3.
I made as such as you proposed above, but I came across at this:

[2] pry(main)> AWSConfig.new
NoMethodError: undefined method `relative_path_from' for "/app/config/credentials/development.yml.enc":String
from /usr/local/bundle/gems/anyway_config-2.2.2/lib/anyway/rails/loaders/credentials.rb:56:in `credentials_path'

Go to debug and see:

[2] pry(main)> Rails.application.config.credentials => {:content_path=>"/app/config/credentials/development.yml.enc", :key_path=>"/app/config/credentials/development.key"}

I made a little fix in your code at /lib/backport_rails_six_credentials.rb#15 as:
@credentials.content_path = Pathname default_credentials_content_path and it works!

Have you ideas, why it does not work without my fix? Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment