Explorar o código

feat(api): introduce per-domain minimum TTL setting, fixes #216

Peter Thomassen %!s(int64=6) %!d(string=hai) anos
pai
achega
bbfd19526f

+ 1 - 0
.env.default

@@ -22,6 +22,7 @@ DESECSTACK_API_EMAIL_PORT=
 DESECSTACK_API_SECRETKEY=
 DESECSTACK_API_SECRETKEY=
 DESECSTACK_API_PSL_RESOLVER=
 DESECSTACK_API_PSL_RESOLVER=
 DESECSTACK_DBAPI_PASSWORD_desec=
 DESECSTACK_DBAPI_PASSWORD_desec=
+DESECSTACK_MINIMUM_TTL_DEFAULT=900
 DESECSTACK_NORECAPTCHA_SITE_KEY=
 DESECSTACK_NORECAPTCHA_SITE_KEY=
 DESECSTACK_NORECAPTCHA_SECRET_KEY=
 DESECSTACK_NORECAPTCHA_SECRET_KEY=
 
 

+ 1 - 0
.env.dev

@@ -22,6 +22,7 @@ DESECSTACK_API_EMAIL_PORT=
 DESECSTACK_API_SECRETKEY=insecure
 DESECSTACK_API_SECRETKEY=insecure
 DESECSTACK_API_PSL_RESOLVER=9.9.9.9
 DESECSTACK_API_PSL_RESOLVER=9.9.9.9
 DESECSTACK_DBAPI_PASSWORD_desec=insecure
 DESECSTACK_DBAPI_PASSWORD_desec=insecure
+DESECSTACK_MINIMUM_TTL_DEFAULT=
 DESECSTACK_NORECAPTCHA_SITE_KEY=
 DESECSTACK_NORECAPTCHA_SITE_KEY=
 DESECSTACK_NORECAPTCHA_SECRET_KEY=
 DESECSTACK_NORECAPTCHA_SECRET_KEY=
 
 

+ 1 - 0
.travis.yml

@@ -29,6 +29,7 @@ env:
    - DESECSTACK_IPV6_ADDRESS=bade:affe:dead:beef:b011:0642:ac10:0080
    - DESECSTACK_IPV6_ADDRESS=bade:affe:dead:beef:b011:0642:ac10:0080
    - DESECSTACK_WWW_CERTS=./certs
    - DESECSTACK_WWW_CERTS=./certs
    - DESECSTACK_DBMASTER_CERTS=./dbmastercerts
    - DESECSTACK_DBMASTER_CERTS=./dbmastercerts
+   - DESECSTACK_MINIMUM_TTL_DEFAULT=3600
    - DESECSTACK_NORECAPTCHA_SITE_KEY=9Fn33T5yGulkjhdidid
    - DESECSTACK_NORECAPTCHA_SITE_KEY=9Fn33T5yGulkjhdidid
    - DESECSTACK_NORECAPTCHA_SECRET_KEY=9Fn33T5yGulkjhoiwhetoi
    - DESECSTACK_NORECAPTCHA_SECRET_KEY=9Fn33T5yGulkjhoiwhetoi
 
 

+ 1 - 0
README.md

@@ -43,6 +43,7 @@ Although most configuration is contained in this repository, some external depen
       - `DESECSTACK_API_SECRETKEY`: Django secret
       - `DESECSTACK_API_SECRETKEY`: Django secret
       - `DESECSTACK_API_PSL_RESOLVER`: Resolver IP address to use for PSL lookups. If empty, the system's default resolver is used.
       - `DESECSTACK_API_PSL_RESOLVER`: Resolver IP address to use for PSL lookups. If empty, the system's default resolver is used.
       - `DESECSTACK_DBAPI_PASSWORD_desec`: mysql password for desecapi
       - `DESECSTACK_DBAPI_PASSWORD_desec`: mysql password for desecapi
+      - `DESECSTACK_MINIMUM_TTL_DEFAULT`: minimum TTL users can set for RRsets. The setting is per domain, and the default defined here is used on domain creation.
     - nslord-related
     - nslord-related
       - `DESECSTACK_DBLORD_PASSWORD_pdns`: mysql password for pdns on nslord
       - `DESECSTACK_DBLORD_PASSWORD_pdns`: mysql password for pdns on nslord
       - `DESECSTACK_NSLORD_APIKEY`: pdns API key on nslord
       - `DESECSTACK_NSLORD_APIKEY`: pdns API key on nslord

+ 1 - 0
api/api/settings.py

@@ -171,6 +171,7 @@ NORECAPTCHA_SECRET_KEY = os.environ['DESECSTACK_NORECAPTCHA_SECRET_KEY']
 NORECAPTCHA_WIDGET_TEMPLATE = 'captcha-widget.html'
 NORECAPTCHA_WIDGET_TEMPLATE = 'captcha-widget.html'
 
 
 # abuse protection
 # abuse protection
+MINIMUM_TTL_DEFAULT = int(os.environ['DESECSTACK_MINIMUM_TTL_DEFAULT'])
 ABUSE_BY_REMOTE_IP_LIMIT = 1
 ABUSE_BY_REMOTE_IP_LIMIT = 1
 ABUSE_BY_REMOTE_IP_PERIOD_HRS = 48
 ABUSE_BY_REMOTE_IP_PERIOD_HRS = 48
 ABUSE_BY_EMAIL_HOSTNAME_LIMIT = 1
 ABUSE_BY_EMAIL_HOSTNAME_LIMIT = 1

+ 23 - 0
api/desecapi/migrations/0004_domain_minimum_ttl.py

@@ -0,0 +1,23 @@
+# Generated by Django 2.2.3 on 2019-07-19 12:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('desecapi', '0003_validation'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='domain',
+            name='minimum_ttl',
+            field=models.PositiveIntegerField(default=60),
+        ),
+        migrations.AlterField(
+            model_name='rrset',
+            name='ttl',
+            field=models.PositiveIntegerField(),
+        ),
+    ]

+ 6 - 4
api/desecapi/models.py

@@ -11,7 +11,7 @@ import rest_framework.authtoken.models
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
 from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from django.core.validators import MinValueValidator, RegexValidator
+from django.core.validators import RegexValidator
 from django.db import models
 from django.db import models
 from django.db.models import Manager
 from django.db.models import Manager
 from django.utils import timezone
 from django.utils import timezone
@@ -153,13 +153,15 @@ class Domain(models.Model):
                                         ])
                                         ])
     owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='domains')
     owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='domains')
     published = models.DateTimeField(null=True, blank=True)
     published = models.DateTimeField(null=True, blank=True)
+    minimum_ttl = models.PositiveIntegerField(default=settings.MINIMUM_TTL_DEFAULT)
 
 
     @property
     @property
     def keys(self):
     def keys(self):
         return pdns.get_keys(self)
         return pdns.get_keys(self)
 
 
-    def partition_name(self):
-        subname, _, parent_name = self.name.partition('.')
+    def partition_name(domain):
+        name = domain.name if isinstance(domain, Domain) else domain
+        subname, _, parent_name = name.partition('.')
         return subname, parent_name or None
         return subname, parent_name or None
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
@@ -261,7 +263,7 @@ class RRset(models.Model):
             )
             )
         ]
         ]
     )
     )
-    ttl = models.PositiveIntegerField(validators=[MinValueValidator(1)])
+    ttl = models.PositiveIntegerField()
 
 
     objects = RRsetManager()
     objects = RRsetManager()
 
 

+ 5 - 1
api/desecapi/serializers.py

@@ -1,5 +1,6 @@
 import re
 import re
 
 
+from django.core.validators import MinValueValidator
 from django.db.models import Model, Q
 from django.db.models import Model, Q
 from djoser import serializers as djoser_serializers
 from djoser import serializers as djoser_serializers
 from rest_framework import serializers
 from rest_framework import serializers
@@ -167,6 +168,7 @@ class NonBulkOnlyDefault:
 class RRsetSerializer(ConditionalExistenceModelSerializer):
 class RRsetSerializer(ConditionalExistenceModelSerializer):
     domain = serializers.SlugRelatedField(read_only=True, slug_field='name')
     domain = serializers.SlugRelatedField(read_only=True, slug_field='name')
     records = RRsField(allow_empty=True)
     records = RRsField(allow_empty=True)
+    ttl = serializers.IntegerField(max_value=604800)
 
 
     class Meta:
     class Meta:
         model = RRset
         model = RRset
@@ -191,6 +193,7 @@ class RRsetSerializer(ConditionalExistenceModelSerializer):
         fields = super().get_fields()
         fields = super().get_fields()
         fields['subname'].validators.append(ReadOnlyOnUpdateValidator())
         fields['subname'].validators.append(ReadOnlyOnUpdateValidator())
         fields['type'].validators.append(ReadOnlyOnUpdateValidator())
         fields['type'].validators.append(ReadOnlyOnUpdateValidator())
+        fields['ttl'].validators.append(MinValueValidator(limit_value=self.domain.minimum_ttl))
         return fields
         return fields
 
 
     def get_validators(self):
     def get_validators(self):
@@ -425,10 +428,11 @@ class DomainSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Domain
         model = Domain
-        fields = ('created', 'published', 'name', 'keys')
+        fields = ('created', 'published', 'name', 'keys', 'minimum_ttl',)
         extra_kwargs = {
         extra_kwargs = {
             'name': {'trim_whitespace': False},
             'name': {'trim_whitespace': False},
             'published': {'read_only': True},
             'published': {'read_only': True},
+            'minimum_ttl': {'read_only': True},
         }
         }
 
 
     def get_fields(self):
     def get_fields(self):

+ 5 - 2
api/desecapi/tests/base.py

@@ -885,12 +885,15 @@ class DomainOwnerTestCase(DesecTestCase):
 
 
         cls.owner = cls.create_user(dyn=cls.DYN)
         cls.owner = cls.create_user(dyn=cls.DYN)
 
 
+        domain_kwargs = {'suffix': cls.AUTO_DELEGATION_DOMAINS if cls.DYN else None}
+        if cls.DYN:
+            domain_kwargs['minimum_ttl'] = 60
         cls.my_domains = [
         cls.my_domains = [
-            cls.create_domain(suffix=cls.AUTO_DELEGATION_DOMAINS if cls.DYN else None, owner=cls.owner)
+            cls.create_domain(owner=cls.owner, **domain_kwargs)
             for _ in range(cls.NUM_OWNED_DOMAINS)
             for _ in range(cls.NUM_OWNED_DOMAINS)
         ]
         ]
         cls.other_domains = [
         cls.other_domains = [
-            cls.create_domain(suffix=cls.AUTO_DELEGATION_DOMAINS if cls.DYN else None)
+            cls.create_domain(**domain_kwargs)
             for _ in range(cls.NUM_OTHER_DOMAINS)
             for _ in range(cls.NUM_OTHER_DOMAINS)
         ]
         ]
 
 

+ 16 - 0
api/desecapi/tests/test_domains.py

@@ -269,6 +269,14 @@ class DomainOwnerTestCase1(DomainOwnerTestCase):
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
             self.assertEqual(len(mail.outbox), 0)
             self.assertEqual(len(mail.outbox), 0)
 
 
+    def test_domain_minimum_ttl(self):
+        url = self.reverse('v1:domain-list')
+        name = self.random_domain_name()
+        with self.assertPdnsRequests(self.requests_desec_domain_creation(name=name)):
+            response = self.client.post(url, {'name': name})
+        self.assertStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(response.data['minimum_ttl'], settings.MINIMUM_TTL_DEFAULT)
+
 
 
 class LockedDomainOwnerTestCase1(LockedDomainOwnerTestCase):
 class LockedDomainOwnerTestCase1(LockedDomainOwnerTestCase):
 
 
@@ -377,6 +385,14 @@ class AutoDelegationDomainOwnerTests(DomainOwnerTestCase):
         self.assertStatus(response, status.HTTP_403_FORBIDDEN)
         self.assertStatus(response, status.HTTP_403_FORBIDDEN)
         self.assertEqual(len(mail.outbox), user_quota)
         self.assertEqual(len(mail.outbox), user_quota)
 
 
+    def test_domain_minimum_ttl(self):
+        url = self.reverse('v1:domain-list')
+        name = self.random_domain_name(self.AUTO_DELEGATION_DOMAINS)
+        with self.assertPdnsRequests(self.requests_desec_domain_creation_auto_delegation(name)):
+            response = self.client.post(url, {'name': name})
+        self.assertStatus(response, status.HTTP_201_CREATED)
+        self.assertEqual(response.data['minimum_ttl'], 60)
+
 
 
 class LockedAutoDelegationDomainOwnerTests(LockedDomainOwnerTestCase):
 class LockedAutoDelegationDomainOwnerTests(LockedDomainOwnerTestCase):
     DYN = True
     DYN = True

+ 22 - 10
api/desecapi/tests/test_rrsets.py

@@ -65,9 +65,9 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
     def test_create_my_rr_sets(self):
     def test_create_my_rr_sets(self):
         for subname in [None, 'create-my-rr-sets', 'foo.create-my-rr-sets', 'bar.baz.foo.create-my-rr-sets']:
         for subname in [None, 'create-my-rr-sets', 'foo.create-my-rr-sets', 'bar.baz.foo.create-my-rr-sets']:
             for data in [
             for data in [
-                {'subname': subname, 'records': ['1.2.3.4'], 'ttl': 60, 'type': 'A'},
-                {'subname': '' if subname is None else subname, 'records': ['desec.io.'], 'ttl': 900, 'type': 'PTR'},
-                {'subname': '' if subname is None else subname, 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
+                {'subname': subname, 'records': ['1.2.3.4'], 'ttl': 3660, 'type': 'A'},
+                {'subname': '' if subname is None else subname, 'records': ['desec.io.'], 'ttl': 36900, 'type': 'PTR'},
+                {'subname': '' if subname is None else subname, 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
             ]:
             ]:
                 # Try POST with missing subname
                 # Try POST with missing subname
                 if data['subname'] is None:
                 if data['subname'] is None:
@@ -132,7 +132,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
         self.assertStatus(response, status.HTTP_404_NOT_FOUND)
         self.assertStatus(response, status.HTTP_404_NOT_FOUND)
 
 
     def test_create_my_rr_sets_twice(self):
     def test_create_my_rr_sets_twice(self):
-        data = {'records': ['1.2.3.4'], 'ttl': 60, 'type': 'A'}
+        data = {'records': ['1.2.3.4'], 'ttl': 3660, 'type': 'A'}
         with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
         with self.assertPdnsRequests(self.requests_desec_rr_sets_update(self.my_empty_domain.name)):
             response = self.client.post_rr_set(self.my_empty_domain.name, **data)
             response = self.client.post_rr_set(self.my_empty_domain.name, **data)
             self.assertStatus(response, status.HTTP_201_CREATED)
             self.assertStatus(response, status.HTTP_201_CREATED)
@@ -153,9 +153,21 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             with self.assertPdnsRequests(
             with self.assertPdnsRequests(
                     self.request_pdns_zone_update_unknown_type(name=self.my_domain.name, unknown_types=_type)
                     self.request_pdns_zone_update_unknown_type(name=self.my_domain.name, unknown_types=_type)
             ):
             ):
-                response = self.client.post_rr_set(self.my_domain.name, records=['1234'], ttl=60, type=_type)
+                response = self.client.post_rr_set(self.my_domain.name, records=['1234'], ttl=3660, type=_type)
                 self.assertStatus(response, status.HTTP_422_UNPROCESSABLE_ENTITY)
                 self.assertStatus(response, status.HTTP_422_UNPROCESSABLE_ENTITY)
 
 
+    def test_create_my_rr_sets_insufficient_ttl(self):
+        ttl = settings.MINIMUM_TTL_DEFAULT - 1
+        response = self.client.post_rr_set(self.my_empty_domain.name, records=['1.2.3.4'], ttl=ttl, type='A')
+        self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
+        detail = f'Ensure this value is greater than or equal to {self.my_empty_domain.minimum_ttl}.'
+        self.assertEqual(response.data['ttl'][0], detail)
+
+        ttl += 1
+        with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_empty_domain.name)):
+            response = self.client.post_rr_set(self.my_empty_domain.name, records=['1.2.23.4'], ttl=ttl, type='A')
+        self.assertStatus(response, status.HTTP_201_CREATED)
+
     def test_retrieve_my_rr_sets_apex(self):
     def test_retrieve_my_rr_sets_apex(self):
         response = self.client.get_rr_set(self.my_rr_set_domain.name, subname='', type_='A')
         response = self.client.get_rr_set(self.my_rr_set_domain.name, subname='', type_='A')
         self.assertStatus(response, status.HTTP_200_OK)
         self.assertStatus(response, status.HTTP_200_OK)
@@ -172,19 +184,19 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
     def test_update_my_rr_sets(self):
     def test_update_my_rr_sets(self):
         for subname in self.SUBNAMES:
         for subname in self.SUBNAMES:
             with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
             with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
-                data = {'records': ['2.2.3.4'], 'ttl': 30, 'type': 'A', 'subname': subname}
+                data = {'records': ['2.2.3.4'], 'ttl': 3630, 'type': 'A', 'subname': subname}
                 response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
                 response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
                 self.assertStatus(response, status.HTTP_200_OK)
                 self.assertStatus(response, status.HTTP_200_OK)
 
 
             response = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A')
             response = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A')
             self.assertStatus(response, status.HTTP_200_OK)
             self.assertStatus(response, status.HTTP_200_OK)
             self.assertEqual(response.data['records'], ['2.2.3.4'])
             self.assertEqual(response.data['records'], ['2.2.3.4'])
-            self.assertEqual(response.data['ttl'], 30)
+            self.assertEqual(response.data['ttl'], 3630)
 
 
             response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'records': ['2.2.3.5']})
             response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'records': ['2.2.3.5']})
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
 
 
-            response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'ttl': 37})
+            response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'ttl': 3637})
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
 
 
     def test_update_my_rr_set_with_invalid_payload_type(self):
     def test_update_my_rr_set_with_invalid_payload_type(self):
@@ -205,10 +217,10 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
         for subname in self.SUBNAMES:
         for subname in self.SUBNAMES:
             current_rr_set = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A').data
             current_rr_set = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A').data
             for data in [
             for data in [
-                {'records': ['2.2.3.4'], 'ttl': 30},
+                {'records': ['2.2.3.4'], 'ttl': 3630},
                 {'records': ['3.2.3.4']},
                 {'records': ['3.2.3.4']},
                 {'records': ['3.2.3.4', '9.8.8.7']},
                 {'records': ['3.2.3.4', '9.8.8.7']},
-                {'ttl': 37},
+                {'ttl': 3637},
             ]:
             ]:
                 with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
                 with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
                     response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
                     response = self.client.patch_rr_set(self.my_rr_set_domain.name, subname, 'A', data)

+ 25 - 24
api/desecapi/tests/test_rrsets_bulk.py

@@ -1,5 +1,6 @@
 import copy
 import copy
 
 
+from django.conf import settings
 from rest_framework import status
 from rest_framework import status
 
 
 from desecapi.tests.base import AuthenticatedRRSetBaseTestCase
 from desecapi.tests.base import AuthenticatedRRSetBaseTestCase
@@ -12,8 +13,8 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
         super().setUpTestDataWithPdns()
         super().setUpTestDataWithPdns()
 
 
         cls.data = [
         cls.data = [
-            {'subname': 'my-bulk', 'records': ['1.2.3.4'], 'ttl': 60, 'type': 'A'},
-            {'subname': 'my-bulk', 'records': ['desec.io.', 'foobar.example.'], 'ttl': 60, 'type': 'PTR'},
+            {'subname': 'my-bulk', 'records': ['1.2.3.4'], 'ttl': 3600, 'type': 'A'},
+            {'subname': 'my-bulk', 'records': ['desec.io.', 'foobar.example.'], 'ttl': 3600, 'type': 'PTR'},
         ]
         ]
 
 
         cls.data_no_records = copy.deepcopy(cls.data)
         cls.data_no_records = copy.deepcopy(cls.data)
@@ -115,22 +116,22 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
             self.client.bulk_post_rr_sets(
             self.client.bulk_post_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                    {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
-                    {'subname': '', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                    {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
+                    {'subname': '', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'SOA',
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'SOA',
                      'records': ['ns1.desec.io. peter.desec.io. 2018034419 10800 3600 604800 60']},
                      'records': ['ns1.desec.io. peter.desec.io. 2018034419 10800 3600 604800 60']},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'OPT', 'records': ['9999']},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'OPT', 'records': ['9999']},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
                 ]
                 ]
             ),
             ),
             status.HTTP_400_BAD_REQUEST,
             status.HTTP_400_BAD_REQUEST,
             [
             [
                 {'type': ['This field is required.']},
                 {'type': ['This field is required.']},
-                {'ttl': ['Ensure this value is greater than or equal to 1.']},
+                {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
                 {'subname': ['This field is required.']},
                 {'subname': ['This field is required.']},
                 {},
                 {},
                 {'ttl': ['This field is required.']},
                 {'ttl': ['This field is required.']},
@@ -192,7 +193,7 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
 
 
     def test_bulk_patch_change_ttl(self):
     def test_bulk_patch_change_ttl(self):
         data_no_records = copy.deepcopy(self.data_no_records)
         data_no_records = copy.deepcopy(self.data_no_records)
-        data_no_records[1]['ttl'] = 911
+        data_no_records[1]['ttl'] = 3911
         with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.bulk_domain.name)):
         with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.bulk_domain.name)):
             response = self.client.bulk_patch_rr_sets(domain_name=self.bulk_domain.name, payload=data_no_records)
             response = self.client.bulk_patch_rr_sets(domain_name=self.bulk_domain.name, payload=data_no_records)
             self.assertStatus(response, status.HTTP_200_OK)
             self.assertStatus(response, status.HTTP_200_OK)
@@ -212,7 +213,7 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
             self.client.bulk_patch_rr_sets(
             self.client.bulk_patch_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': 'a', 'type': 'A', 'records': [], 'ttl': 22},
+                    {'subname': 'a', 'type': 'A', 'records': [], 'ttl': 3622},
                     {'subname': 'b', 'type': 'AAAA', 'records': []},
                     {'subname': 'b', 'type': 'AAAA', 'records': []},
                 ]),
                 ]),
             status.HTTP_200_OK,
             status.HTTP_200_OK,
@@ -224,25 +225,25 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
             self.client.bulk_post_rr_sets(
             self.client.bulk_post_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': '', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                    {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA', 'ttl': 3},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA', 'records': ['::1', '::2']},
+                    {'subname': '', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                    {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA', 'ttl': 3603},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
                 ]
                 ]
             )
             )
         self.assertResponse(
         self.assertResponse(
             self.client.bulk_patch_rr_sets(
             self.client.bulk_patch_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                    {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                    {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
                 ]),
                 ]),
             status.HTTP_400_BAD_REQUEST,
             status.HTTP_400_BAD_REQUEST,
             [
             [
                 {'type': ['This field is required.']},
                 {'type': ['This field is required.']},
-                {'ttl': ['Ensure this value is greater than or equal to 1.']},
+                {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
                 {'subname': ['This field is required.']},
                 {'subname': ['This field is required.']},
                 {},
                 {},
                 {},
                 {},
@@ -254,23 +255,23 @@ class AuthenticatedRRSetBulkTestCase(AuthenticatedRRSetBaseTestCase):
             self.client.bulk_post_rr_sets(
             self.client.bulk_post_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': '', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']}
+                    {'subname': '', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
                 ]
                 ]
             )
             )
         self.assertResponse(
         self.assertResponse(
             self.client.bulk_patch_rr_sets(
             self.client.bulk_patch_rr_sets(
                 domain_name=self.my_empty_domain.name,
                 domain_name=self.my_empty_domain.name,
                 payload=[
                 payload=[
-                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                    {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                     {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                    {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                    {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                     {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                    {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
+                    {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
                 ]),
                 ]),
             status.HTTP_400_BAD_REQUEST,
             status.HTTP_400_BAD_REQUEST,
             [
             [
                 {'type': ['This field is required.']},
                 {'type': ['This field is required.']},
-                {'ttl': ['Ensure this value is greater than or equal to 1.']},
+                {'ttl': [f'Ensure this value is greater than or equal to {settings.MINIMUM_TTL_DEFAULT}.']},
                 {'subname': ['This field is required.']},
                 {'subname': ['This field is required.']},
                 {'ttl': ['This field is required.']},
                 {'ttl': ['This field is required.']},
                 {'records': ['This field is required.']},
                 {'records': ['This field is required.']},

+ 7 - 3
api/desecapi/views.py

@@ -168,11 +168,15 @@ class DomainList(ListCreateAPIView):
             ex.status_code = status.HTTP_403_FORBIDDEN
             ex.status_code = status.HTTP_403_FORBIDDEN
             raise ex
             raise ex
 
 
+        parent_domain_name = Domain.partition_name(domain_name)[1]
+        domain_is_local = parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES
         try:
         try:
             with PDNSChangeTracker():
             with PDNSChangeTracker():
-                domain = serializer.save(owner=self.request.user)
-            parent_domain_name = domain.partition_name()[1]
-            if parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES:
+                domain_kwargs = {'owner': self.request.user}
+                if domain_is_local:
+                    domain_kwargs['minimum_ttl'] = 60
+                domain = serializer.save(**domain_kwargs)
+            if domain_is_local:
                 parent_domain = Domain.objects.get(name=parent_domain_name)
                 parent_domain = Domain.objects.get(name=parent_domain_name)
                 # NOTE we need two change trackers here, as the first transaction must be committed to
                 # NOTE we need two change trackers here, as the first transaction must be committed to
                 # pdns in order to have keys available for the delegation
                 # pdns in order to have keys available for the delegation

+ 1 - 0
docker-compose.yml

@@ -121,6 +121,7 @@ services:
     - DESECSTACK_NSLORD_APIKEY
     - DESECSTACK_NSLORD_APIKEY
     - DESECSTACK_NSLORD_DEFAULT_TTL
     - DESECSTACK_NSLORD_DEFAULT_TTL
     - DESECSTACK_NSMASTER_APIKEY
     - DESECSTACK_NSMASTER_APIKEY
+    - DESECSTACK_MINIMUM_TTL_DEFAULT
     - DESECSTACK_NORECAPTCHA_SITE_KEY
     - DESECSTACK_NORECAPTCHA_SITE_KEY
     - DESECSTACK_NORECAPTCHA_SECRET_KEY
     - DESECSTACK_NORECAPTCHA_SECRET_KEY
     networks:
     networks:

+ 14 - 1
docs/domains.rst

@@ -16,7 +16,6 @@ A JSON object representing a domain has the following structure::
 
 
     {
     {
         "created": "2018-09-18T16:36:16.510368Z",
         "created": "2018-09-18T16:36:16.510368Z",
-        "name": "example.com",
         "keys": [
         "keys": [
             {
             {
                 "dnskey": "257 3 13 WFRl60...",
                 "dnskey": "257 3 13 WFRl60...",
@@ -31,6 +30,8 @@ A JSON object representing a domain has the following structure::
             },
             },
             ...
             ...
         ],
         ],
+        "minimum_ttl": 3600,
+        "name": "example.com",
         "published": "2018-09-18T17:21:38.348112Z"
         "published": "2018-09-18T17:21:38.348112Z"
     }
     }
 
 
@@ -62,6 +63,18 @@ Field details:
       We look at each active ``cryptokey_resource`` (``active`` is true) and
       We look at each active ``cryptokey_resource`` (``active`` is true) and
       then use the ``dnskey``, ``ds``, ``flags``, and ``keytype`` fields.
       then use the ``dnskey``, ``ds``, ``flags``, and ``keytype`` fields.
 
 
+.. _`minimum TTL`:
+
+``minimum_ttl``
+    :Access mode: read-only
+
+    Smallest TTL that can be used in an `RRset <RRset object_>`__. The value
+    is set automatically by the server.
+
+    If you would like to use lower TTL values, you can apply for an exception
+    by contacting support.  We reserve the right to reject applications at our
+    discretion.
+
 ``name``
 ``name``
     :Access mode: read, write-once (upon domain creation)
     :Access mode: read, write-once (upon domain creation)
 
 

+ 2 - 2
docs/rrsets.rst

@@ -92,8 +92,8 @@ Field details:
     :Access mode: read, write
     :Access mode: read, write
 
 
     TTL (time-to-live) value, which dictates for how long resolvers may cache
     TTL (time-to-live) value, which dictates for how long resolvers may cache
-    this RRset, measured in seconds.  Only positive integer values are allowed.
-    Additional restrictions may apply.
+    this RRset, measured in seconds.  The smallest acceptable value is given by
+    the domain's `minimum TTL`_ setting.  The maximum value is 604800 (one week).
 
 
 ``type``
 ``type``
     :Access mode: read, write-once (upon RRset creation)
     :Access mode: read, write-once (upon RRset creation)

+ 95 - 95
test/e2e/spec/api_spec.js

@@ -433,8 +433,8 @@ describe("API v1", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 22 },
-                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 33 }
+                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
+                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(201);
                         expect(response).to.have.status(201);
@@ -443,13 +443,13 @@ describe("API v1", function () {
                     });
                     });
 
 
                     itPropagatesToTheApi([
                     itPropagatesToTheApi([
-                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 22, records: ['dead::beef']},
-                        {subname: '', domain: domain, type: 'MX', ttl: 33, records: ['10 mail.example.com.', '20 mail.example.net.']},
+                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
+                        {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
                     ]);
                     ]);
 
 
-                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 22);
+                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
 
 
-                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 33);
+                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
                 });
                 });
 
 
                 describe("cannot bulk-post with missing or invalid fields", function () {
                 describe("cannot bulk-post with missing or invalid fields", function () {
@@ -457,27 +457,27 @@ describe("API v1", function () {
                         // Set an RRset that we'll try to overwrite
                         // Set an RRset that we'll try to overwrite
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            {'ttl': 50, 'type': 'TXT', 'records': ['"foo"']}
+                            {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
                         );
                         );
                         expect(response).to.have.status(201);
                         expect(response).to.have.status(201);
 
 
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                                {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'SOA', 'records': ['ns1.desec.io. peter.desec.io. 2018034419 10800 3600 604800 60']},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'OPT', 'records': ['9999']},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'SOA', 'records': ['ns1.desec.io. peter.desec.io. 2018034419 10800 3600 604800 60']},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'OPT', 'records': ['9999']},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'TYPE099', 'records': ['v=spf1 mx -all']},
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(400);
                         expect(response).to.have.status(400);
                         expect(response).to.have.json([
                         expect(response).to.have.json([
                             { type: [ 'This field is required.' ] },
                             { type: [ 'This field is required.' ] },
-                            { ttl: [ 'Ensure this value is greater than or equal to 1.' ] },
+                            { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
                             { subname: [ 'This field is required.' ] },
                             { subname: [ 'This field is required.' ] },
                             { ttl: [ 'This field is required.' ] },
                             { ttl: [ 'This field is required.' ] },
                             { records: [ 'This field is required.' ] },
                             { records: [ 'This field is required.' ] },
@@ -500,7 +500,7 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 }),
                                 }),
                             ]);
                             ]);
@@ -514,11 +514,11 @@ describe("API v1", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'c.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'delete-test', 'ttl': 50, 'type': 'A', 'records': ['127.1.2.3']},
-                                {'subname': 'replace-test-1', 'ttl': 50, 'type': 'AAAA', 'records': ['::1', '::2']},
-                                {'subname': 'replace-test-2', 'ttl': 50, 'type': 'AAAA', 'records': ['::1', '::2']},
+                                {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'c.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'delete-test', 'ttl': 3650, 'type': 'A', 'records': ['127.1.2.3']},
+                                {'subname': 'replace-test-1', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
+                                {'subname': 'replace-test-2', 'ttl': 3650, 'type': 'AAAA', 'records': ['::1', '::2']},
                             ]
                             ]
                         );
                         );
                         return expect(response).to.have.status(201);
                         return expect(response).to.have.status(201);
@@ -542,8 +542,8 @@ describe("API v1", function () {
                             var response = chakram.put(
                             var response = chakram.put(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'replace-test-1', 'ttl': 50, 'type': 'AAAA', 'records': []},
-                                    {'subname': 'replace-test-1', 'ttl': 1, 'type': 'CNAME', 'records': ['example.com.']},
+                                    {'subname': 'replace-test-1', 'ttl': 3650, 'type': 'AAAA', 'records': []},
+                                    {'subname': 'replace-test-1', 'ttl': 3601, 'type': 'CNAME', 'records': ['example.com.']},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(200);
                             return expect(response).to.have.status(200);
@@ -563,8 +563,8 @@ describe("API v1", function () {
                             var response = chakram.put(
                             var response = chakram.put(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'replace-test-2', 'ttl': 50, 'type': 'AAAA', 'records': []},
-                                    {'subname': 'replace-test-2', 'ttl': 1, 'type': 'CNAME', 'records': ['no.trailing.dot']},
+                                    {'subname': 'replace-test-2', 'ttl': 3650, 'type': 'AAAA', 'records': []},
+                                    {'subname': 'replace-test-2', 'ttl': 3601, 'type': 'CNAME', 'records': ['no.trailing.dot']},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(422);
                             return expect(response).to.have.status(422);
@@ -586,8 +586,8 @@ describe("API v1", function () {
                             response = chakram.post(
                             response = chakram.post(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'a.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
-                                    {'subname': 'a.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 ]
                                 ]
                             );
                             );
                             expect(response).to.have.status(400);
                             expect(response).to.have.status(400);
@@ -607,12 +607,12 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 });
                                 });
                         });
                         });
 
 
-                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"foo"'], 50);
+                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"foo"'], 3650);
                     });
                     });
 
 
                     describe("cannot delete RRsets via bulk-post", function () {
                     describe("cannot delete RRsets via bulk-post", function () {
@@ -622,7 +622,7 @@ describe("API v1", function () {
                             response = chakram.post(
                             response = chakram.post(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'c.2', 'ttl': 40, 'type': 'TXT', 'records': []},
+                                    {'subname': 'c.2', 'ttl': 3640, 'type': 'TXT', 'records': []},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(400);
                             return expect(response).to.have.status(400);
@@ -640,7 +640,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid type", function () {
                     it("gives the right response for invalid type", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'INVALID', 'records': ['"foo"']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -648,7 +648,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid records", function () {
                     it("gives the right response for invalid records", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -656,7 +656,7 @@ describe("API v1", function () {
                     it("gives the right response for records contents being null", function () {
                     it("gives the right response for records contents being null", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4', null]}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
                         );
                         );
                         return expect(response).to.have.status(400);
                         return expect(response).to.have.status(400);
                     });
                     });
@@ -675,11 +675,11 @@ describe("API v1", function () {
                     before(function () {
                     before(function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 62 }
+                            { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 3662 }
                         ).then(function () {
                         ).then(function () {
                             return chakram.put(
                             return chakram.put(
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
-                                { 'subname': 'single', 'type': 'AAAA', 'records': ['fefe::bade'], 'ttl': 31 }
+                                { 'subname': 'single', 'type': 'AAAA', 'records': ['fefe::bade'], 'ttl': 3631 }
                             );
                             );
                         });
                         });
                         expect(response).to.have.status(200);
                         expect(response).to.have.status(200);
@@ -688,10 +688,10 @@ describe("API v1", function () {
                     });
                     });
 
 
                     itPropagatesToTheApi([
                     itPropagatesToTheApi([
-                        {subname: 'single', domain: domain, type: 'AAAA', ttl: 31, records: ['fefe::bade']},
+                        {subname: 'single', domain: domain, type: 'AAAA', ttl: 3631, records: ['fefe::bade']},
                     ]);
                     ]);
 
 
-                    itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 31);
+                    itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 3631);
                 });
                 });
 
 
                 describe("can bulk-put an AAAA and an MX record", function () {
                 describe("can bulk-put an AAAA and an MX record", function () {
@@ -699,8 +699,8 @@ describe("API v1", function () {
                         var response = chakram.put(
                         var response = chakram.put(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 22 },
-                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 33 }
+                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
+                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(200);
                         expect(response).to.have.status(200);
@@ -709,13 +709,13 @@ describe("API v1", function () {
                     });
                     });
 
 
                     itPropagatesToTheApi([
                     itPropagatesToTheApi([
-                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 22, records: ['dead::beef']},
-                        {subname: '', domain: domain, type: 'MX', ttl: 33, records: ['10 mail.example.com.', '20 mail.example.net.']},
+                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
+                        {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
                     ]);
                     ]);
 
 
-                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 22);
+                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
 
 
-                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 33);
+                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
                 });
                 });
 
 
                 describe("cannot bulk-put with missing or invalid fields", function () {
                 describe("cannot bulk-put with missing or invalid fields", function () {
@@ -723,24 +723,24 @@ describe("API v1", function () {
                         // Set an RRset that we'll try to overwrite
                         // Set an RRset that we'll try to overwrite
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            {'ttl': 50, 'type': 'TXT', 'records': ['"foo"']}
+                            {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
                         );
                         );
                         expect(response).to.have.status(201);
                         expect(response).to.have.status(201);
 
 
                         var response = chakram.put(
                         var response = chakram.put(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                                {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(400);
                         expect(response).to.have.status(400);
                         expect(response).to.have.json([
                         expect(response).to.have.json([
                             { type: [ 'This field is required.' ] },
                             { type: [ 'This field is required.' ] },
-                            { ttl: [ 'Ensure this value is greater than or equal to 1.' ] },
+                            { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
                             { subname: [ 'This field is required.' ] },
                             { subname: [ 'This field is required.' ] },
                             { ttl: [ 'This field is required.' ] },
                             { ttl: [ 'This field is required.' ] },
                             { records: [ 'This field is required.' ] },
                             { records: [ 'This field is required.' ] },
@@ -760,7 +760,7 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 }),
                                 }),
                             ]);
                             ]);
@@ -774,9 +774,9 @@ describe("API v1", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'b.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'c.2', 'ttl': 50, 'type': 'A', 'records': ['1.2.3.4']},
+                                {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'b.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'c.2', 'ttl': 3650, 'type': 'A', 'records': ['1.2.3.4']},
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(201);
                         expect(response).to.have.status(201);
@@ -790,7 +790,7 @@ describe("API v1", function () {
                             response = chakram.put(
                             response = chakram.put(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'a.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 ]
                                 ]
                             );
                             );
                             expect(response).to.have.status(200);
                             expect(response).to.have.status(200);
@@ -803,12 +803,12 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 40);
+                                    expect(response).to.have.json('ttl', 3640);
                                     expect(response.body.records).to.have.members(['"bar"']);
                                     expect(response.body.records).to.have.members(['"bar"']);
                                 });
                                 });
                         });
                         });
 
 
-                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 40);
+                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 3640);
                     });
                     });
 
 
                     describe("cannot bulk-put duplicate RRsets", function () {
                     describe("cannot bulk-put duplicate RRsets", function () {
@@ -818,8 +818,8 @@ describe("API v1", function () {
                             response = chakram.put(
                             response = chakram.put(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'b.2', 'ttl': 60, 'type': 'TXT', 'records': ['"bar"']},
-                                    {'subname': 'b.2', 'ttl': 60, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'b.2', 'ttl': 3660, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'b.2', 'ttl': 3660, 'type': 'TXT', 'records': ['"bar"']},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(400);
                             return expect(response).to.have.status(400);
@@ -837,12 +837,12 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 });
                                 });
                         });
                         });
 
 
-                        itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 50);
+                        itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 3650);
                     });
                     });
 
 
                     describe("can delete RRsets via bulk-put", function () {
                     describe("can delete RRsets via bulk-put", function () {
@@ -852,7 +852,7 @@ describe("API v1", function () {
                             response = chakram.put(
                             response = chakram.put(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'c.2', 'ttl': 40, 'type': 'A', 'records': []},
+                                    {'subname': 'c.2', 'ttl': 3640, 'type': 'A', 'records': []},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(200);
                             return expect(response).to.have.status(200);
@@ -869,7 +869,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid type", function () {
                     it("gives the right response for invalid type", function () {
                         var response = chakram.put(
                         var response = chakram.put(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'INVALID', 'records': ['"foo"']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -877,7 +877,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid records", function () {
                     it("gives the right response for invalid records", function () {
                         var response = chakram.put(
                         var response = chakram.put(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -885,7 +885,7 @@ describe("API v1", function () {
                     it("gives the right response for records contents being null", function () {
                     it("gives the right response for records contents being null", function () {
                         var response = chakram.put(
                         var response = chakram.put(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4', null]}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
                         );
                         );
                         return expect(response).to.have.status(400);
                         return expect(response).to.have.status(400);
                     });
                     });
@@ -904,11 +904,11 @@ describe("API v1", function () {
                     before(function () {
                     before(function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 62 }
+                            { 'subname': 'single', 'type': 'AAAA', 'records': ['bade::fefe'], 'ttl': 3662 }
                         ).then(function () {
                         ).then(function () {
                             return chakram.patch(
                             return chakram.patch(
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
-                                { 'records': ['fefe::bade'], 'ttl': 31 }
+                                { 'records': ['fefe::bade'], 'ttl': 3631 }
                             );
                             );
                         });
                         });
                         expect(response).to.have.status(200);
                         expect(response).to.have.status(200);
@@ -917,10 +917,10 @@ describe("API v1", function () {
                     });
                     });
 
 
                     itPropagatesToTheApi([
                     itPropagatesToTheApi([
-                        {subname: 'single', domain: domain, type: 'AAAA', ttl: 31, records: ['fefe::bade']},
+                        {subname: 'single', domain: domain, type: 'AAAA', ttl: 3631, records: ['fefe::bade']},
                     ]);
                     ]);
 
 
-                    itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 31);
+                    itShowsUpInPdnsAs('single', domain, 'AAAA', ['fefe::bade'], 3631);
                 });
                 });
 
 
                 describe("can bulk-patch an AAAA and an MX record", function () {
                 describe("can bulk-patch an AAAA and an MX record", function () {
@@ -928,8 +928,8 @@ describe("API v1", function () {
                         var response = chakram.patch(
                         var response = chakram.patch(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 22 },
-                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 33 }
+                                { 'subname': 'ipv6', 'type': 'AAAA', 'records': ['dead::beef'], 'ttl': 3622 },
+                                { 'subname': '', 'type': 'MX', 'records': ['10 mail.example.com.', '20 mail.example.net.'], 'ttl': 3633 }
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(200);
                         expect(response).to.have.status(200);
@@ -938,13 +938,13 @@ describe("API v1", function () {
                     });
                     });
 
 
                     itPropagatesToTheApi([
                     itPropagatesToTheApi([
-                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 22, records: ['dead::beef']},
-                        {subname: '', domain: domain, type: 'MX', ttl: 33, records: ['10 mail.example.com.', '20 mail.example.net.']},
+                        {subname: 'ipv6', domain: domain, type: 'AAAA', ttl: 3622, records: ['dead::beef']},
+                        {subname: '', domain: domain, type: 'MX', ttl: 3633, records: ['10 mail.example.com.', '20 mail.example.net.']},
                     ]);
                     ]);
 
 
-                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 22);
+                    itShowsUpInPdnsAs('ipv6', domain, 'AAAA', ['dead::beef'], 3622);
 
 
-                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 33);
+                    itShowsUpInPdnsAs('', domain, 'MX', ['10 mail.example.com.', '20 mail.example.net.'], 3633);
                 });
                 });
 
 
                 describe("cannot bulk-patch with missing or invalid fields", function () {
                 describe("cannot bulk-patch with missing or invalid fields", function () {
@@ -952,24 +952,24 @@ describe("API v1", function () {
                         // Set an RRset that we'll try to overwrite
                         // Set an RRset that we'll try to overwrite
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            {'ttl': 50, 'type': 'TXT', 'records': ['"foo"']}
+                            {'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']}
                         );
                         );
                         expect(response).to.have.status(201);
                         expect(response).to.have.status(201);
 
 
                         var response = chakram.patch(
                         var response = chakram.patch(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 22},
+                                {'subname': 'a.1', 'records': ['dead::beef'], 'ttl': 3622},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
                                 {'subname': 'b.1', 'ttl': -50, 'type': 'AAAA', 'records': ['dead::beef']},
-                                {'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                {'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
                                 {'subname': 'c.1', 'records': ['dead::beef'], 'type': 'AAAA'},
-                                {'subname': 'd.1', 'ttl': 50, 'type': 'AAAA'},
+                                {'subname': 'd.1', 'ttl': 3650, 'type': 'AAAA'},
                             ]
                             ]
                         );
                         );
                         expect(response).to.have.status(400);
                         expect(response).to.have.status(400);
                         expect(response).to.have.json([
                         expect(response).to.have.json([
                             { type: [ 'This field is required.' ] },
                             { type: [ 'This field is required.' ] },
-                            { ttl: [ 'Ensure this value is greater than or equal to 1.' ] },
+                            { ttl: [ 'Ensure this value is greater than or equal to 60.' ] },
                             { subname: [ 'This field is required.' ] },
                             { subname: [ 'This field is required.' ] },
                             { ttl: ['This field is required.']} ,
                             { ttl: ['This field is required.']} ,
                             { records: ['This field is required.']} ,
                             { records: ['This field is required.']} ,
@@ -989,7 +989,7 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 }),
                                 }),
                             ]);
                             ]);
@@ -1003,12 +1003,12 @@ describe("API v1", function () {
                         var response = chakram.post(
                         var response = chakram.post(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
                             [
                             [
-                                {'subname': 'a.1', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'a.2', 'ttl': 50, 'type': 'A', 'records': ['4.3.2.1']},
-                                {'subname': 'a.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'b.2', 'ttl': 50, 'type': 'A', 'records': ['5.4.3.2']},
-                                {'subname': 'b.2', 'ttl': 50, 'type': 'TXT', 'records': ['"foo"']},
-                                {'subname': 'c.2', 'ttl': 50, 'type': 'A', 'records': ['1.2.3.4']},
+                                {'subname': 'a.1', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'a.2', 'ttl': 3650, 'type': 'A', 'records': ['4.3.2.1']},
+                                {'subname': 'a.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'b.2', 'ttl': 3650, 'type': 'A', 'records': ['5.4.3.2']},
+                                {'subname': 'b.2', 'ttl': 3650, 'type': 'TXT', 'records': ['"foo"']},
+                                {'subname': 'c.2', 'ttl': 3650, 'type': 'A', 'records': ['1.2.3.4']},
                             ]
                             ]
                         );
                         );
                         return expect(response).to.have.status(201);
                         return expect(response).to.have.status(201);
@@ -1022,7 +1022,7 @@ describe("API v1", function () {
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
                                     {'subname': 'a.1', 'type': 'TXT', 'records': ['"bar"']},
                                     {'subname': 'a.1', 'type': 'TXT', 'records': ['"bar"']},
-                                    {'subname': 'a.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'a.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 ]
                                 ]
                             );
                             );
                             expect(response).to.have.status(200);
                             expect(response).to.have.status(200);
@@ -1036,20 +1036,20 @@ describe("API v1", function () {
                                     .get('/domains/' + domain + '/rrsets/a.1.../TXT/')
                                     .get('/domains/' + domain + '/rrsets/a.1.../TXT/')
                                     .then(function (response) {
                                     .then(function (response) {
                                         expect(response).to.have.status(200);
                                         expect(response).to.have.status(200);
-                                        expect(response).to.have.json('ttl', 50);
+                                        expect(response).to.have.json('ttl', 3650);
                                         expect(response.body.records).to.have.members(['"bar"']);
                                         expect(response.body.records).to.have.members(['"bar"']);
                                     }),
                                     }),
                                 chakram
                                 chakram
                                     .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                     .get('/domains/' + domain + '/rrsets/a.2.../TXT/')
                                     .then(function (response) {
                                     .then(function (response) {
                                         expect(response).to.have.status(200);
                                         expect(response).to.have.status(200);
-                                        expect(response).to.have.json('ttl', 40);
+                                        expect(response).to.have.json('ttl', 3640);
                                         expect(response.body.records).to.have.members(['"bar"']);
                                         expect(response.body.records).to.have.members(['"bar"']);
                                     }),
                                     }),
                             ]);
                             ]);
                         });
                         });
 
 
-                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 40);
+                        itShowsUpInPdnsAs('a.2', domain, 'TXT', ['"bar"'], 3640);
                     });
                     });
 
 
                     describe("cannot bulk-patch duplicate RRsets", function () {
                     describe("cannot bulk-patch duplicate RRsets", function () {
@@ -1059,8 +1059,8 @@ describe("API v1", function () {
                             response = chakram.patch(
                             response = chakram.patch(
                                 '/domains/' + domain + '/rrsets/',
                                 '/domains/' + domain + '/rrsets/',
                                 [
                                 [
-                                    {'subname': 'b.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
-                                    {'subname': 'b.2', 'ttl': 40, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'b.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
+                                    {'subname': 'b.2', 'ttl': 3640, 'type': 'TXT', 'records': ['"bar"']},
                                 ]
                                 ]
                             );
                             );
                             return expect(response).to.have.status(400);
                             return expect(response).to.have.status(400);
@@ -1078,12 +1078,12 @@ describe("API v1", function () {
                                 .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
                                 .get('/domains/' + domain + '/rrsets/b.2.../TXT/')
                                 .then(function (response) {
                                 .then(function (response) {
                                     expect(response).to.have.status(200);
                                     expect(response).to.have.status(200);
-                                    expect(response).to.have.json('ttl', 50);
+                                    expect(response).to.have.json('ttl', 3650);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                     expect(response.body.records).to.have.members(['"foo"']);
                                 });
                                 });
                         });
                         });
 
 
-                        itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 50);
+                        itShowsUpInPdnsAs('b.2', domain, 'TXT', ['"foo"'], 3650);
                     });
                     });
 
 
                     describe("can delete RRsets via bulk-patch", function () {
                     describe("can delete RRsets via bulk-patch", function () {
@@ -1110,7 +1110,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid type", function () {
                     it("gives the right response for invalid type", function () {
                         var response = chakram.patch(
                         var response = chakram.patch(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'INVALID', 'records': ['"foo"']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'INVALID', 'records': ['"foo"']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -1118,7 +1118,7 @@ describe("API v1", function () {
                     it("gives the right response for invalid records", function () {
                     it("gives the right response for invalid records", function () {
                         var response = chakram.patch(
                         var response = chakram.patch(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4']}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4']}]
                         );
                         );
                         return expect(response).to.have.status(422);
                         return expect(response).to.have.status(422);
                     });
                     });
@@ -1126,7 +1126,7 @@ describe("API v1", function () {
                     it("gives the right response for records contents being null", function () {
                     it("gives the right response for records contents being null", function () {
                         var response = chakram.patch(
                         var response = chakram.patch(
                             '/domains/' + domain + '/rrsets/',
                             '/domains/' + domain + '/rrsets/',
-                            [{'subname': 'a.2', 'ttl': 50, 'type': 'MX', 'records': ['1.2.3.4', null]}]
+                            [{'subname': 'a.2', 'ttl': 3650, 'type': 'MX', 'records': ['1.2.3.4', null]}]
                         );
                         );
                         return expect(response).to.have.status(400);
                         return expect(response).to.have.status(400);
                     });
                     });