Storing External API Keys for a Different Application in Rails

Suppose your Rails application needs to store and access a different application's API Keys on behalf of a user. For example, each user has a Google Calendar API Key. How do you secure sensitive credentials in a meaningful way? There is well-known information security principle called "encryption at rest". This principle states that where ever data is stored, it is stored in an encrypted state rather than plain text. There are a few ways to accomplish this, however, two common themes are: full disk encryption, and application-level data encryption. In our example case, we will look at an example of storing encrypted External API credentials (ie. not generated by our application) in an application database using Lockbox. Let's take a look at an example with lockbox.

Note: please read Andrew's amazing article about storing sensitive credentials in Rails, which has been paraphrased and added to below.

Enter Lockbox

Lockbox is a gem designed for application level encryption with consideration for database ORMs like ActiveRecord. The gem allows developers to store encrypted ciphertext database fields and retrieve values in a decrypted state at runtime.

A basic Lockbox workflow is composed of three steps.

  1. A ciphertext column for the attribute must be added to the model table. For example, consider this fictional migration which adds a ciphertext field to a User's credit_card attribute:
class AddCreditCardCipherTextToUsers < ActiveRecord::Migration[6.0]
  def change
   add_column :users, :credit_card_ciphertext, :text
  end
end

2. Once the cipher text field is in place, the model with the encrypted attribute must invoke the encrypts class method provided by Lockbox to be able to retrieve the plain text attribute.

class User < ApplicationRecord
  encrypts :credit_card
end

3. After the model and database are wired up with Lockbox, the encrypted attribute can be used like normal!

user = User.first
# => <User ...>

user.credit_card 
# => 4539393950884198

Simple gems such as Lockbox are critical for ensuring that security is not a painful afterthought in development, and instead considered naturally during rapid development with Ruby on Rails. While Lockbox is a powerful gem with  other functionalities, this overview provides us just enough to get started using it.

Setting up Lockbox and Rails Credentials

Configuration for Lockbox begins by first adding the gem to our Gemfile and running our trusty bundle install command to pull down the dependency from Ruby Gems.

# Gemfile [ref27]
# --- snipped --- 
gem 'lockbox', '~> 0.4.8'
# --- snipped --- 

Once the gem is installed we can follow the documentation for integrating Lockbox. The starting point is generating a key that is used to encrypt the credentials before the data is stored in the database. Thankfully, Lockbox does this for us with the Lockbox.generate_key method. Boot up the Rails console and generate an encryption key for your application

$ bundle exec rails console
> Lockbox.generate_key
#=> "c81d492244e7590a27fb9264119e58b5ecafeae4b3985e92db8de94ada0bd4b5"

With the key created, we now need to determine a place to store it safely! We do not want to check it into version control as plain text, so what can we do? Rails has an answer for us!

With the release of Rails 5, the Rails core team introduced credentials. The new tools allows us to define and store sensitive data in a single YAML file that is only decryptable with the config/master.key. From the accompanying Rails guide:

Rails stores secrets in config/credentials.yml.enc, which is encrypted and hence cannot be edited directly. Rails uses config/master.key or alternatively looks for the environment variable  ENV["RAILS_MASTER_KEY"] to encrypt the credentials file. Because the credentials file is encrypted, it can be stored in version control, as long as the master key is kept safe."

Once encrypted, credentials are then accessible at runtime by calling Rails.application.credentials, along with the associated key name. The code snippet below provides an example of Rails credentials:

# Example credentials.yml file!

secret_key_base: abc1235....
an_api_key: KEY_TO_AWS
another_api_key: KEY_TO_DIGITAL_OCeAN

To open and edit your application credentials by running bundle exec rails credentials:edit. By default, Rails generates and stores secret_key_base credentials which are used for verifying and signing cookies for the application.

Within the credentials file, add an entry for the lockbox_key and save the file:

 # config/credentials.yml.enc

 # Auto generated for you don't touch!
 secret_key_base: abc123

 # Put an entry for your lockbox key
 lockbox_key: c81d492244e7590a27fb9264119e58b5ecafeae4b3985e92db8de94ada0bd4b5

Upon saving, Rails will re-encrypt the new document using the config/master.key present in the application. This leads us to the next configuration step which is informing Lockbox of its master_key in an initializer. Go ahead and create a file called config/initializers/lockbox.rb and add the entry for your lockbox_key.

# config/initializers/lockbox.rb
Lockbox.master_key = Rails.application.credentials.lockbox_key

With the initializer complete Lockbox is ready for action. We will now be able to encrypt and decrypt sensitive credential fields on an Api Key model which we will create next.

Migration

With Lockbox in place, we are ready to generate the  API Key model. Recall from the previous sections that we will be encrypting the sensitive fields such as token and passphrase for an API key with Lockbox. This means we will be creating ciphertext fields for these attributes instead of normal ones. With this in mind, go ahead and generate the model using the Rails generation command:

$ bundle exec rails g model ExternalApiKey \
	name:string \ 
    users:refereneces
	token_ciphertext:text \ 
	passphrase_ciphertext:text

invoke  active_record
		 create    db/migrate/20210516144521_create_external_api_keys.rb
		 create    app/models/external_api_key.rb
		 invoke    test_unit
		 create      test/models/external_api_key_test.rb
		 create      test/fixtures/external_api_keys.yml

If Rails willingly complies with your request, you should have a brand new migration similar to the code listing below.

# db/migrate/YYYYMMMDD_create_external_api_keys.rb

class CreateExternalApiKeys < ActiveRecord::Migration[6.0]
 def change
  create_table :external_api_keys do |t|
 	 t.string :name
     t.references :user, null: false, foreign_key: true
 	 t.text :token_ciphertext
 	 t.text :passphrase_ciphertext
 
 	 t.timestamps
  end
 end
end

After double checking your migration is correct, update the database by running one of our favourite commands:

 $ bundle exec rails db:migrate

Model

Once the model is created, we can begin by adding the Lockbox invocation on the model to ensure that we can encrypt and decrypt the sensitive attributes.

# app/models/external_api_key.rb

class ExternalApiKey < ApplicationRecord
  belongs_to :user
  encrypts :token, :passphrase ❶
end

Recall that the migration from the previous section created ciphertext equivalents for the token and passphrase fields. To ensure that we can retrieve the plain text variants of these fields, we call the encrypts class method provided by Lockbox to enable decryption and usage of these fields ❶. Lockbox will now encrypt and decrypt the sensitive External API Credentials for the model.