diff --git a/conftest.py b/conftest.py index cf50ba1..00ef0c7 100644 --- a/conftest.py +++ b/conftest.py @@ -14,8 +14,8 @@ TEST_HOST2 = 'test2.' + BASEDOMAIN TEST_SECRET2 = "somethingelse" NAMESERVER_IP = "85.10.192.104" NAMESERVER_UPDATE_ALGORITHM = "HMAC_SHA512" -# no problem, you can ONLY update the TEST_HOST with this key, nothing else: -NAMESERVER_UPDATE_KEY = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==" +# no problem, you can ONLY update the TEST_HOST with this secret, nothing else: +NAMESERVER_UPDATE_SECRET = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQ==" NAMESERVER_PUBLIC = True USERNAME = 'test' @@ -43,7 +43,7 @@ def db_init(db): # note: db is a predefined fixture and required here to have t domain=TEST_HOST, # special: single-host update secret! nameserver_ip=NAMESERVER_IP, nameserver_update_algorithm=NAMESERVER_UPDATE_ALGORITHM, - nameserver_update_key=NAMESERVER_UPDATE_KEY, + nameserver_update_secret=NAMESERVER_UPDATE_SECRET, public=NAMESERVER_PUBLIC, created_by=u, ) @@ -52,7 +52,7 @@ def db_init(db): # note: db is a predefined fixture and required here to have t domain=BASEDOMAIN, nameserver_ip=NAMESERVER_IP, nameserver_update_algorithm=NAMESERVER_UPDATE_ALGORITHM, - nameserver_update_key='invalid=', # we don't send updates there (and the real key is really secret) + nameserver_update_secret='invalid=', # we don't send updates there (and the real key is really secret) public=NAMESERVER_PUBLIC, created_by=u2, ) diff --git a/docs/security.rst b/docs/security.rst index b543a89..80ec427 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -86,21 +86,21 @@ If you lose the secret, you'll have to generate a new one and change it in your update client also. -Nameserver Update Key (backend, RFC 2136) ------------------------------------------ +Nameserver Update Secret (backend, RFC 2136) +-------------------------------------------- -We currently store this key (which is basically a base64 encoded shared secret, +We currently store this secret (which is basically a base64 encoded shared secret, one per dynamic zone) "as is" into the database ("Domain" records there). This is somehow critical, but also hard to do better - encryption would only -help very little here as we would need to decrypt the update key before using it, +help very little here as we would need to decrypt the update secret before using it, so we would need the unlocked decryption key on the same machine. -Make sure no unauthorized person gets that key or he/she will be able to update +Make sure no unauthorized person gets that secret or he/she will be able to update ANY record in the respective zone / nameserver directly (without going over nsupdate.info software / service). -We support creating random update keys, so you don't need an extra tool for this. +We support creating a random update secret, so you don't need an extra tool for this. CSRF protection diff --git a/nsupdate/main/dnstools.py b/nsupdate/main/dnstools.py index 7b434be..ae608aa 100644 --- a/nsupdate/main/dnstools.py +++ b/nsupdate/main/dnstools.py @@ -203,12 +203,12 @@ def parse_name(fqdn, origin=None): def get_ns_info(fqdn, origin=None): """ - Get the master nameserver for the zone, the key needed + Get the master nameserver for the zone, the key secret needed to update the zone and the key algorithm used. :param fqdn: the fully qualified hostname we are dealing with (str) :param origin: zone we are dealing with, must be with trailing dot (default:autodetect) (str) - :return: master nameserver, origin, domain, update keyname, update key, update algo + :return: master nameserver, origin, domain, update keyname, update secret, update algo :raises: NameServerNotAvailable if ns was flagged unavailable in the db """ fqdn_str = str(fqdn) @@ -238,7 +238,7 @@ def get_ns_info(fqdn, origin=None): # retry timeout is over, set it available again set_ns_availability(domain, True) algorithm = getattr(dns.tsig, d.nameserver_update_algorithm) - return d.nameserver_ip, origin, domain, name, keyname, d.nameserver_update_key, algorithm + return d.nameserver_ip, origin, domain, name, keyname, d.nameserver_update_secret, algorithm def update_ns(fqdn, rdtype='A', ipaddr=None, origin=None, action='upd', ttl=60): diff --git a/nsupdate/main/forms.py b/nsupdate/main/forms.py index cad4cbe..515ea4a 100644 --- a/nsupdate/main/forms.py +++ b/nsupdate/main/forms.py @@ -33,4 +33,4 @@ class EditDomainForm(forms.ModelForm): class Meta(object): model = Domain fields = ['comment', 'nameserver_ip', 'public', 'available', - 'nameserver_update_algorithm', 'nameserver_update_key'] + 'nameserver_update_algorithm', 'nameserver_update_secret'] diff --git a/nsupdate/main/migrations/0018_auto__del_field_domain_nameserver_update_key__add_field_domain_nameser.py b/nsupdate/main/migrations/0018_auto__del_field_domain_nameserver_update_key__add_field_domain_nameser.py new file mode 100644 index 0000000..ee9f6fc --- /dev/null +++ b/nsupdate/main/migrations/0018_auto__del_field_domain_nameserver_update_key__add_field_domain_nameser.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # renaming field 'Domain.nameserver_update_key' + db.rename_column(u'main_domain', 'nameserver_update_key', 'nameserver_update_secret') + + def backwards(self, orm): + # renaming field 'Domain.nameserver_update_secret' + db.rename_column(u'main_domain', 'nameserver_update_secret', 'nameserver_update_key') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'main.blacklisteddomain': { + 'Meta': {'object_name': 'BlacklistedDomain'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'main.domain': { + 'Meta': {'object_name': 'Domain'}, + 'available': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'domain': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'nameserver_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'nameserver_update_algorithm': ('django.db.models.fields.CharField', [], {'default': "'HMAC_SHA512'", 'max_length': '16'}), + 'nameserver_update_secret': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '88'}), + 'public': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + u'main.host': { + 'Meta': {'unique_together': "(('subdomain', 'domain'),)", 'object_name': 'Host'}, + 'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': u"orm['auth.User']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['main.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_update': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'last_update_ipv4': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'last_update_ipv6': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'ssl_update_ipv4': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'ssl_update_ipv6': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'subdomain': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'update_secret': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + } + } + + complete_apps = ['main'] diff --git a/nsupdate/main/models.py b/nsupdate/main/models.py index 8d6af22..c583065 100644 --- a/nsupdate/main/models.py +++ b/nsupdate/main/models.py @@ -60,8 +60,9 @@ class Domain(models.Model): nameserver_ip = models.GenericIPAddressField( max_length=40, # ipv6 = 8 * 4 digits + 7 colons help_text="IP where the dynamic DNS updates for this zone will be sent to") - nameserver_update_key = models.CharField( + nameserver_update_secret = models.CharField( max_length=88, # 512 bits base64 -> 88 bytes + default='', help_text="Shared secret that allows updating this zone (base64 encoded)") nameserver_update_algorithm = models.CharField( max_length=16, # see elements of UPDATE_ALGORITHM_CHOICES @@ -94,9 +95,9 @@ class Domain(models.Model): algorithm = self.nameserver_update_algorithm bitlength = UPDATE_ALGORITHMS[algorithm].bitlength secret = User.objects.make_random_password(length=bitlength / 8) - self.nameserver_update_key = key = base64.b64encode(secret) + self.nameserver_update_secret = secret_base64 = base64.b64encode(secret) self.save() - return key + return secret_base64 def get_bind9_algorithm(self): return UPDATE_ALGORITHMS.get(self.nameserver_update_algorithm).bind_name