kms_rails
kms_rails (based on kms_attrs) is a gem for easily adding Amazon Web Services KMS encryption to your ActiveRecord model attributes and ActiveJob arguments. It uses the GenerateDataKey method to perform "envelope" encryption locally with an OpenSSL AES-256-CBC cipher.
It improves upon kms_attrs by adding support for ActiveJob argument encryption, moving to a more efficient serialization model and introducing a fairly comprehensive test suite.
Getting started
Add this line to your application's Gemfile:
gem 'kms_rails'
And then execute:
$ bundle
Or install it yourself as:
$ gem install kms_rails
ActiveRecord
To use on ActiveRecord, simply put the following code in your models for the fields you want to encrypt:
kms_attr :my_attribute, key_id: 'my-aws-kms-key-id'
Encryption is done at time of assignment and is stored in the real database field 'my_attribute_enc'.
To retrieve the decrypted data, call:
my_model_instance.my_attribute
Encrypted data is stored as a MessagePack blob in your database in the #{my_attribute}_enc
column. It should be a binary column of sufficient size to store the encrypted data + metadata (suggested 65535).
You can also toggle whether or not the model instance should retain decrypted values. Default is false. Change to true if you want to reduce the AWS API calls made for constant decryption. The security implications of enabling or disabling retaining are not commented upon.
kms_attr :my_attribute, key_id: 'my-aws-kms-key-id',
retain: true
To clear a retained decrypted value, call:
my_model_instance.my_attribute_clear
This will attempt mutate the stored string to contain just null bytes, and then dereference it to be garbage collected. No guarantees are provided about additional copies of the retained data being cached elsewhere.
Data Serialization
By default kms_rails will convert your encrypted values into strings, however if you would like higher level structures to be stored, you can set msgpack: true
on any kms_attr declaration. This will encode and decode your values using MessagePack, as long as those value types are supported by it.
ActiveJob
To use on ActiveJob, simply put the following code in your job for the arguments you wish to encrypt in flight.
kms_arg 0, key_id: 'my-aws-kms-key-id'
kms_args [0, 1], key_id: 'my-aws-kms-key-id'
# The below sample will ensure param1 is encrypted before entering the job store
class TestJob < ActiveJob::Base
kms_arg 0, key_id: 'my-aws-kms-key-id'
def perform(param1, param2)
# Do things
end
end
Note: You can only declare kms_arg/kms_args once. Use kms_args if you want to encrypt multiple arguments. Seperate keys per argument are not implemented at this time.
Encryption is done when the job is seralized into the data store and is stored as a JSON hash of the necessary encyption information.
The encryption is automatically reversed when the job is deserialized.
Data Serialization
Like kms_attr above, by default your encrypted kms_args values are converted to and from strings. Similarly, you can set msgpack: true
to enable msgpack serialization and deserialization for arguments instead.
Already encrypted parameters
You also have the option of passing the value from your ActiveRecord encrypted field directly into the ActiveJob. If you do this, the value will not be encrypted twice. However, if you do this, you must ensure that the encryption key ID is the same for both the ActiveRecord attribute and ActiveJob parameter. It is also wise to use the same msgpack:
configuration options for both instances to ensure it is correctly decoded.
For instance, if you want to enqueue an encrypted value into a job on a node that cannot decrypt that value, you could do something like this;
value = MyModel.find(10).secret_field_enc
MyImportantJob.perform_later(value)
In this instance, value
will not be decrypted, nor encrypted twice.
Additional Options
You can add encryption contexts as strings or procs to kms_attr and kms_arg/args. Default is none.
kms_attr :my_attribute, key_id: 'my-aws-kms-key-id',
context_key: 'my context key', context_value: 'my context value'
kms_attr :my_attribute, key_id: 'my-aws-kms-key-id',
context_key: Proc.new { }, context_value: Proc.new { }
Aws Configuration
This gem expects some standard Aws SDK configuration. The Aws client is initiated with no credentials. This should then load credentials either from ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'], Aws
object, or an IAM role on an EC2 instance.
You can configure your region in a Rails initializer with;
Aws.config[:region] = 'us-east-1'
by using the documented AWS environmental variables, or by passing a Aws::KMS::Client
instance with your own config set (see below).
Custom KMS client
A basic fake implementation of Aws::KMS::Client
has been written (KmsRails::KmsClientMock
), allowing kms_rails functionality to be used in test environments without making any web requests. The fake implementation emulates the functionality of the two API calls kms_rails issues to AWS and performs fake encryption (the key is 'encrypted' by reversing it).
You can enable it (or set any custom KMS client with alternate config) in your Rails initializers with the following
KmsRails.configure do |config|
config.kms_client = KmsRails::KmsClientMock.new
end
Alias prefixes
You can use the alias_prefix
configuration option to automatically add a prefix to the key_ids that you specify. For example;
KmsRails.configure do |config|
config.alias_prefix = Rails.env + '/'
end
kms_attr :my_attribute, key_id: 'my-key-alias'
Will resolve 'my-key-alias' to 'alias/production/my-key-alias' in the production environment, and 'alias/staging/my-key-alias' in staging.
Directly specifying a key_id as a UUID or with the alias/
prefix explicitly declared will prevent this behaviour from occurring.
ARN prefixes
You can use the arn_prefix
configuration option to specify that the keys you're referencing are located in a different AWS account or region than the default. For example;
KmsRails.configure do |config|
config.arn_prefix = 'arn:aws:kms:ap-southeast-1:11111111111:'
end
kms_attr :my_attribute, key_id: 'my-key-alias'
Will resolve 'my-key-alias' to 'arn:aws:kms:ap-southeast-1:11111111111:alias/my-key-alias', which may be a key in a different region or AWS account.
This works for aliases and UUID keys, but Proc based key_ids will not have the ARN prefixed.
You can use this in combination with alias prefixes. A prefix like 'foo/' would result in a final key of 'arn:aws:kms:ap-southeast-1:11111111111:alias/foo/my-key-alias'.
Other stuff
Notes
This gem has been developed against Ruby 2.3.1, Rails 4.2, and AWS SDK v3. Credit where credit is due, strongbox by spikex was used as an inspiration and guide when creating this. https://github.com/spikex/strongbox
Disclaimer
No claims are made about enhanced security when using this gem.
Read more about AWS KMS
Development
After checking out the repo, run bin/setup
to install dependencies. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/appbot/kms_rails.