encrypt TSIG keys using Fernet cryptography library
This commit is contained in:
parent
fba94af629
commit
1af8f1e31b
|
@ -90,6 +90,14 @@ python manage.py createsuperuser
|
||||||
python manage.py dumpdata -o binder/fixtures/initial_data.json
|
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 ##
|
## External configuration ##
|
||||||
|
|
||||||
Aside from the Binder application itself, other infrastructure is required
|
Aside from the Binder application itself, other infrastructure is required
|
||||||
|
|
|
@ -5,6 +5,7 @@ import binascii
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
# 3rd Party
|
# 3rd Party
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
from pybindxml import reader as bindreader
|
from pybindxml import reader as bindreader
|
||||||
import dns.exception
|
import dns.exception
|
||||||
import dns.query
|
import dns.query
|
||||||
|
@ -14,6 +15,7 @@ import dns.zone
|
||||||
# App Imports
|
# App Imports
|
||||||
from binder import exceptions
|
from binder import exceptions
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
|
TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
|
||||||
('hmac-sha1', 'SHA1'),
|
('hmac-sha1', 'SHA1'),
|
||||||
|
@ -24,10 +26,7 @@ TSIG_ALGORITHMS = (('HMAC-MD5.SIG-ALG.REG.INT', 'MD5'),
|
||||||
|
|
||||||
class Key(models.Model):
|
class Key(models.Model):
|
||||||
|
|
||||||
"""Store and reference TSIG keys.
|
"""Store and reference TSIG keys."""
|
||||||
|
|
||||||
TODO: Should/Can we encrypt these DNS keys in the DB?
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255,
|
name = models.CharField(max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
|
@ -46,17 +45,38 @@ class Key(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["name"]
|
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):
|
def create_keyring(self):
|
||||||
if self.name is None:
|
if self.name is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
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:
|
except binascii.Error, err:
|
||||||
raise exceptions.KeyringException("Incorrect key data. Verify key: %s. Reason: %s" % (self.name, err))
|
raise exceptions.KeyringException("Incorrect key data. Verify key: %s. Reason: %s" % (self.name, err))
|
||||||
|
|
||||||
return keyring
|
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):
|
class BindServer(models.Model):
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Django settings for binder project.
|
# Django settings for binder project.
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -163,6 +164,16 @@ RECORD_TYPE_CHOICES = (("A", "A"),
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/'
|
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:
|
try:
|
||||||
from local_settings import *
|
from local_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -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 django.db import IntegrityError
|
||||||
|
|
||||||
from binder import models
|
from binder import exceptions, models
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Model_BindServer_Tests(TestCase):
|
class Model_BindServer_Tests(TestCase):
|
||||||
|
@ -40,3 +43,27 @@ class Model_Key_Tests(TestCase):
|
||||||
def test_NonExistantKey(self):
|
def test_NonExistantKey(self):
|
||||||
with self.assertRaisesMessage(models.Key.DoesNotExist, "Key matching query does not exist"):
|
with self.assertRaisesMessage(models.Key.DoesNotExist, "Key matching query does not exist"):
|
||||||
models.Key.objects.get(name="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))
|
||||||
|
|
|
@ -53,6 +53,13 @@ def view_zone_records(request, dns_server, zone_name):
|
||||||
return render(request, "bcommon/list_zone.html",
|
return render(request, "bcommon/list_zone.html",
|
||||||
{"zone_name": zone_name,
|
{"zone_name": zone_name,
|
||||||
"dns_server": this_server})
|
"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",
|
return render(request, "bcommon/list_zone.html",
|
||||||
{"zone_array": zone_array,
|
{"zone_array": zone_array,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
cryptography
|
||||||
Django>=1.10
|
Django>=1.10
|
||||||
dnspython>=1.11
|
dnspython>=1.11
|
||||||
git+https://github.com/jforman/pybindxml@0.6.1
|
git+https://github.com/jforman/pybindxml@0.6.1
|
||||||
|
|
Loading…
Reference in New Issue