Portunus
Portunus is an opininated, object-oriented encryption engine built for Ruby on Rails applications. It utilizes a KEK (Key Encryption Key) & DEK (Data Encryption Key) scheme.
KEK keys should be stored outside the application. Portunus provides a few default adaptors for working with common deployment setups. While this is more secure than having unencrypted database data, the best use of Portunus would connect to and do the encryption / decryption of your keys inside of an HSM. Portunus is easily extensible to accomplish this but it's not included due to the extensive variety of possible deployments.
Lastly, Portnus has scripts included to do automatic rotation of these keys. It's important to rotate both master keys and data encryption keys. Scripts are included for both of these that can be scheduled via cron.
Background
Privacy and security need to be considered from the very start of building an
application. While web development has gotten more accessible, application
security has not. Portunus is intended to be a drop in utility that requires
just minutes of set up to ensure your app is using a DEK and KEK encryption
key to protect your database from several types of attacks. If you want to
go futher, it's easily extensible for your custom solution.
Installation
Install the gem
$ gem "portunus"
And then execute:
$ bundle
Or install it yourself as:
$ gem install portunus
Run the generator. This will create the required Portunus tables.
$ rails generate portunus:install
$ rails db:migrate
Include the encryptable module on any of your models or add it to
ApplicationRecord
to ensure all your models have access to field encryption.
include Portunus::Encryptable
Set up your master keys
Portunus comes with two adaptors for your master keys, "credentials" and "environment". This should cover the most common deploy scenarios. Before Portunus can function, enabled master keys need to be added. There is a generator to create the keys for you to then install in the proper location.
$ bundle exec rake portunus:generate_master_keys
If you are using the credentials adaptor (default), add the keys here. Make sure to generate keys for each environment.
$ bundle exec rails credentials:edit --environment=development
Spring / Postgres / OSX
When using this combination a bug may arise that prompts a weird error message:
$ objc[4182]: +[__NSPlaceholderDictionary initialize] may have been in progress in another thread when fork() was called.
You can circumvent it by using the below command in High Sierra / Catalina. It might not work in Mojave but I believe this issue unrelated to Portunus. Alternatively just don't use spring.
$ export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
Additional devise notes
There is additional configuration required if you are using devise and desire to encrypt your email column. Devise will by default downcase email addresses. The downcasing performed by devise happens after the portunus encryption and result in broken encrypted values. This behaviour needs to be disabled from devise and you will need to handle the downcasing prior to encryption.
Basic Configuration
To enable encryption on a column, add the encrypted_fields
method in the
model and give it the fields you want to encrypt.
class Member < ApplicationRecord
encrypted_fields :email
end
Database level defaults
Since the database does not have access to your encryption engine, default values will break the encryption. You need to ensure defaults for encrypted columns are set within your application logic.
Type casting
In order to provide a simpler implementation in your app, Portunus has type casting support. The encrypted data must be stored as strings. To utilize Portunus with different types, you may specify the type on the field.
class User < ApplicationRecord
encrypted_fields :email, :firstname, birthdate: { type: :date }
end
Supported types
- Boolean (:boolean)
- Date (:date)
- DateTime (:datetime)
- Float (:float)
- Integer (:integer)
- String (:string) (default)
Hashing
Encrypted data cannot be searched. Portunus provides an automatic hash
mechanism for encrypted data. The hashing happens prior to validation on the
model and will take your encrypted_field and put it into a column with a name
of hashed_encrypted_field
.
For instance, a migration for this may look like.
create_table :members do |t|
t.string :hashed_email, null: false
t.string :email, null: false
end
and the model:
class Member < ApplicationRecord
encrypted_fields :email
end
Portunus::Hasher
There is a class provided to perform the hashing that you can utilize to look up date.
User.find_by(email: ::Portunus::Hasher.for(params[:email])
Advanced Setup
Configuration block
Portunus can be easily customized using a configuration initializer.
Portunus.configuration do |config|
config.storage_adaptor = Portunus::StorageAdaptors::Credentials
config.encrypter = Portunus::Encrypters::OpenSslAes
config.max_key_duration = 1.month
end
Options
-
storage_adaptor
- This is finds and looks up master keys. -
encrypter
- This is responsible for setting the encrypter that encrypts decrypts the data. -
max_key_duration
- Timeframe for how old you want to allow keys to exist for. Ideally your keys are constantly being rotated. Used in key rotation tasks.
Storage adaptors
Storage adaptors provide the interface to determine which master key to decrypt a data key. Portunus comes with two adaptors to access master keys out of the box.
- Portunus::StorageAdaptors::Environment
- Portunus::StorageAdaptors::Credentials
We need to keep track of the following items:
- Key name - This is what is stored on the data encryption key table to find the master key
- Enabled - Whether the key is enabled for new data keys. Note: If you disable a key, that just stops future keys from generating. Until all the keys are rotated, do not remove the key.
- Created date - When the key was created to help track rotation duration
The master key id is stored on the data key table. These adaptors work like hash maps. The key id is passed and a value is returned. The value for both default adaptors is the master key. However if you were writing for an environment where keys are stored inside an HSM the value could be the key id in the HSM. The encrypter would then take that key id and interface with the HSM.
Adaptors are easily registered in the config so you can take an existing one and customize to your requirements.
EnvironmentAdaptor
Store and manage keys through any environment. Great for deployments like Heroku. The environment adaptor needs multiple keys per master key to track the key value, date created and enabled.
Credentials adaptor (default)
This gets your master keys from your rails credential files. An example structure is:
portunus:
f9e59a8c17c5f430f17745a522ebc2b7:
key: 93a05a5ce18afb85162a34d552c953b3
enabled: true
created_at: "2020-03-13T12:11:11+01:00"
140f33e69f0647cbc14b64605f002ff6:
key: d2c2aa9b7aeff75513ca24efcd8b8dd3
enabled: true
created_at: "2020-03-13T12:11:11+01:00"
Key rotation
Portunus provides key rotation scripts to rotate DEKs, KEKs, and both at once. The DEK rotation script will rotate keys every six months. If provided a force option as an environment variable it will rotate all the keys. The KEK rotation will rotate all master keys. This will probably take a long time in many apps so therefore you can rotate the master keys invidually by providing the key name.
$ bundle exec rake portunus:rotate_deks
$ FORCE=true bundle exec rake portunus:rotate_deks
$ bundle exec rake portunus:rotate_keks
$ KEY_NAME=<keyname> bundle exec rake portunus:rotate_deks
Tips
- Security is about applying layers. Using Portunus with the default configuration helps protects against specific types of attacks. However, in the event your complete environment is compromised there is not much that can be done.
- Providing seperation of concerns within your organization can help if you need the data to survive even if someone gets direct server access. Every aspect of Portunus is easily configured to ensure this is possible for you.
- When deciding how many master keys to use, keep the amount of data in mind. Each key is responsible for encrypting a certain number of DEKs. The lower this is kept the easier it will be to rotate.
- Schedule rotation often. The dek rotator can be run every day or on even smaller intervals.
Improvements
Some items I'd like to see added:
- Migration support from an unencrypted to encrypted column
- Google Cloud HSM Encrypter
- Improve key rotations
- Research better devise solution.
- Different encrypters or key sources for different data rows
- Dashboard to show key usage and which keys can be removed
- Automatic master key introduction and rotation
Development
After checking out the repo, run `bundle install to install dependencies.
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/colinpetruno/portunus. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Portunus project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.