diff --git a/setup.cfg b/setup.cfg
index ebbd1ac295db4e0950f28815949e5900618e59b1..272d0352b9788bdb6a26a85634423b34be3a9aaa 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -34,7 +34,7 @@ install_requires =
     attrs
     zope.interface
     eliot
-    aniso8601
+    isodate
     python-challenge-bypass-ristretto
     # The pip resolver sometimes finds treq's dependencies first and these are
     # incompatible with Tahoe-LAFS'.  So duplicate them here (the ones that
diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py
index 9fe3186224f334446e83e5ac829e47942d78a2e3..d4f010d1eca6f6feda80e13a45d52fda34b7dfb9 100644
--- a/src/_zkapauthorizer/lease_maintenance.py
+++ b/src/_zkapauthorizer/lease_maintenance.py
@@ -29,7 +29,7 @@ from allmydata.util.hashutil import (
     file_cancel_secret_hash,
     file_renewal_secret_hash,
 )
-from aniso8601 import parse_datetime
+from isodate import duration_isoformat, parse_datetime, parse_duration
 from twisted.application.service import Service
 from twisted.internet.defer import inlineCallbacks, maybeDeferred
 from twisted.python.log import err
@@ -309,12 +309,17 @@ class _FuzzyTimerService(Service):
 
     :ivar IReactorTime reactor: A Twisted reactor to use to schedule runs of
         the operation.
+
+    :ivar get_config: A function to call to return the service's
+        configuration.  The configuration is represented as a service-specific
+        object.
     """
 
     name = attr.ib()
     operation = attr.ib()
     initial_interval = attr.ib()
     sample_interval_distribution = attr.ib()
+    get_config = attr.ib()  # type: () -> Any
     reactor = attr.ib()
 
     def startService(self):
@@ -414,6 +419,12 @@ def lease_maintenance_service(
             timedelta(0),
         )
 
+    def get_config():
+        return LeaseMaintenanceConfig(
+            crawl_interval_mean=interval_mean,
+            crawl_interval_range=interval_range,
+        )
+
     return _FuzzyTimerService(
         SERVICE_NAME,
         lambda: bracket(
@@ -426,10 +437,41 @@ def lease_maintenance_service(
         ),
         initial_interval,
         sample_interval_distribution,
+        get_config,
         reactor,
     )
 
 
+@attr.s(frozen=True)
+class LeaseMaintenanceConfig(object):
+    """
+    Represent the configuration for a lease maintenance service.
+    """
+
+    crawl_interval_mean = attr.ib()  # type: datetime.timedelta
+    crawl_interval_range = attr.ib()  # type: datetime.timedelta
+
+
+def lease_maintenance_config_to_dict(lease_maint_config):
+    # type: (LeaseMaintenanceConfig) -> Dict[str, str]
+    return {
+        "lease.crawl-interval.mean": duration_isoformat(
+            lease_maint_config.crawl_interval_mean,
+        ),
+        "lease.crawl-interval.range": duration_isoformat(
+            lease_maint_config.crawl_interval_range,
+        ),
+    }
+
+
+def lease_maintenance_config_from_dict(d):
+    # type: (Dict[str, str]) -> LeaseMaintenanceConfig
+    return LeaseMaintenanceConfig(
+        crawl_interval_mean=parse_duration(d["lease.crawl-interval.mean"]),
+        crawl_interval_range=parse_duration(d["lease.crawl-interval.range"]),
+    )
+
+
 def write_time_to_path(path, when):
     """
     Write an ISO8601 datetime string to a file.
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 27e2fe947ebbf01d329022d72353c6b072aedca8..1b398950188e11120bd76d6a1701c63ca1970be2 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -24,7 +24,7 @@ from sqlite3 import OperationalError
 from sqlite3 import connect as _connect
 
 import attr
-from aniso8601 import parse_datetime as _parse_datetime
+from isodate import parse_datetime
 from twisted.logger import Logger
 from twisted.python.filepath import FilePath
 from zope.interface import Interface, implementer
@@ -39,18 +39,6 @@ from .storage_common import (
 from .validators import greater_than, has_length, is_base64_encoded
 
 
-def parse_datetime(s, **kw):
-    """
-    Like ``aniso8601.parse_datetime`` but accept unicode as well.
-    """
-    if isinstance(s, unicode):
-        s = s.encode("utf-8")
-    assert isinstance(s, bytes)
-    if "delimiter" in kw and isinstance(kw["delimiter"], unicode):
-        kw["delimiter"] = kw["delimiter"].encode("utf-8")
-    return _parse_datetime(s, **kw)
-
-
 class ILeaseMaintenanceObserver(Interface):
     """
     An object which is interested in receiving events related to the progress
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 567c94405e6aac2009255ead776784e22ab95bad..ec952ec46ed346a66acd2401421cf459ed10d3ba 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -17,7 +17,7 @@ Hypothesis strategies for property testing.
 """
 
 from base64 import b64encode, urlsafe_b64encode
-from datetime import datetime
+from datetime import datetime, timedelta
 from urllib import quote
 
 import attr
@@ -30,6 +30,7 @@ from hypothesis.strategies import (
     datetimes,
     dictionaries,
     fixed_dictionaries,
+    floats,
     integers,
     just,
     lists,
@@ -39,6 +40,7 @@ from hypothesis.strategies import (
     sampled_from,
     sets,
     text,
+    timedeltas,
     tuples,
 )
 from twisted.internet.defer import succeed
@@ -47,6 +49,7 @@ from twisted.web.test.requesthelper import DummyRequest
 from zope.interface import implementer
 
 from ..configutil import config_string_from_sections
+from ..lease_maintenance import LeaseMaintenanceConfig, lease_maintenance_config_to_dict
 from ..model import (
     DoubleSpend,
     Error,
@@ -347,6 +350,50 @@ def client_errorredeemer_configurations(details):
     )
 
 
+def interval_means():
+    """
+    Build timedeltas representing the mean time between lease maintenance
+    runs.
+    """
+    return floats(
+        # It doesn't make sense to have a negative check interval mean.
+        min_value=0,
+        # We can't make this value too large or it isn't convertable to a
+        # timedelta.  Also, even values as large as this one are of
+        # questionable value.
+        max_value=60 * 60 * 24 * 365,
+    ).map(
+        # By representing the result as a timedelta we avoid the cases where
+        # the lower precision of timedelta compared to float drops the whole
+        # value (anything between 0 and 1 microsecond).  This is just one
+        # example of how working with timedeltas is nicer, in general.
+        lambda s: timedelta(seconds=s),
+    )
+
+
+def lease_maintenance_configurations():
+    """
+    Build LeaseMaintenanceConfig instances.
+    """
+    return builds(
+        LeaseMaintenanceConfig,
+        interval_means(),
+        timedeltas(min_value=timedelta(0)),
+    )
+
+
+def client_lease_maintenance_configurations(maint_configs=None):
+    """
+    Build dictionaries representing the lease maintenance options that go into
+    the ZKAPAuthorizer client plugin section.
+    """
+    if maint_configs is None:
+        maint_configs = lease_maintenance_configurations()
+    return maint_configs.map(
+        lambda lease_maint_config: lease_maintenance_config_to_dict(lease_maint_config),
+    )
+
+
 def direct_tahoe_configs(
     zkapauthz_v1_configuration=client_dummyredeemer_configurations(),
     shares=just((None, None, None)),
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 954ad66d0dcf99d73bff11e75d443b2d1345dfad..4b1663d1d74c1925e657be540223a1ed2f07850b 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -26,7 +26,6 @@ from urllib import quote
 
 import attr
 from allmydata.client import config_from_string
-from aniso8601 import parse_datetime
 from fixtures import TempDir
 from hypothesis import given, note
 from hypothesis.strategies import (
@@ -44,6 +43,7 @@ from hypothesis.strategies import (
     text,
     tuples,
 )
+from isodate import parse_datetime
 from testtools import TestCase
 from testtools.content import text_content
 from testtools.matchers import (
diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py
index 87345284ccf38f17272596dd71e1e6bf916bff6e..a0a9c3a0129754b78c7ec24e96ba283335f17cdc 100644
--- a/src/_zkapauthorizer/tests/test_lease_maintenance.py
+++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py
@@ -60,6 +60,8 @@ from ..foolscap import ShareStat
 from ..lease_maintenance import (
     MemoryMaintenanceObserver,
     NoopMaintenanceObserver,
+    lease_maintenance_config_from_dict,
+    lease_maintenance_config_to_dict,
     lease_maintenance_service,
     maintain_leases_from_root,
     renew_leases,
@@ -68,6 +70,8 @@ from ..lease_maintenance import (
 from .matchers import Provides, between, leases_current
 from .strategies import (
     clocks,
+    interval_means,
+    lease_maintenance_configurations,
     node_hierarchies,
     posix_timestamps,
     sharenums,
@@ -75,23 +79,6 @@ from .strategies import (
 )
 
 
-def interval_means():
-    return floats(
-        # It doesn't make sense to have a negative check interval mean.
-        min_value=0,
-        # We can't make this value too large or it isn't convertable to a
-        # timedelta.  Also, even values as large as this one are of
-        # questionable value.
-        max_value=60 * 60 * 24 * 365,
-    ).map(
-        # By representing the result as a timedelta we avoid the cases where
-        # the lower precision of timedelta compared to float drops the whole
-        # value (anything between 0 and 1 microsecond).  This is just one
-        # example of how working with timedeltas is nicer, in general.
-        lambda s: timedelta(seconds=s),
-    )
-
-
 def dummy_maintain_leases():
     pass
 
@@ -217,6 +204,23 @@ def storage_brokers(draw, clocks):
     )
 
 
+class LeaseMaintenanceConfigTests(TestCase):
+    """
+    Tests related to ``LeaseMaintenanceConfig``.
+    """
+
+    @given(lease_maintenance_configurations())
+    def test_config_roundtrip(self, config):
+        """
+        ``LeaseMaintenanceConfig`` round-trips through
+        ``lease_maintenance_config_to_dict`` and
+        ``lease_maintenance_config_from_dict``.
+        """
+        dumped = lease_maintenance_config_to_dict(config)
+        loaded = lease_maintenance_config_from_dict(dumped)
+        self.assertThat(config, Equals(loaded))
+
+
 class LeaseMaintenanceServiceTests(TestCase):
     """
     Tests for the service returned by ``lease_maintenance_service``.
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index 78959a5d37712fd2d57f9eb5ebff7cda75f514bf..ac4d342f55c760a6ee3c0ba4e03ec12bc00d6f71 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -74,8 +74,10 @@ from .matchers import Provides, raises
 from .strategies import (
     announcements,
     client_dummyredeemer_configurations,
+    client_lease_maintenance_configurations,
     dummy_ristretto_keys,
     lease_cancel_secrets,
+    lease_maintenance_configurations,
     lease_renew_secrets,
     minimal_tahoe_configs,
     pass_counts,
@@ -537,44 +539,38 @@ class LeaseMaintenanceServiceTests(TestCase):
     Tests for the plugin's initialization of the lease maintenance service.
     """
 
-    def _created_test(self, get_config, servers_yaml, rootcap):
-        original_tempdir = tempfile.tempdir
+    def _create(self, get_config, servers_yaml, rootcap):
+        """
+        Create a client node using ``create_client_from_config``.
+
+        :param get_config: A function to call to get a Tahoe-LAFS config
+            object.
+
+        :param servers_yaml: ``None`` or a string giving the contents for the
+            node's ``servers.yaml`` file.
 
+        :param rootcap: ``True`` to write some bytes to the node's ``rootcap``
+            file, ``False`` otherwise.
+        """
         tempdir = self.useFixture(TempDir())
         nodedir = tempdir.join(b"node")
         privatedir = tempdir.join(b"node", b"private")
         makedirs(privatedir)
         config = get_config(nodedir, b"tub.port")
 
-        # Provide it a statically configured server to connect to.
-        config.write_private_config(
-            b"servers.yaml",
-            servers_yaml,
-        )
+        if servers_yaml is not None:
+            # Provide it a statically configured server to connect to.
+            config.write_private_config(
+                b"servers.yaml",
+                servers_yaml,
+            )
         if rootcap:
             config.write_private_config(
                 b"rootcap",
                 b"dddddddd",
             )
 
-        try:
-            d = create_client_from_config(config)
-            self.assertThat(
-                d,
-                succeeded(
-                    AfterPreprocessing(
-                        lambda client: client.getServiceNamed(SERVICE_NAME),
-                        Always(),
-                    ),
-                ),
-            )
-        finally:
-            # create_client_from_config (indirectly) rewrites tempfile.tempdir
-            # in a destructive manner that fails most of the rest of the test
-            # suite if we don't clean it up.  We can't do this with a tearDown
-            # or a fixture or an addCleanup because hypothesis doesn't run any
-            # of those at the right time. :/
-            tempfile.tempdir = original_tempdir
+        return create_client_from_config(config)
 
     @settings(
         deadline=None,
@@ -589,7 +585,8 @@ class LeaseMaintenanceServiceTests(TestCase):
         maintenance service after it has at least one storage server to
         connect to.
         """
-        return self._created_test(get_config, servers_yaml, rootcap=True)
+        d = self._create(get_config, servers_yaml, rootcap=True)
+        self.assertThat(d, succeeded(has_lease_maintenance_service()))
 
     @settings(
         deadline=None,
@@ -603,7 +600,72 @@ class LeaseMaintenanceServiceTests(TestCase):
         The lease maintenance service can be created even if no rootcap has yet
         been written to the client's configuration directory.
         """
-        return self._created_test(get_config, servers_yaml, rootcap=False)
+        d = self._create(get_config, servers_yaml, rootcap=False)
+        self.assertThat(d, succeeded(has_lease_maintenance_service()))
+
+    @given(
+        # First build the simple lease maintenance configuration object that
+        # represents the example to test.
+        lease_maintenance_configurations().flatmap(
+            # Then generate build a function that will get us a Tahoe
+            # configuration that includes at least that lease maintenance
+            # configuration.
+            lambda lease_maint_config: tahoe_configs(
+                zkapauthz_v1_configuration=client_lease_maintenance_configurations(
+                    just(lease_maint_config),
+                ),
+            ).map(
+                # Then bundle up both pieces to pass to the function.  By
+                # preserving the simple lease maintenance configuration and
+                # making it available to the test, the test logic is much
+                # simplified (eg, we don't have to read values out of the
+                # Tahoe configuration to figure out what example we're working
+                # on).
+                lambda get_config: (lease_maint_config, get_config),
+            ),
+        ),
+    )
+    def test_values_from_configuration(self, config_objs):
+        """
+        If values for lease maintenance parameters are supplied in the
+        configuration file then the lease maintenance service is created with
+        those values.
+        """
+        lease_maint_config, get_config = config_objs
+        d = self._create(get_config, servers_yaml=None, rootcap=False)
+        self.assertThat(
+            d,
+            succeeded(has_lease_maintenance_configuration(lease_maint_config)),
+        )
+
+
+def has_lease_maintenance_service():
+    """
+    Return a matcher for a Tahoe-LAFS client object that has a lease
+    maintenance service.
+    """
+    # type: () -> Matcher
+    return AfterPreprocessing(
+        lambda client: client.getServiceNamed(SERVICE_NAME),
+        Always(),
+    )
+
+
+def has_lease_maintenance_configuration(lease_maint_config):
+    """
+    Return a matcher for a Tahoe-LAFS client object that has a lease
+    maintenance service with the given configuration.
+    """
+    # type: (_LeaseMaintenanceConfig) -> Matcher
+    def get_lease_maintenance_config(lease_maint_service):
+        return lease_maint_service.get_config()
+
+    return AfterPreprocessing(
+        lambda client: get_lease_maintenance_config(
+            client.getServiceNamed(SERVICE_NAME),
+        ),
+        Equals(lease_maint_config),
+    )
 
 
 class LoadSigningKeyTests(TestCase):