diff --git a/README.markdown b/README.markdown index dec1c31..69312a5 100644 --- a/README.markdown +++ b/README.markdown @@ -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 diff --git a/binder/models.py b/binder/models.py index b880967..de9cc9a 100644 --- a/binder/models.py +++ b/binder/models.py @@ -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): diff --git a/binder/settings.py b/binder/settings.py index 9f5dce3..1b4f134 100644 --- a/binder/settings.py +++ b/binder/settings.py @@ -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: diff --git a/binder/tests/testModels.py b/binder/tests/testModels.py index 5828888..1787231 100644 --- a/binder/tests/testModels.py +++ b/binder/tests/testModels.py @@ -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)) diff --git a/binder/views.py b/binder/views.py index 85a8f13..62494d2 100644 --- a/binder/views.py +++ b/binder/views.py @@ -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, diff --git a/requirements.txt b/requirements.txt index 799ea60..7a81325 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +cryptography Django>=1.10 dnspython>=1.11 git+https://github.com/jforman/pybindxml@0.6.1