encrypt TSIG keys using Fernet cryptography library

This commit is contained in:
Jeffrey Forman 2017-05-02 20:21:11 -04:00
parent fba94af629
commit 1af8f1e31b
6 changed files with 81 additions and 7 deletions

View File

@ -90,6 +90,14 @@ python manage.py createsuperuser
python manage.py dumpdata -o binder/fixtures/initial_data.json
```
### Encrypted TSIG Keys ###
Starting with version 1.5, TSIG keys inside the database are encrypted using the [Crytography](https://cryptography.io/en/latest/fernet/) library and Fernet facilities.
Normally on startup, a new Fernet encryption key is created. This will change upon reboot as the process dies and restarts.
If you wish to use a statically configured encryption/decryption key, one must pass the DJANGO_FERNET_KEY environment variable, containing this key string. This *should* be used in production. This key *MUST* be kept secret or your TSIG keys will be able to be decrypted.
## External configuration ##
Aside from the Binder application itself, other infrastructure is required

View File

@ -5,6 +5,7 @@ import binascii
import socket
# 3rd Party
from cryptography.fernet import Fernet, InvalidToken
from pybindxml import reader as bindreader
import dns.exception
import dns.query
@ -14,6 +15,7 @@ import dns.zone
# App Imports
from binder import exceptions
from django.db import models
from django.conf import settings
TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
('hmac-sha1', 'SHA1'),
@ -24,10 +26,7 @@ TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
class Key(models.Model):
"""Store and reference TSIG keys.
TODO: Should/Can we encrypt these DNS keys in the DB?
"""
"""Store and reference TSIG keys."""
name = models.CharField(max_length=255,
unique=True,
@ -46,17 +45,38 @@ class Key(models.Model):
class Meta:
ordering = ["name"]
def save(self, *args, **kwargs):
f = Fernet(settings.FERNET_KEY)
crypted_key = f.encrypt(bytes(self.data))
self.data = crypted_key
super(Key, self).save(*args, **kwargs)
def create_keyring(self):
if self.name is None:
return None
try:
keyring = dns.tsigkeyring.from_text({self.name: self.data})
key_data = self.decrypt_keydata()
keyring = dns.tsigkeyring.from_text({self.name: key_data})
except binascii.Error, err:
raise exceptions.KeyringException("Incorrect key data. Verify key: %s. Reason: %s" % (self.name, err))
return keyring
def decrypt_keydata(self, key=None):
if key:
fernet_key=key
else:
fernet_key=settings.FERNET_KEY
try:
f = Fernet(fernet_key)
decrypted_key = f.decrypt(bytes(self.data))
except InvalidToken:
raise exceptions.KeyringException()
return decrypted_key
class BindServer(models.Model):

View File

@ -1,6 +1,7 @@
# Django settings for binder project.
import logging
import os
from cryptography.fernet import Fernet
from django.contrib.messages import constants as messages
logger = logging.getLogger(__name__)
@ -163,6 +164,16 @@ RECORD_TYPE_CHOICES = (("A", "A"),
LOGIN_REDIRECT_URL = '/'
# TSIG Encryption Key
# If not passed as an environment variable,
# create a new Fernet key for encrypting the TSIG key.
# NOTE: In production, you'll want to pass your own key in.
# Otherwise, on successive Binder restarts, you will not be able
# to decrypt your TSIG Key and perform DNS updates because the keys
# would have changed.
FERNET_KEY=os.environ.get("DJANGO_FERNET_KEY", Fernet.generate_key())
try:
from local_settings import *
except ImportError:

View File

@ -1,7 +1,10 @@
from django.test import TestCase
from cryptography.fernet import Fernet, InvalidToken
from django.conf import settings
from django.test import TestCase, override_settings
from django.db import IntegrityError
from binder import models
from binder import exceptions, models
import os
class Model_BindServer_Tests(TestCase):
@ -40,3 +43,27 @@ class Model_Key_Tests(TestCase):
def test_NonExistantKey(self):
with self.assertRaisesMessage(models.Key.DoesNotExist, "Key matching query does not exist"):
models.Key.objects.get(name="does_not_exist")
@override_settings(FERNET_KEY='yfE1kyYLNlpR-2ybdB-Mvs_k1ZoDMFFVtE_PpWYxVgs=')
def test_FernetKeyDecryptionSuccess(self):
"""Test encrypt/decryption when Fernet key is generated by Django."""
original_tsig_key = 'oGyDayyZ2mDUiJCuTUODnA=='
key_1 = models.Key(name='testencryptedkey1',
data=original_tsig_key,
algorithm='MD5')
key_1.save()
decrypt_key = Fernet(settings.FERNET_KEY)
decrypted_tsig_key = decrypt_key.decrypt(bytes(key_1.data))
self.assertEqual(original_tsig_key, decrypted_tsig_key)
@override_settings(FERNET_KEY='yfE1kyYLNlpR-2ybdB-Mvs_k1ZoDMFFVtE_PpWYxVgs=')
def test_FernetKeyDecryptionFailure(self):
"""Test encrypt/decryption when Fernet key changes."""
original_tsig_key = 'oGyDayyZ2mDUiJCuTUODnA=='
key_1 = models.Key(name='testencryptedkey1',
data=original_tsig_key,
algorithm='MD5')
key_1.save()
new_fkey = Fernet(Fernet.generate_key())
with self.assertRaises(InvalidToken):
decrypted_tsig_key = new_fkey.decrypt(bytes(key_1.data))

View File

@ -53,6 +53,13 @@ def view_zone_records(request, dns_server, zone_name):
return render(request, "bcommon/list_zone.html",
{"zone_name": zone_name,
"dns_server": this_server})
except KeyringException:
messages.error(request, "Unable to get zone list. A problem was encountered "
"decrypting your TSIG key. Ensure the key is correctly "
"specified in the Binder Database.")
return render(request, "bcommon/list_zone.html",
{ "dns_server": this_server,
"zone_name" :zone_name })
return render(request, "bcommon/list_zone.html",
{"zone_array": zone_array,

View File

@ -1,3 +1,4 @@
cryptography
Django>=1.10
dnspython>=1.11
git+https://github.com/jforman/pybindxml@0.6.1