Diffing Rails credentials in Rails 6.0

rails secrets

Rails credentials are a pretty nice feature to deal with secrets in your Rails application. Instead of setting a whole list of secrets in your server’s environment variables, you only have to set the decryption key RAILS_MASTER_KEY (or RAILS_PRODUCTION_KEY if you are using environment-specific credentials).

Something that can be annoying with Rail’s credentials is that it is hard to resolve merge-conflicts within them or to compare your current credentials to some other branch’s credentials since git’s diff will only show you blobs.

Since Rails version 6.1 there is a credential diff task which will help with this, but I am working on a Rails 6.0 project right now and there are some dependency issues that don’t allow us to upgrade yet.

Some people have suggested a .gitattributes based diffing option, but that didn’t play nice for me with multi-environment credentials.

This led to me writing the following script:

#!/usr/bin/env ruby
require 'open3'

def branch_exists?(branch_name)
  o, s = Open3.capture2("git rev-parse --verify #{branch_name}")

  s.success?
end

def write_credentials_to_file(credentials_path, key_path, target_path)
  o, s = Open3.capture2("bin/rails encrypted:show --key #{key_path} #{credentials_path}")
  File.write(target_path, o)
end

def write_credential_to_file(environment, target_path, credentials_base: "config/credentials")
  write_credentials_to_file("#{credentials_base}/#{environment}.yml.enc", "config/credentials/#{environment}.key", target_path)
end

def compare_conflicted_credentials(environment)
  `git checkout --ours config/credentials/#{environment}.yml.enc`
  write_credential_to_file(environment, 'tmp/ours')

  `git checkout --theirs config/credentials/#{environment}.yml.enc`
  write_credential_to_file(environment, 'tmp/theirs')

  puts "\n\n\n"
  puts "'Ours' is what is in master, 'theirs' is what is in your branch'"
  puts "\n\n\n"
  puts `icdiff tmp/ours tmp/theirs`

ensure
  `rm tmp/ours`
  `rm tmp/theirs`
end

def compare_to_branch(environment, branch_name)
  write_credential_to_file(environment, 'tmp/your_branch')

  `mkdir -p tmp/credentials`
  `git show #{branch_name}:config/credentials/#{environment}.yml.enc > tmp/credentials/#{environment}.yml.enc`
  write_credential_to_file(environment, 'tmp/other_branch', credentials_base: 'tmp/credentials')

  puts `icdiff tmp/your_branch tmp/other_branch`

ensure
  `rm tmp/your_branch`
  `rm tmp/other_branch`
  `rm -r tmp/credentials`
end

cred_path = ARGV[0]
raise 'USAGE: diff_my_creds.sh PATH_TO_CREDS' if cred_path.nil?

environment = cred_path.match(/config\/credentials\/([a-z]+).yml.enc/)[1]
raise "CANNOT DETERMINE ENVIRONMENT FROM #{cred_path}" if environment.nil?

compare_to_branch = ARGV[1] if ARGV[1]

if compare_to_branch
  raise "GIVEN BRANCH DOES NOT EXIST" unless branch_exists?(compare_to_branch)

  compare_to_branch(environment, compare_to_branch)
else
  compare_conflicted_credentials(environment)
end

`rails credentials:edit -e #{environment}`

Put this in bin/diff_my_creds.sh and do chmod +x bin/diff_my_creds.sh to make it runnable.

Usage when you have a conflict on lets say config/credentials/test.yml.enc:

bin/diff_my_creds.sh config/credentials/test.yml.enc

Usage when you want to diff with a branch:

bin/diff_my_creds.sh config/credentials/test.yml.enc master # or any other branch name than 'master'

Output:

tmp/your_branch                                         tmp/other_branch
  access_key_id: 123                                      access_key_id: 123
  secret_access_key: 345                                  secret_access_key: 345

some_service:                                           some_service:
  api_key: abc                                            api_key: abc
  nice_day_to: diff

A dependency for showing the diff in this way is icdiff which you can install with brew install icdiff, but you could replace the diff command with anything you want, the basic idea stays the same.