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))
|
>>> secret_key = secret_key + ''.join(random.choice(string.digits) for x in range(6))
|
||||||
|
|
||||||
|
|
||||||
.. data:: LEMUR_ENCRYPTION_KEY
|
.. data:: LEMUR_ENCRYPTION_KEYS
|
||||||
:noindex:
|
:noindex:
|
||||||
|
|
||||||
The LEMUR_ENCRYPTION_KEY is used to encrypt data at rest within Lemur's database. Without this key Lemur will refuse
|
The LEMUR_ENCRYPTION_KEYS is used to encrypt data at rest within Lemur's database. Without a key Lemur will refuse
|
||||||
to start.
|
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
|
Certificate Default Options
|
||||||
|
|
|
@ -4,8 +4,8 @@ Frequently Asked Questions
|
||||||
Common Problems
|
Common Problems
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
In my startup logs I see *'Aborting... Lemur cannot locate db encryption key, is LEMUR_ENCRYPTION_KEY set?'*
|
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_KEY**. See
|
You likely have not correctly configured **LEMUR_ENCRYPTION_KEYS**. See
|
||||||
:doc:`administration/index` for more information.
|
:doc:`administration/index` for more information.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -13,9 +13,7 @@ from cryptography.hazmat.backends import default_backend
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
|
||||||
|
|
||||||
from sqlalchemy_utils import EncryptedType
|
from lemur.utils import Vault
|
||||||
|
|
||||||
from lemur.utils import get_key
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
|
|
||||||
|
@ -213,7 +211,7 @@ class Certificate(db.Model):
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
owner = Column(String(128))
|
owner = Column(String(128))
|
||||||
body = Column(Text())
|
body = Column(Text())
|
||||||
private_key = Column(EncryptedType(String, get_key))
|
private_key = Column(Vault)
|
||||||
status = Column(String(128))
|
status = Column(String(128))
|
||||||
deleted = Column(Boolean, index=True)
|
deleted = Column(Boolean, index=True)
|
||||||
name = Column(String(128))
|
name = Column(String(128))
|
||||||
|
|
|
@ -72,7 +72,7 @@ SECRET_KEY = '{flask_secret_key}'
|
||||||
|
|
||||||
# You should consider storing these separately from your config
|
# You should consider storing these separately from your config
|
||||||
LEMUR_TOKEN_SECRET = '{secret_token}'
|
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
|
# this is a list of domains as regexes that only admins can issue
|
||||||
LEMUR_RESTRICTED_DOMAINS = []
|
LEMUR_RESTRICTED_DOMAINS = []
|
||||||
|
@ -171,7 +171,9 @@ def generate_settings():
|
||||||
settings file.
|
settings file.
|
||||||
"""
|
"""
|
||||||
output = CONFIG_TEMPLATE.format(
|
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)),
|
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
|
||||||
flask_secret_key=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.orm import relationship
|
||||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey
|
||||||
|
|
||||||
from sqlalchemy_utils import EncryptedType
|
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.utils import get_key
|
from lemur.utils import Vault
|
||||||
from lemur.models import roles_users
|
from lemur.models import roles_users
|
||||||
|
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ class Role(db.Model):
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String(128), unique=True)
|
name = Column(String(128), unique=True)
|
||||||
username = Column(String(128))
|
username = Column(String(128))
|
||||||
password = Column(EncryptedType(String, get_key))
|
password = Column(Vault)
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
authority_id = Column(Integer, ForeignKey('authorities.id'))
|
||||||
user_id = Column(Integer, ForeignKey('users.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
|
# You should consider storing these separately from your config
|
||||||
LEMUR_TOKEN_SECRET = 'test'
|
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
|
# this is a list of domains as regexes that only admins can issue
|
||||||
LEMUR_RESTRICTED_DOMAINS = []
|
LEMUR_RESTRICTED_DOMAINS = []
|
||||||
|
|
|
@ -5,17 +5,93 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
|
import six
|
||||||
from flask import current_app
|
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:
|
: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:
|
try:
|
||||||
return current_app.config.get('LEMUR_ENCRYPTION_KEY').strip()
|
keys = current_app.config.get('LEMUR_ENCRYPTION_KEYS')
|
||||||
except RuntimeError:
|
except:
|
||||||
print("No Encryption Key Found")
|
print("no encryption keys")
|
||||||
return ''
|
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