Use MultiFernet for encryption
Facilitates key rotation and uses more secure encryption than what sqlalchemy-utils does. Fixes #117 and #119.
This commit is contained in:
parent
90636a5329
commit
40eb950e94
|
@ -87,17 +87,23 @@ Basic Configuration
|
|||
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
|
||||
|
||||
|
||||
.. data:: LEMUR_ENCRYPTION_KEY
|
||||
.. data:: LEMUR_ENCRYPTION_KEYS
|
||||
:noindex:
|
||||
|
||||
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
|
||||
to start.
|
||||
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
|
||||
to start. Multiple keys can be provided to facilitate key rotation. The first key in the list is used for
|
||||
encryption and all keys are tried for decryption until one works. Each key must be 32 URL safe base-64 encoded bytes.
|
||||
|
||||
See `LEMUR_TOKEN_SECRET` for methods of secure secret generation.
|
||||
Running lemur create_config will securely generate a key for your configuration file.
|
||||
If you would like to generate your own, we recommend the following method:
|
||||
|
||||
>>> import os
|
||||
>>> import base64
|
||||
>>> base64.urlsafe_b64encode(os.urandom(32))
|
||||
|
||||
::
|
||||
|
||||
LEMUR_ENCRYPTION_KEY = 'supersupersecret'
|
||||
LEMUR_ENCRYPTION_KEYS = ['1YeftooSbxCiX2zo8m1lXtpvQjy27smZcUUaGmffhMY=', 'LAfQt6yrkLqOK5lwpvQcT4jf2zdeTQJV1uYeh9coT5s=']
|
||||
|
||||
|
||||
Certificate Default Options
|
||||
|
|
|
@ -4,8 +4,8 @@ Frequently Asked Questions
|
|||
Common Problems
|
||||
---------------
|
||||
|
||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
|
||||
You likely have not correctly configured **LEMUR_ENCRYPTION_KEY**. See
|
||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEYS set?'*
|
||||
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
|
||||
:doc:`administration/index` for more information.
|
||||
|
||||
|
||||
|
|
|
@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
|
|||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.database import db
|
||||
from lemur.plugins.base import plugins
|
||||
|
||||
|
@ -213,7 +211,7 @@ class Certificate(db.Model):
|
|||
id = Column(Integer, primary_key=True)
|
||||
owner = Column(String(128))
|
||||
body = Column(Text())
|
||||
private_key = Column(EncryptedType(String, get_key))
|
||||
private_key = Column(Vault)
|
||||
status = Column(String(128))
|
||||
deleted = Column(Boolean, index=True)
|
||||
name = Column(String(128))
|
||||
|
|
|
@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
|
|||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_TOKEN_SECRET = '{secret_token}'
|
||||
LEMUR_ENCRYPTION_KEY = '{encryption_key}'
|
||||
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
@ -171,7 +171,9 @@ def generate_settings():
|
|||
settings file.
|
||||
"""
|
||||
output = CONFIG_TEMPLATE.format(
|
||||
encryption_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
# we use Fernet.generate_key to make sure that the key length is
|
||||
# compatible with Fernet
|
||||
encryption_key=Fernet.generate_key(),
|
||||
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||
)
|
||||
|
|
|
@ -12,9 +12,8 @@
|
|||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||
|
||||
from sqlalchemy_utils import EncryptedType
|
||||
from lemur.database import db
|
||||
from lemur.utils import get_key
|
||||
from lemur.utils import Vault
|
||||
from lemur.models import roles_users
|
||||
|
||||
|
||||
|
@ -23,7 +22,7 @@ class Role(db.Model):
|
|||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(128), unique=True)
|
||||
username = Column(String(128))
|
||||
password = Column(EncryptedType(String, get_key))
|
||||
password = Column(Vault)
|
||||
description = Column(Text)
|
||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||
user_id = Column(Integer, ForeignKey('users.id'))
|
||||
|
|
|
@ -21,7 +21,7 @@ SECRET_KEY = 'I/dVhOZNSMZMqrFJa5tWli6VQccOGudKerq3eWPMSzQNmHHVhMAQfQ=='
|
|||
|
||||
# You should consider storing these separately from your config
|
||||
LEMUR_TOKEN_SECRET = 'test'
|
||||
LEMUR_ENCRYPTION_KEY = 'jPd2xwxgVGXONqghHNq7/S761sffYSrT3UAgKwgtMxbqa0gmKYCfag=='
|
||||
LEMUR_ENCRYPTION_KEYS = 'o61sBLNBSGtAckngtNrfVNd8xy8Hp9LBGDstTbMbqCY='
|
||||
|
||||
# this is a list of domains as regexes that only admins can issue
|
||||
LEMUR_RESTRICTED_DOMAINS = []
|
||||
|
|
|
@ -5,17 +5,93 @@
|
|||
:license: Apache, see LICENSE for more details.
|
||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||
"""
|
||||
import six
|
||||
from flask import current_app
|
||||
from cryptography.fernet import Fernet, MultiFernet
|
||||
import sqlalchemy.types as types
|
||||
|
||||
|
||||
def get_key():
|
||||
def get_keys():
|
||||
"""
|
||||
Gets the current encryption key
|
||||
Gets the encryption keys.
|
||||
|
||||
This supports multiple keys to facilitate key rotation. The first
|
||||
key in the list is used to encrypt. Decryption is attempted with
|
||||
each key in succession.
|
||||
|
||||
:return:
|
||||
"""
|
||||
|
||||
# when running lemur create_config, this code needs to work despite
|
||||
# the fact that there is not a current_app with a config at that point
|
||||
try:
|
||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
||||
except RuntimeError:
|
||||
print("No Encryption Key Found")
|
||||
return ''
|
||||
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||
except:
|
||||
print("no encryption keys")
|
||||
return []
|
||||
|
||||
# this function is expected to return a list of keys, but we want
|
||||
# to let people just specify a single key
|
||||
if not isinstance(keys, list):
|
||||
keys = [keys]
|
||||
|
||||
# make sure there is no accidental whitespace
|
||||
keys = [key.strip() for key in keys]
|
||||
|
||||
return keys
|
||||
|
||||
|
||||
class Vault(types.TypeDecorator):
|
||||
"""
|
||||
A custom SQLAlchemy column type that transparently handles encryption.
|
||||
|
||||
This uses the MultiFernet from the cryptography package to faciliate
|
||||
key rotation. That class handles encryption and signing.
|
||||
|
||||
Fernet uses AES in CBC mode with 128-bit keys and PKCS7 padding. It
|
||||
uses HMAC-SHA256 for ciphertext authentication. Initialization
|
||||
vectors are generated using os.urandom().
|
||||
"""
|
||||
|
||||
# required by SQLAlchemy. defines the underlying column type
|
||||
impl = types.Binary
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
"""
|
||||
Encrypt values on the way into the database.
|
||||
|
||||
MultiFernet.encrypt uses the first key in the list.
|
||||
"""
|
||||
|
||||
# we assume that the user's keys are already Fernet keys (32 byte
|
||||
# keys that have been base64 encoded).
|
||||
self.keys = [Fernet(key) for key in get_keys()]
|
||||
|
||||
# we only support strings and they should be of type bytes for Fernet
|
||||
if not isinstance(value, six.string_types):
|
||||
return None
|
||||
|
||||
value = bytes(value)
|
||||
|
||||
return MultiFernet(self.keys).encrypt(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
"""
|
||||
Decrypt values on the way out of the database.
|
||||
|
||||
MultiFernet tries each key until one works.
|
||||
"""
|
||||
|
||||
# we assume that the user's keys are already Fernet keys (32 byte
|
||||
# keys that have been base64 encoded).
|
||||
self.keys = [Fernet(key) for key in get_keys()]
|
||||
|
||||
# if the value is not a string we aren't going to try to decrypt
|
||||
# it. this is for the case where the column is null
|
||||
if not isinstance(value, six.string_types):
|
||||
return None
|
||||
|
||||
# TODO this may raise an InvalidToken exception in certain
|
||||
# cases. Should we handle that?
|
||||
# https://cryptography.io/en/latest/fernet/#cryptography.fernet.Fernet.decrypt
|
||||
return MultiFernet(self.keys).decrypt(value)
|
||||
|
|
Loading…
Reference in New Issue