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.
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.
- 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
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.keyor 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.
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
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.