diff --git a/src/_zkapauthorizer/config.py b/src/_zkapauthorizer/config.py
index 064ff52220e67080b6d3dd587b0fd94190b5ae09..b30b43fbf7e3b49091ba8bc8fd4175000ee755fa 100644
--- a/src/_zkapauthorizer/config.py
+++ b/src/_zkapauthorizer/config.py
@@ -82,6 +82,40 @@ def lease_maintenance_from_tahoe_config(node_config):
     )
 
 
+def get_configured_lease_duration(node_config):
+    """
+    Return the minimum amount of time for which a newly granted lease will
+    ensure data is stored.
+
+    The actual lease duration is hard-coded in Tahoe-LAFS in many places.
+    However, we have local configuration that tells us when to renew a lease.
+    Since lease renewal discards any remaining time on a current lease and
+    puts a new lease period in its place, starting from the time of the
+    operation, the amount of time we effectively get from a lease is based on
+    Tahoe-LAFS' hard-coded lease duration and our own lease renewal
+    configuration.
+
+    Since this function only promises to return the *minimum* time a client
+    can expect a lease to last, we respond with a lease time shortened by our
+    configuration.
+
+    An excellent goal to pursue in the future would be to change the lease
+    renewal behavior in Tahoe-LAFS so that we can control the length of leases
+    and/or add to an existing lease instead of replacing it.  The former
+    option would let us really configure lease durations.  The latter would
+    let us stop worrying so much about what is lost by renewing a lease before
+    the last second of its validity period.
+
+    :return int: The minimum number of seconds for which a newly acquired
+        lease will be valid.
+    """
+    # See lots of places in Tahoe-LAFS, eg src/allmydata/storage/server.py
+    upper_bound = 31 * 24 * 60 * 60
+    lease_maint_config = lease_maintenance_from_tahoe_config(node_config)
+    min_time_remaining = lease_maint_config.min_lease_remaining.total_seconds()
+    return int(upper_bound - min_time_remaining)
+
+
 def _read_duration(cfg, option, default):
     """
     Read an integer number of seconds from the ZKAPAuthorizer section of a
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 7b81c26579e36108a2a369271841a6f355f528b2..4f955e2c46304260d40be28a091ced5e43223511 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -33,12 +33,12 @@ from zope.interface import Attribute
 
 from . import __version__ as _zkapauthorizer_version
 from ._base64 import urlsafe_b64decode
+from .config import get_configured_lease_duration
 from .controller import PaymentController, get_redeemer
 from .pricecalculator import PriceCalculator
 from .private import create_private_tree
 from .storage_common import (
     get_configured_allowed_public_keys,
-    get_configured_lease_duration,
     get_configured_pass_value,
     get_configured_shares_needed,
     get_configured_shares_total,
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index 605abc1a59a09bda04ea09f6cadb7d0c43a78805..bbe326a292b502024eef0b77b4181977c4f2509d 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -121,17 +121,6 @@ def get_configured_pass_value(node_config):
     )
 
 
-def get_configured_lease_duration(node_config):
-    """
-    Just kidding.  Lease duration is hard-coded.
-
-    :return int: The number of seconds after which a newly acquired lease will
-        be valid.
-    """
-    # See lots of places in Tahoe-LAFS, eg src/allmydata/storage/server.py
-    return 31 * 24 * 60 * 60
-
-
 def get_configured_allowed_public_keys(node_config):
     """
     Read the set of allowed issuer public keys from the given configuration.
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 6cf2806d56c0f48ac6af30517bf33869284e33c9..dc8763acd552c258d9bab1db243a506b8c62f8b2 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -61,6 +61,58 @@ from ..model import (
     Voucher,
 )
 
+_POSIX_EPOCH = datetime.utcfromtimestamp(0)
+
+
+def posix_safe_datetimes():
+    """
+    Build datetime instances in a range that can be represented as floats
+    without losing microsecond precision.
+    """
+    return datetimes(
+        # I don't know that time-based parts of the system break down
+        # before the POSIX epoch but I don't know that they work, either.
+        # Don't time travel with this code.
+        min_value=_POSIX_EPOCH,
+        # Once we get far enough into the future we lose the ability to
+        # represent a timestamp with microsecond precision in a floating point
+        # number, which we do with any POSIX timestamp-like API (eg
+        # twisted.internet.task.Clock).  So don't go far enough into the
+        # future.  Furthermore, once we don't fit into an unsigned 4 byte
+        # integers, we can't round-trip through all the things that expect a
+        # time_t.  Stay back from the absolute top to give tests a little
+        # space to advance time, too.
+        max_value=datetime.utcfromtimestamp(2 ** 31),
+    )
+
+
+def posix_timestamps():
+    """
+    Build floats in a range that can represent time without losing microsecond
+    precision.
+    """
+    return posix_safe_datetimes().map(
+        lambda when: (when - _POSIX_EPOCH).total_seconds(),
+    )
+
+
+def clocks(now=posix_timestamps()):
+    """
+    Build ``twisted.internet.task.Clock`` instances set to a time built by
+    ``now``.
+
+    :param now: A strategy that builds POSIX timestamps (ie, ints or floats in
+        the range of time_t).
+    """
+
+    def clock_at_time(when):
+        c = Clock()
+        c.advance(when)
+        return c
+
+    return now.map(clock_at_time)
+
+
 # Sizes informed by
 # https://github.com/brave-intl/challenge-bypass-ristretto/blob/2f98b057d7f353c12b2b12d0f5ae9ad115f1d0ba/src/oprf.rs#L18-L33
 
@@ -237,7 +289,10 @@ def zkapauthz_configuration(
         ``extra_configurations``.
     """
 
-    def merge(extra_configuration, allowed_public_keys):
+    def merge(
+        extra_configuration,
+        allowed_public_keys,
+    ):
         config = {
             u"default-token-count": u"32",
             u"allowed-public-keys": u",".join(allowed_public_keys),
@@ -266,21 +321,46 @@ def client_ristrettoredeemer_configurations():
     )
 
 
-def client_dummyredeemer_configurations():
+def client_dummyredeemer_configurations(
+    crawl_means=one_of(none(), posix_timestamps()),
+    crawl_ranges=one_of(none(), posix_timestamps()),
+    min_times_remaining=one_of(none(), posix_timestamps()),
+):
     """
     Build DummyRedeemer-using configuration values for the client-side plugin.
     """
 
+    def make_lease_config(crawl_mean, crawl_range, min_time_remaining):
+        config = {}
+        if crawl_mean is not None:
+            # Don't allow the mean to be 0
+            config["lease.crawl-interval.mean"] = str(int(crawl_mean) + 1)
+        if crawl_range is not None:
+            config["lease.crawl-interval.range"] = str(int(crawl_range))
+        if min_time_remaining is not None:
+            config["lease.min-time-remaining"] = str(int(min_time_remaining))
+        return config
+
     def share_a_key(allowed_keys):
-        return zkapauthz_configuration(
-            just(
+        lease_configs = builds(
+            make_lease_config,
+            crawl_means,
+            crawl_ranges,
+            min_times_remaining,
+        )
+        extra_config = lease_configs.map(
+            lambda config: config.update(
                 {
                     u"redeemer": u"dummy",
                     # Pick out one of the allowed public keys so that the dummy
                     # appears to produce usable tokens.
                     u"issuer-public-key": next(iter(allowed_keys)),
                 }
-            ),
+            )
+            or config,
+        )
+        return zkapauthz_configuration(
+            extra_config,
             allowed_public_keys=just(allowed_keys),
         )
 
@@ -895,58 +975,6 @@ def announcements():
     )
 
 
-_POSIX_EPOCH = datetime.utcfromtimestamp(0)
-
-
-def posix_safe_datetimes():
-    """
-    Build datetime instances in a range that can be represented as floats
-    without losing microsecond precision.
-    """
-    return datetimes(
-        # I don't know that time-based parts of the system break down
-        # before the POSIX epoch but I don't know that they work, either.
-        # Don't time travel with this code.
-        min_value=_POSIX_EPOCH,
-        # Once we get far enough into the future we lose the ability to
-        # represent a timestamp with microsecond precision in a floating point
-        # number, which we do with any POSIX timestamp-like API (eg
-        # twisted.internet.task.Clock).  So don't go far enough into the
-        # future.  Furthermore, once we don't fit into an unsigned 4 byte
-        # integers, we can't round-trip through all the things that expect a
-        # time_t.  Stay back from the absolute top to give tests a little
-        # space to advance time, too.
-        max_value=datetime.utcfromtimestamp(2 ** 31),
-    )
-
-
-def posix_timestamps():
-    """
-    Build floats in a range that can represent time without losing microsecond
-    precision.
-    """
-    return posix_safe_datetimes().map(
-        lambda when: (when - _POSIX_EPOCH).total_seconds(),
-    )
-
-
-def clocks(now=posix_timestamps()):
-    """
-    Build ``twisted.internet.task.Clock`` instances set to a time built by
-    ``now``.
-
-    :param now: A strategy that builds POSIX timestamps (ie, ints or floats in
-        the range of time_t).
-    """
-
-    def clock_at_time(when):
-        c = Clock()
-        c.advance(when)
-        return c
-
-    return now.map(clock_at_time)
-
-
 @implementer(IFilesystemNode)
 @attr.s(frozen=True)
 class _LeafNode(object):
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index 954ad66d0dcf99d73bff11e75d443b2d1345dfad..b4e928c6d99231f90a79161561b97f7258a3ffd3 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -88,7 +88,6 @@ from ..pricecalculator import PriceCalculator
 from ..resource import NUM_TOKENS, from_configuration, get_token_count
 from ..storage_common import (
     get_configured_allowed_public_keys,
-    get_configured_lease_duration,
     get_configured_pass_value,
     required_passes,
 )
@@ -102,6 +101,7 @@ from .strategies import (
     client_nonredeemer_configurations,
     client_unpaidredeemer_configurations,
     direct_tahoe_configs,
+    posix_timestamps,
     request_paths,
     requests,
     share_parameters,
@@ -746,12 +746,15 @@ class UnblindedTokenTests(TestCase):
         using_a_token = after(getting_initial_tokens, use_a_token)
         getting_tokens_after = after(using_a_token, get_tokens)
 
+        def check_tokens(before_and_after):
+            initial_tokens, tokens_after = before_and_after
+            return initial_tokens[1:] == tokens_after
+
         self.assertThat(
             gatherResults([getting_initial_tokens, getting_tokens_after]),
             succeeded(
                 MatchesPredicate(
-                    lambda (initial_tokens, tokens_after): initial_tokens[1:]
-                    == tokens_after,
+                    check_tokens,
                     u"initial, after (%s): initial[1:] != after",
                 ),
             ),
@@ -1586,30 +1589,36 @@ class CalculatePriceTests(TestCase):
         )
 
     @given(
-        # Make the share encoding parameters easily accessible without going
-        # through the Tahoe-LAFS configuration.
-        share_parameters().flatmap(
-            lambda params: tuples(
-                just(params),
-                tahoe_configs(shares=just(params)),
+        tuples(
+            # Make the share encoding parameters easily accessible without
+            # going through the Tahoe-LAFS configuration.
+            share_parameters(),
+            # Same goes for the minimum lease time remaining configuration.
+            posix_timestamps().map(int),
+        ).flatmap(
+            lambda share_and_lease_time: tuples(
+                just(share_and_lease_time),
+                direct_tahoe_configs(
+                    zkapauthz_v1_configuration=client_dummyredeemer_configurations(
+                        min_times_remaining=just(share_and_lease_time[1]),
+                    ),
+                    shares=just(share_and_lease_time[0]),
+                ),
             ),
         ),
         api_auth_tokens(),
         lists(integers(min_value=0)),
     )
-    def test_calculated_price(
-        self, encoding_params_and_get_config, api_auth_token, sizes
-    ):
+    def test_calculated_price(self, encoding_params_and_config, api_auth_token, sizes):
         """
         A well-formed request returns the price in ZKAPs as an integer and the
         storage period (the minimum allowed) that they pay for.
         """
-        encoding_params, get_config = encoding_params_and_get_config
+        (encoding_params, min_time_remaining), config = encoding_params_and_config
         shares_needed, shares_happy, shares_total = encoding_params
-
-        config = get_config_with_api_token(
-            self.useFixture(TempDir()),
-            get_config,
+        add_api_token_to_config(
+            self.useFixture(TempDir()).join(b"tahoe"),
+            config,
             api_auth_token,
         )
         root = root_from_config(config, datetime.now)
@@ -1639,7 +1648,7 @@ class CalculatePriceTests(TestCase):
                         Equals(
                             {
                                 u"price": expected_price,
-                                u"period": get_configured_lease_duration(config),
+                                u"period": 60 * 60 * 24 * 31 - min_time_remaining,
                             }
                         ),
                     ),