Skip to content

Instantly share code, notes, and snippets.

@palkan
Last active April 30, 2023 00:03
Show Gist options
  • Select an option

  • Save palkan/e27e4885535ff25753aefce45378e0cb to your computer and use it in GitHub Desktop.

Select an option

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

ldthorne commented Feb 5, 2020

Copy link
Copy Markdown

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

palkan commented Feb 5, 2020

Copy link
Copy Markdown
Author

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

ldthorne commented Feb 5, 2020

Copy link
Copy Markdown

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

@Fcukit

Fcukit commented Mar 1, 2022

Copy link
Copy Markdown

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