diff --git a/morph/grid/local/README.rst b/morph/grid/local/README.rst
index 345547244635734278aa76cb5cd59946f2afd37f..f899c4975bab131e969acc79e8b39dee139b1554 100644
--- a/morph/grid/local/README.rst
+++ b/morph/grid/local/README.rst
@@ -33,13 +33,6 @@ Use the local development environment
 
     install -d ~/.ssh ; vagrant ssh-config >> ~/.ssh/config
 
-5. Edit the generated configuration: Add the ``publicIP`` addresses from ``grid.nix`` to ssh config **Host** match blocks (**not** HostName) so the ``Host`` lines all read like::
-
-    Host payments 192.168.67.21
-      HostName 127.0.0.1
-      User vagrant
-      [...]
-
   Latest Morph honors the ``SSH_CONFIG_FILE`` environment variable (`since 3f90aa88 (March 2020, v 1.5.0) <https://github.com/DBCDK/morph/commit/3f90aa885fac1c29fce9242452fa7c0c505744ef#diff-d155ad793bd62e6ea4c44ba985049ecb13a4f4f32f799791b2bce695a16c0101>`_), so in the future this should get a bit more convenient.
 
 6. Add your SSH key to ``users.nix`` so you'll be able to log in after deploying the new configuration::
@@ -56,4 +49,3 @@ Use the local development environment
     morph upload-secrets grid.nix
 
   You should now be able to log in with the users and keys you set in your ``users.nix`` file.
-
diff --git a/morph/grid/local/Vagrantfile b/morph/grid/local/Vagrantfile
index 7ad95ca872a72e5da6c11b3269e2a824cf8a55f9..fd374a2cc26da63262a16fdf462e235fc5523fd1 100644
--- a/morph/grid/local/Vagrantfile
+++ b/morph/grid/local/Vagrantfile
@@ -8,7 +8,7 @@ Vagrant.configure("2") do |config|
   # For a complete reference, please see the online documentation at
   # https://docs.vagrantup.com.
 
-  config.vm.define "payments" do |config|
+  config.vm.define "payments.localdev" do |config|
     config.vm.hostname = "payments"
     config.vm.box = "esselius/nixos"
     config.vm.box_version = "20.09"
@@ -20,7 +20,7 @@ Vagrant.configure("2") do |config|
     config.vm.provision "shell", inline: "sudo mv /tmp/payments-localdev-ssl/* /var/lib/letsencrypt/live/payments.localdev/"
   end
 
-  config.vm.define "storage1" do |config|
+  config.vm.define "storage1.localdev" do |config|
     config.vm.hostname = "storage1"
     config.vm.box = "esselius/nixos"
     config.vm.box_version = "20.09"
@@ -28,7 +28,7 @@ Vagrant.configure("2") do |config|
     config.vm.network "private_network", ip: "192.168.67.22"
   end
 
-  config.vm.define "storage2" do |config|
+  config.vm.define "storage2.localdev" do |config|
     config.vm.hostname = "storage2"
     config.vm.box = "esselius/nixos"
     config.vm.box_version = "20.09"
@@ -36,7 +36,7 @@ Vagrant.configure("2") do |config|
     config.vm.network "private_network", ip: "192.168.67.23"
   end
 
-  config.vm.define "monitoring" do |config|
+  config.vm.define "monitoring.localdev" do |config|
     config.vm.hostname = "monitoring"
     config.vm.box = "esselius/nixos"
     config.vm.box_version = "20.09"
@@ -54,4 +54,3 @@ Vagrant.configure("2") do |config|
   end
 
 end
-
diff --git a/morph/grid/local/config.json b/morph/grid/local/config.json
index 38f00367bf2fa36ad7663c89f7849146783b8515..3d377cc0e1ebbdec0dff421c806c901e2e5ce06d 100644
--- a/morph/grid/local/config.json
+++ b/morph/grid/local/config.json
@@ -1,4 +1,5 @@
-{ "publicStoragePort": 8898
+{ "domain": "localdev"
+, "publicStoragePort": 8898
 , "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
 , "stripeSecretKeyPath": "./secrets/stripe.secret"
 , "monitoringvpnKeyDir": "./secrets/monitoringvpn"
diff --git a/morph/grid/local/grid.nix b/morph/grid/local/grid.nix
index fdc0cde55be4f1b644c212ce20f6c3e44af8e3df..5345a16198e79dd8c91c8566fb62480ce5cea51a 100644
--- a/morph/grid/local/grid.nix
+++ b/morph/grid/local/grid.nix
@@ -1,64 +1,73 @@
-# Load the helper function and call it with arguments tailored for the local
-# grid.  It will make the morph configuration for us.  We share this function
-# with the production grid and have one fewer possible point of divergence.
-import ../../lib/make-grid.nix {
-  name = "LocalDev";
-  config = ./config.json;
-  nodes = cfg:
-  let
+let
+  pkgs = import <nixpkgs> { };
+
+  gridlib = import ../../lib;
+  rawConfig = pkgs.lib.trivial.importJSON ./config.json;
+  config = rawConfig // {
     sshUsers = import ./secrets/users.nix;
 
     # Get absolute vpn key directory path, as a string:
-    monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}";
+    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+  };
+
+  payments = {
+    imports = [
+      gridlib.issuer
+      (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.21"; }))
+      (gridlib.customize-issuer (config // {
+          monitoringvpnIPv4 = "172.23.23.11";
+      }))
+    ];
+  };
 
-    # TBD: derive these automatically:
-    hostsMap = {
-      "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
-      "172.23.23.11" = [ "payments" "payments.monitoringvpn" ];
-      "172.23.23.12" = [ "storage1" "storage1.monitoringvpn" ];
-      "172.23.23.13" = [ "storage2" "storage2.monitoringvpn" ];
-    };
-    vpnClientIPs = [ "172.23.23.11" "172.23.23.12" "172.23.23.13" ];
-    nodeExporterTargets = [ "monitoring" "payments" "storage1" "storage2" ];
+  storage1 = {
+    imports = [
+      gridlib.storage
+      (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.22"; }))
+      (gridlib.customize-storage (config // {
+        monitoringvpnIPv4 = "172.23.23.12";
+        stateVersion = "19.09";
+      }))
+    ];
+  };
 
-  in {
-    "payments" = import ../../lib/make-issuer.nix (cfg // rec {
-      publicIPv4 = "192.168.67.21";
-      monitoringvpnIPv4 = "172.23.23.11";
-      hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; });
-      stateVersion = "19.03";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-    });
+  storage2 = {
+    imports = [
+      gridlib.storage
+      (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.23"; }))
+      (gridlib.customize-storage (config // {
+        monitoringvpnIPv4 = "172.23.23.13";
+        stateVersion = "19.09";
+      }))
+    ];
+  };
 
-    "storage1" = import ../../lib/make-testing.nix (cfg // rec {
-      publicIPv4 = "192.168.67.22";
-      monitoringvpnIPv4 = "172.23.23.12";
-      hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; });
-      stateVersion = "19.09";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-    });
+  monitoring = {
+    imports = [
+      gridlib.monitoring
+      (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.24"; }))
+      (gridlib.customize-monitoring {
+        inherit hostsMap vpnClientIPs nodeExporterTargets;
+        inherit (config) domain monitoringvpnKeyDir;
+        monitoringvpnIPv4 = "172.23.23.1";
+        stateVersion = "19.09";
+      })
+    ];
+  };
 
-    "storage2" = import ../../lib/make-testing.nix (cfg // rec {
-      publicIPv4 = "192.168.67.23";
-      monitoringvpnIPv4 = "172.23.23.13";
-      hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; });
-      stateVersion = "19.09";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-    });
+  # TBD: derive these automatically:
+  hostsMap = {
+    "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
+    "172.23.23.11" = [ "payments" "payments.monitoringvpn" ];
+    "172.23.23.12" = [ "storage1" "storage1.monitoringvpn" ];
+    "172.23.23.13" = [ "storage2" "storage2.monitoringvpn" ];
+  };
+  vpnClientIPs = [ "172.23.23.11" "172.23.23.12" "172.23.23.13" ];
+  nodeExporterTargets = [ "monitoring" "payments" "storage1" "storage2" ];
 
-    "monitoring" = import ../../lib/make-monitoring.nix (cfg // rec {
-      publicIPv4 = "192.168.67.24";
-      monitoringvpnIPv4 = "172.23.23.1";
-      inherit vpnClientIPs;
-      inherit hostsMap;
-      inherit nodeExporterTargets;
-      hardware = import ./virtual-hardware.nix ({ inherit publicIPv4; });
-      stateVersion = "19.09";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-    });
+in {
+  network = {
+    description = "PrivateStorage.io LocalDev Grid";
   };
+  inherit payments monitoring storage1 storage2;
 }
diff --git a/morph/grid/production/config.json b/morph/grid/production/config.json
index ef7dc53649febcd7beb7901bb3608204df197059..21e080d587ae2713a73f756b2b7e078d843b2a95 100644
--- a/morph/grid/production/config.json
+++ b/morph/grid/production/config.json
@@ -1,4 +1,5 @@
-{ "publicStoragePort": 8898
+{ "domain": "private.storage"
+, "publicStoragePort": 8898
 , "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
 , "stripeSecretKeyPath": "./secrets/stripe.secret"
 , "monitoringvpnKeyDir": "./secrets/monitoringvpn"
diff --git a/morph/grid/production/grid.nix b/morph/grid/production/grid.nix
index fee0c9be6faed47d4a702b5b53c2419cbb677ba6..ae51174b4f15a72ca0c1d1798b067ecb1db64bb3 100644
--- a/morph/grid/production/grid.nix
+++ b/morph/grid/production/grid.nix
@@ -1,117 +1,111 @@
-# Load the helper function and call it with arguments tailored for the testing
-# grid.  It will make the morph configuration for us.  We share this function
-# with the testing grid and have one fewer possible point of divergence.
-import ../../lib/make-grid.nix {
-  name = "Production";
-  config = ./config.json;
-  nodes = cfg:
-    let
-      sshUsers = import ./secrets/users.nix;
+# See morph/grid/local/grid.nix for additional commentary.
+let
+  pkgs = import <nixpkgs> { };
 
-      # Get absolute vpn key directory path, as a string:
-      monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}";
+  gridlib = import ../../lib;
+  rawConfig = pkgs.lib.trivial.importJSON ./config.json;
+  config = rawConfig // {
+    sshUsers = import ./secrets/users.nix;
 
-      # TBD: derive these automatically:
-      hostsMap = {
-        "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
-        "172.23.23.11" = [   "payments"   "payments.monitoringvpn" ];
-        "172.23.23.21" = [ "storage001" "storage001.monitoringvpn" ];
-        "172.23.23.22" = [ "storage002" "storage002.monitoringvpn" ];
-        "172.23.23.23" = [ "storage003" "storage003.monitoringvpn" ];
-        "172.23.23.24" = [ "storage004" "storage004.monitoringvpn" ];
-        "172.23.23.25" = [ "storage005" "storage005.monitoringvpn" ];
-      };
-      vpnClientIPs = [
-        "172.23.23.11"
-        "172.23.23.21"
-        "172.23.23.22"
-        "172.23.23.23"
-        "172.23.23.24"
-        "172.23.23.25"
-      ];
-      nodeExporterTargets = [
-        "monitoring"
-        "payments"
-        "storage001"
-        "storage002"
-        "storage003"
-        "storage004"
-        "storage005"
-      ];
+    # Get absolute vpn key directory path, as a string:
+    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+  };
 
-    in {
-    # Here are the hosts that are in this morph network.  This is sort of like
-    # a server manifest.  We try to keep as many of the specific details as
-    # possible out of *this* file so that this file only grows as server count
-    # grows.  If it grows too much, we can load servers by listing contents of
-    # a directory or reading from another JSON file or some such.  For now,
-    # I'm just manually maintaining these entries.
-    #
-    # The name on the left of the `=` is mostly irrelevant but it does provide
-    # a default hostname for the server if the configuration on the right side
-    # doesn't specify one.
-    #
-    # The names must be unique!
-    "payments.privatestorage.io" = import ../../lib/make-issuer.nix (cfg // {
-      publicIPv4 = "18.184.142.208";
-      monitoringvpnIPv4 = "172.23.23.11";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-      hardware = ../../lib/issuer-aws.nix;
-      stateVersion = "19.03";
-    });
+  payments = {
+    imports = [
+      gridlib.issuer
+      gridlib.hardware-aws
+      (gridlib.customize-issuer (config // {
+        monitoringvpnIPv4 = "172.23.23.11";
+      }))
+    ];
+  };
 
-    "storage001" = import ../../lib/make-storage.nix (cfg // {
-        cfg = import ./storage001-config.nix;
-        inherit sshUsers;
-        hardware = ./storage001-hardware.nix;
-        stateVersion = "19.09";
-        monitoringvpnIPv4 = "172.23.23.21";
-        inherit monitoringvpnKeyDir;
-    });
-    "storage002" = import ../../lib/make-storage.nix (cfg // {
-        cfg = import ./storage002-config.nix;
-        inherit sshUsers;
-        hardware = ./storage002-hardware.nix;
-        stateVersion = "19.09";
-        monitoringvpnIPv4 = "172.23.23.22";
-        inherit monitoringvpnKeyDir;
-    });
-    "storage003" = import ../../lib/make-storage.nix (cfg // {
-        cfg = import ./storage003-config.nix;
-        inherit sshUsers;
-        hardware = ./storage003-hardware.nix;
+  monitoring = {
+    imports = [
+      gridlib.monitoring
+      gridlib.hardware-aws
+      (gridlib.customize-monitoring {
+        inherit hostsMap vpnClientIPs nodeExporterTargets;
+        inherit (config) domain monitoringvpnKeyDir;
+        monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
-        monitoringvpnIPv4 = "172.23.23.23";
-        inherit monitoringvpnKeyDir;
-    });
-    "storage004" = import ../../lib/make-storage.nix (cfg // {
-        cfg = import ./storage004-config.nix;
-        inherit sshUsers;
-        hardware = ./storage004-hardware.nix;
-        stateVersion = "19.09";
-        monitoringvpnIPv4 = "172.23.23.24";
-        inherit monitoringvpnKeyDir;
-    });
-    "storage005" = import ../../lib/make-storage.nix (cfg // {
-        cfg = import ./storage005-config.nix;
-        inherit sshUsers;
-        hardware = ./storage005-hardware.nix;
-        stateVersion = "19.03";
-        monitoringvpnIPv4 = "172.23.23.25";
-        inherit monitoringvpnKeyDir;
-    });
+      })
+    ];
+  };
+
+  defineStorageNode = name: { vpnIP, stateVersion }:
+  let
+    nodecfg = import "${./.}/${name}-config.nix";
+    hardware ="${./.}/${name}-hardware.nix";
+  in {
+    imports = [
+      # Get some of the very lowest-level system configuration for this
+      # node.  This isn't all *completely* hardware related.  Maybe some
+      # more factoring is in order, someday.
+      hardware
+
+      # Slightly awkwardly, enable some of our hardware / network / bootloader options.
+      ../../../nixos/modules/100tb.nix
+
+      # Get all of the configuration that is common across all storage nodes.
+      gridlib.storage
+
+      # Then customize the storage system a little bit based on this node's particulars.
+      (gridlib.customize-storage (config // nodecfg // {
+        monitoringvpnIPv4 = vpnIP;
+        inherit stateVersion;
+      }))
+    ];
+
+    # And supply configuration for those hardware / network / bootloader
+    # options.  See the 100tb module for handling of this value.  The module
+    # name is quoted because `1` makes `100tb` look an awful lot like a
+    # number.
+   "100tb".config = nodecfg;
+  };
+
+  # Define all of the storage nodes for this grid.
+  storageNodes = builtins.mapAttrs defineStorageNode {
+    storage001 = { vpnIP = "172.23.23.21"; stateVersion = "19.09"; };
+    storage002 = { vpnIP = "172.23.23.22"; stateVersion = "19.09"; };
+    storage003 = { vpnIP = "172.23.23.23"; stateVersion = "19.09"; };
+    storage004 = { vpnIP = "172.23.23.24"; stateVersion = "19.09"; };
+    storage005 = { vpnIP = "172.23.23.25"; stateVersion = "19.03"; };
+  };
+
+  # TBD: derive these automatically:
+  hostsMap = {
+    "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
+    "172.23.23.11" = [   "payments"   "payments.monitoringvpn" ];
+    "172.23.23.21" = [ "storage001" "storage001.monitoringvpn" ];
+    "172.23.23.22" = [ "storage002" "storage002.monitoringvpn" ];
+    "172.23.23.23" = [ "storage003" "storage003.monitoringvpn" ];
+    "172.23.23.24" = [ "storage004" "storage004.monitoringvpn" ];
+    "172.23.23.25" = [ "storage005" "storage005.monitoringvpn" ];
+  };
+  vpnClientIPs = [
+    "172.23.23.11"
+    "172.23.23.21"
+    "172.23.23.22"
+    "172.23.23.23"
+    "172.23.23.24"
+    "172.23.23.25"
+  ];
+  nodeExporterTargets = [
+    "monitoring"
+    "payments"
+    "storage001"
+    "storage002"
+    "storage003"
+    "storage004"
+    "storage005"
+  ];
 
-    "monitoring" = import ../../lib/make-monitoring.nix (cfg // {
-      publicIPv4 = "monitoring.private.storage";
-      monitoringvpnIPv4 = "172.23.23.1";
-      inherit monitoringvpnKeyDir;
-      inherit vpnClientIPs;
-      inherit hostsMap;
-      inherit nodeExporterTargets;
-      hardware = ../../lib/issuer-aws.nix;
-      stateVersion = "19.09";
-      inherit sshUsers;
-    });
+in {
+  network = {
+    description = "PrivateStorage.io Production Grid";
   };
-}
+  inherit payments;
+  inherit monitoring;
+} // storageNodes
diff --git a/morph/grid/production/storage001-hardware.nix b/morph/grid/production/storage001-hardware.nix
index 4cd9f59b76dd77b6e6e85709b3fbee771677b641..b2ca97c1db1b9721b93f2662d6e8d34189d5a0ab 100644
--- a/morph/grid/production/storage001-hardware.nix
+++ b/morph/grid/production/storage001-hardware.nix
@@ -12,6 +12,7 @@
   boot.initrd.kernelModules = [ ];
   boot.kernelModules = [ "kvm-intel" ];
   boot.extraModulePackages = [ ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
 
   fileSystems."/" =
     { device = "/dev/disk/by-uuid/f72c1f46-6723-45bf-9ef7-92f31cc37589";
@@ -30,9 +31,12 @@
       fsType = "zfs";
     };
 
-  swapDevices =
-    [ { device = "/dev/disk/by-uuid/f986a811-4912-4e9a-8bc3-01cb6926c4c6"; }
-    ];
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
+
 
   nix.maxJobs = lib.mkDefault 24;
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
diff --git a/morph/grid/production/storage002-hardware.nix b/morph/grid/production/storage002-hardware.nix
index 4fc3a4097e05ec8c38c86db6bfce92e2a1af6f35..2f354ad29930f048f7eb20b54a1504ed87db85a1 100644
--- a/morph/grid/production/storage002-hardware.nix
+++ b/morph/grid/production/storage002-hardware.nix
@@ -12,6 +12,7 @@
   boot.initrd.kernelModules = [ ];
   boot.kernelModules = [ "kvm-intel" ];
   boot.extraModulePackages = [ ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
 
   fileSystems."/" =
     { device = "/dev/disk/by-uuid/0e92ada9-effb-42e2-a26a-9cdb529bcdc7";
@@ -30,9 +31,11 @@
       fsType = "ext4";
     };
 
-  swapDevices =
-    [ { device = "/dev/disk/by-uuid/f762b5e2-bbdd-4a02-bbd9-0bf6b11e0ab5"; }
-    ];
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
 
   nix.maxJobs = lib.mkDefault 24;
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
diff --git a/morph/grid/production/storage003-hardware.nix b/morph/grid/production/storage003-hardware.nix
index 9882f5372cecd52794e1500bdef30e367008496e..d8ffe5d59fb39ba4a9c6b1b73313f199a2ed980b 100644
--- a/morph/grid/production/storage003-hardware.nix
+++ b/morph/grid/production/storage003-hardware.nix
@@ -13,6 +13,7 @@
   boot.kernelModules = [ "kvm-intel" ];
   boot.extraModulePackages = [ ];
   boot.supportedFilesystems = [ "zfs" ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
 
   fileSystems."/" =
     { device = "/dev/disk/by-uuid/240fc1f6-cd55-48a3-ac80-5b3550a32ef5";
@@ -31,7 +32,11 @@
       fsType = "zfs";
     };
 
-  swapDevices = [ ];
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
 
   nix.maxJobs = lib.mkDefault 24;
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
diff --git a/morph/grid/production/storage004-hardware.nix b/morph/grid/production/storage004-hardware.nix
index 07de74e20ef58ab474b02248bcb6eed6189e1079..1fe78a76e813605d8e181d5a858062f77114ba38 100644
--- a/morph/grid/production/storage004-hardware.nix
+++ b/morph/grid/production/storage004-hardware.nix
@@ -12,6 +12,7 @@
   boot.initrd.kernelModules = [ ];
   boot.kernelModules = [ "kvm-intel" ];
   boot.extraModulePackages = [ ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
 
   fileSystems."/" =
     { device = "/dev/disk/by-uuid/d628122e-05d9-4212-b6a5-4b9516d85dbe";
@@ -25,7 +26,11 @@
       fsType = "zfs";
     };
 
-  swapDevices = [ ];
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
 
   nix.maxJobs = lib.mkDefault 32;
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
diff --git a/morph/grid/production/storage005-hardware.nix b/morph/grid/production/storage005-hardware.nix
index 9a5ad02725e30b00619978035772d60bec9fcb8a..e8f7b6391b4cb1c8d3e6059c1fd09512a0cc370b 100644
--- a/morph/grid/production/storage005-hardware.nix
+++ b/morph/grid/production/storage005-hardware.nix
@@ -12,6 +12,7 @@
   boot.initrd.kernelModules = [ ];
   boot.kernelModules = [ "kvm-intel" ];
   boot.extraModulePackages = [ ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
 
   fileSystems."/" =
     { device = "/dev/disk/by-uuid/2653c6bb-396f-4911-b9ff-b68de8f9715d";
@@ -30,7 +31,11 @@
     fsType = "zfs";
   };
 
-  swapDevices = [ ];
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
 
   nix.maxJobs = lib.mkDefault 32;
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
diff --git a/morph/grid/testing/config.json b/morph/grid/testing/config.json
index a44b465f7f293f9d70c369a076c30b6cf810924f..c069bbed531e63a425e16a1838dedbfd2f17374d 100644
--- a/morph/grid/testing/config.json
+++ b/morph/grid/testing/config.json
@@ -1,4 +1,5 @@
-{ "publicStoragePort": 8898
+{ "domain": "privatestorage-staging.com"
+, "publicStoragePort": 8898
 , "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
 , "stripeSecretKeyPath": "./secrets/stripe.secret"
 , "monitoringvpnKeyDir": "./secrets/monitoringvpn"
diff --git a/morph/grid/testing/grid.nix b/morph/grid/testing/grid.nix
index e31a28f2eb7817f393f4e8b6b71972b7fd2f79f1..19eefd9d50d49094a0bcf87698f8c8091032e1fa 100644
--- a/morph/grid/testing/grid.nix
+++ b/morph/grid/testing/grid.nix
@@ -1,54 +1,62 @@
-# Load the helper function and call it with arguments tailored for the testing
-# grid.  It will make the morph configuration for us.  We share this function
-# with the production grid and have one fewer possible point of divergence.
-import ../../lib/make-grid.nix {
-  name = "Testing";
-  config = ./config.json;
-  nodes = cfg:
-  let
+# See morph/grid/local/grid.nix for additional commentary.
+let
+  pkgs = import <nixpkgs> { };
+
+  gridlib = import ../../lib;
+  rawConfig = pkgs.lib.trivial.importJSON ./config.json;
+  config = rawConfig // {
     sshUsers = import ./secrets/users.nix;
 
     # Get absolute vpn key directory path, as a string:
-    monitoringvpnKeyDir = toString ./. + "/${cfg.monitoringvpnKeyDir}";
+    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+  };
 
-    # TBD: derive these automatically:
-    hostsMap = {
-      "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
-      "172.23.23.11" = [ "payments"   "payments.monitoringvpn"   ];
-      "172.23.23.12" = [ "storage001" "storage001.monitoringvpn" ];
-    };
-    vpnClientIPs = [ "172.23.23.11" "172.23.23.12" ];
-    nodeExporterTargets = [ "monitoring" "payments" "storage001" ];
+  payments = {
+    imports = [
+      gridlib.issuer
+      gridlib.hardware-aws
+      (gridlib.customize-issuer (config // {
+        monitoringvpnIPv4 = "172.23.23.11";
+      }))
+    ];
+  };
 
-  in {
-    "payments" = import ../../lib/make-issuer.nix (cfg // {
-      publicIPv4 = "18.194.183.13";
-      monitoringvpnIPv4 = "172.23.23.11";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-      hardware = ../../lib/issuer-aws.nix;
-      stateVersion = "19.03";
-    });
+  storage001 = {
+    imports = [
+      gridlib.storage
+      ./testing001-hardware.nix
+      (gridlib.customize-storage (config // {
+        monitoringvpnIPv4 = "172.23.23.12";
+        stateVersion = "19.03";
+      }))
+    ];
+  };
 
-    "storage001" = import ../../lib/make-testing.nix (cfg // {
-      publicIPv4 = "3.120.26.190";
-      monitoringvpnIPv4 = "172.23.23.12";
-      inherit monitoringvpnKeyDir;
-      inherit sshUsers;
-      hardware = ./testing001-hardware.nix;
-      stateVersion = "19.03";
-    });
+  monitoring = {
+    imports = [
+      gridlib.monitoring
+      gridlib.hardware-aws
+      (gridlib.customize-monitoring {
+        inherit hostsMap vpnClientIPs nodeExporterTargets;
+        inherit (config) domain monitoringvpnKeyDir;
+        monitoringvpnIPv4 = "172.23.23.1";
+        stateVersion = "19.09";
+      })
+    ];
+  };
+
+  # TBD: derive these automatically:
+  hostsMap = {
+    "172.23.23.1"  = [ "monitoring" "monitoring.monitoringvpn" ];
+    "172.23.23.11" = [ "payments"   "payments.monitoringvpn"   ];
+    "172.23.23.12" = [ "storage001" "storage001.monitoringvpn" ];
+  };
+  vpnClientIPs = [ "172.23.23.11" "172.23.23.12" ];
+  nodeExporterTargets = [ "monitoring" "payments" "storage001" ];
 
-    "monitoring" = import ../../lib/make-monitoring.nix (cfg // {
-      publicIPv4 = "18.156.171.217";
-      monitoringvpnIPv4 = "172.23.23.1";
-      inherit monitoringvpnKeyDir;
-      inherit vpnClientIPs;
-      inherit hostsMap;
-      inherit nodeExporterTargets;
-      hardware = ../../lib/issuer-aws.nix;
-      stateVersion = "19.09";
-      inherit sshUsers;
-    });
+in {
+  network = {
+    description = "PrivateStorage.io Testing Grid";
   };
+  inherit payments monitoring storage001;
 }
diff --git a/morph/grid/testing/testing001-hardware.nix b/morph/grid/testing/testing001-hardware.nix
index 958a247862a7e4bb2581e7d1bb85cc0f85f3ea24..dd2f9733796875bb9d8a549538cba5743a8727a3 100644
--- a/morph/grid/testing/testing001-hardware.nix
+++ b/morph/grid/testing/testing001-hardware.nix
@@ -1,6 +1,13 @@
 {
   imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
   ec2.hvm = true;
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
+
 
   boot.supportedFilesystems = [ "zfs" ];
   networking.hostId = "10000000";
diff --git a/morph/lib/customize-issuer.nix b/morph/lib/customize-issuer.nix
new file mode 100644
index 0000000000000000000000000000000000000000..28edb72e7e0b74879e9e676113c327f50b040d40
--- /dev/null
+++ b/morph/lib/customize-issuer.nix
@@ -0,0 +1,88 @@
+# Define a function which returns a value which fills in all the holes left by
+# ``issuer.nix``.
+{
+  # A path on the deployment system to a file containing the Ristretto signing
+  # key.  This is used as the source of the Ristretto signing key morph
+  # secret.
+  ristrettoSigningKeyPath
+
+  # A path on the deployment system to a file containing the Stripe secret
+  # key.  This is used as the source of the Stripe secret key morph secret.
+, stripeSecretKeyPath
+
+  # A path on the deployment system to a directory containing a number of
+  # VPN-related secrets.  This is expected to contain a number of files named
+  # like ``<VPN IPv4 address>.key`` containing the VPN private key for the
+  # corresponding host.  It must also contain ``server.pub`` and
+  # ``preshared.key`` holding the VPN server's public key and the pre-shared
+  # key, respectively.  All of these things are used as the sources of various
+  # VPN-related morph secrets.
+, monitoringvpnKeyDir
+
+  # A string giving the IP address and port number (":"-separated) of the VPN
+  # server.
+, monitoringvpnEndpoint
+
+  # A string giving the VPN IPv4 address for this system.
+, monitoringvpnIPv4
+
+  # A string giving the domain name associated with this grid.  This is meant
+  # to be combined with the hostname for this system to produce a
+  # fully-qualified domain name.  For example, an issuer might have "payments"
+  # as its hostname and belong to a grid with the domain
+  # "example-grid.invalid".  This ``domain`` parameter should have the value
+  # ``"example-grid.invalid"`` for the system figure out that
+  # ``payments.example-grid.invalid`` is the name of this system.
+, domain
+
+  # A set mapping usernames as strings to SSH public keys as strings.  For
+  # each element of the site, the indicated user is configured on the system
+  # with the indicated SSH key as an authorized key.
+, sshUsers
+
+  # A string giving an email address to use for Let's Encrypt registration and
+  # certificate issuance.
+, letsEncryptAdminEmail
+
+  # A list of strings giving the domain names that point at this issuer
+  # system.  These will all be included in Let's Encrypt certificate.
+, issuerDomains
+
+  # A list of strings giving CORS Origins will the issuer will be configured
+  # to allow.
+, allowedChargeOrigins
+, ...
+}:
+{ config, ... }: {
+  # The morph default deployment target the name of the node in the network
+  # attrset.  We don't always want to give the node its proper public address
+  # there (because it depends on which domain is associated with the grid
+  # being configured and using variable names complicates a lot of things).
+  # Instead, just tell morph how to reach the node here - by using its fully
+  # qualified domain name.
+  deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}";
+
+  deployment.secrets = {
+    "ristretto-signing-key".source = ristrettoSigningKeyPath;
+    "stripe-secret-key".source = stripeSecretKeyPath;
+    "monitoringvpn-secret-key".source = "${monitoringvpnKeyDir}/${monitoringvpnIPv4}.key";
+    "monitoringvpn-preshared-key".source = "${monitoringvpnKeyDir}/preshared.key";
+  };
+
+  networking.domain = domain;
+
+  services.private-storage.sshUsers = sshUsers;
+  services.private-storage.monitoring.vpn.client = {
+    enable = true;
+    ip = monitoringvpnIPv4;
+    endpoint = monitoringvpnEndpoint;
+    endpointPublicKeyFile = "${monitoringvpnKeyDir}/server.pub";
+  };
+
+  services.private-storage-issuer = {
+    inherit letsEncryptAdminEmail allowedChargeOrigins;
+    domains = issuerDomains;
+  };
+
+  system.stateVersion = "19.03";
+}
diff --git a/morph/lib/customize-monitoring.nix b/morph/lib/customize-monitoring.nix
new file mode 100644
index 0000000000000000000000000000000000000000..c50eb5062b35480d0b3d296cfaea8abd999f36c5
--- /dev/null
+++ b/morph/lib/customize-monitoring.nix
@@ -0,0 +1,57 @@
+# Define a function which returns a value which fills in all the holes left by
+# ``monitoring.nix``.
+{
+  # A set mapping VPN IP addresses as strings to lists of hostnames as
+  # strings.  The system's ``/etc/hosts`` will be populated with this
+  # information.  Apart from helping with normal forward resolution, this
+  # *also* gives us reverse resolution from the VPN IPs to hostnames which
+  # allows Grafana to show us hostnames instead of VPN IP addresses.
+  hostsMap
+
+  # See ``customize-issuer.nix``.
+, monitoringvpnKeyDir
+, monitoringvpnIPv4
+, domain
+
+  # A list of VPN IP addresses as strings indicating which clients will be
+  # allowed onto the VPN.
+, vpnClientIPs
+
+  # A list of VPN clients (IP addresses or hostnames) as strings indicating
+  # which nodes to scrape "nodeExporter" metrics from.
+, nodeExporterTargets
+
+  # A list of VPN clients (IP addresses or hostnames) as strings indicating
+  # which nodes to scrape "nginxExporter" metrics from.
+, nginxExporterTargets ? []
+
+  # A string giving the NixOS state version for the system.
+, stateVersion
+, ...
+}:
+{ config, ... }: {
+  # See customize-issuer.nix for an explanatoin of targetHost value.
+  deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}";
+
+  deployment.secrets = {
+    "monitoringvpn-private-key".source = "${monitoringvpnKeyDir}/server.key";
+    "monitoringvpn-preshared-key".source = "${monitoringvpnKeyDir}/preshared.key";
+  };
+
+  networking.domain = domain;
+  networking.hosts = hostsMap;
+
+  services.private-storage.monitoring.vpn.server = {
+    enable = true;
+    ip = monitoringvpnIPv4;
+    inherit vpnClientIPs;
+    pubKeysPath = monitoringvpnKeyDir;
+  };
+
+  services.private-storage.monitoring.prometheus = {
+    inherit nodeExporterTargets;
+    inherit nginxExporterTargets;
+  };
+
+  system.stateVersion = stateVersion;
+}
diff --git a/morph/lib/customize-storage.nix b/morph/lib/customize-storage.nix
new file mode 100644
index 0000000000000000000000000000000000000000..0a08743633126b5898e61a877e62a7b58314b34e
--- /dev/null
+++ b/morph/lib/customize-storage.nix
@@ -0,0 +1,47 @@
+# Define a function which returns a value which fills in all the holes left by
+# ``storage.nix``.
+{
+  # See ``customize-issuer.nix``
+  ristrettoSigningKeyPath
+, monitoringvpnKeyDir
+, monitoringvpnEndpoint
+, monitoringvpnIPv4
+, sshUsers
+, domain
+
+  # An integer giving the value of a single pass in byte×months.
+, passValue
+
+  # An integer giving the port number to include in Tahoe storage service
+  # advertisements and on which to listen for storage connections.
+, publicStoragePort
+
+  # A string giving the NixOS state version for the system.
+, stateVersion
+, ...
+}:
+{ config, ... }: {
+  # See customize-issuer.nix for an explanatoin of targetHost value.
+  deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}";
+
+  deployment.secrets = {
+    "ristretto-signing-key".source = ristrettoSigningKeyPath;
+    "monitoringvpn-secret-key".source = "${monitoringvpnKeyDir}/${monitoringvpnIPv4}.key";
+    "monitoringvpn-preshared-key".source = "${monitoringvpnKeyDir}/preshared.key";
+  };
+
+  networking.domain = domain;
+
+  services.private-storage = {
+    inherit sshUsers passValue publicStoragePort;
+  };
+
+  services.private-storage.monitoring.vpn.client = {
+    enable = true;
+    ip = monitoringvpnIPv4;
+    endpoint = monitoringvpnEndpoint;
+    endpointPublicKeyFile = "${monitoringvpnKeyDir}/server.pub";
+  };
+
+  system.stateVersion = stateVersion;
+}
diff --git a/morph/lib/default.nix b/morph/lib/default.nix
new file mode 100644
index 0000000000000000000000000000000000000000..bdd92f4bfe52eba2e19df3ac73a087a4af4a53dc
--- /dev/null
+++ b/morph/lib/default.nix
@@ -0,0 +1,16 @@
+# Gather up the grid library functionality and present it in a (somewhat)
+# coherent public interface.  Application code should prefer these names over
+# directly importing the source files in this directory.
+{
+  hardware-aws = import ./issuer-aws.nix;
+  hardware-virtual = import ./hardware-virtual.nix;
+
+  issuer = import ./issuer.nix;
+  customize-issuer = import ./customize-issuer.nix;
+
+  storage = import ./storage.nix;
+  customize-storage = import ./customize-storage.nix;
+
+  monitoring = import ./monitoring.nix;
+  customize-monitoring = import ./customize-monitoring.nix;
+}
diff --git a/morph/grid/local/virtual-hardware.nix b/morph/lib/hardware-virtual.nix
similarity index 95%
rename from morph/grid/local/virtual-hardware.nix
rename to morph/lib/hardware-virtual.nix
index d5e9067bd5f3b3ca2ea1bb46746253fa39b25cf6..cf1582792bff77c491210ee5e91f99bfbffbf9f3 100644
--- a/morph/grid/local/virtual-hardware.nix
+++ b/morph/lib/hardware-virtual.nix
@@ -11,6 +11,7 @@
 
   boot.initrd.availableKernelModules = [ "ata_piix" "sd_mod" "sr_mod" ];
   boot.initrd.kernelModules = [ ];
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
   boot.kernelModules = [ ];
   boot.extraModulePackages = [ ];
 
@@ -33,4 +34,3 @@
   # We want to push packages with morph without having to sign them
   nix.trustedUsers = [ "@wheel" "root" "vagrant" ];
 }
-
diff --git a/morph/lib/issuer-aws.nix b/morph/lib/issuer-aws.nix
index b4d4757ad5597b69363ef12e4297aec80913f00e..3febd796b3eceb5a2da73e4874d52d50767da947 100644
--- a/morph/lib/issuer-aws.nix
+++ b/morph/lib/issuer-aws.nix
@@ -1,4 +1,15 @@
-{
+{ lib, ... }: {
   imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
+
+  # amazon-image.nix isn't quite aware of nvme-attached storage so give it a
+  # little help configuring grub.
+  boot.loader.grub.device = lib.mkForce "/dev/nvme0n1";
+
   ec2.hvm = true;
+  boot.kernel.sysctl = { "vm.swappiness" = 0; };
+  swapDevices = [ {
+    device = "/var/swapfile";
+    size = 8192; # megabytes
+    randomEncryption = true;
+  } ];
 }
diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix
new file mode 100644
index 0000000000000000000000000000000000000000..417ef7965ea0120322995059fcca7a5a9afe2543
--- /dev/null
+++ b/morph/lib/issuer.nix
@@ -0,0 +1,56 @@
+# This is all of the static NixOS system configuration necessary to specify an
+# "issuer"-type system.  The configuration has various holes in it which must
+# be filled somehow.  These holes correspond to configuration which is not
+# statically known.  This value is suitable for use as a module to be imported
+# into a more complete system configuration.  It is expected that the holes
+# will be filled by a sibling module created by ``customize-issuer.nix``.
+rec {
+  deployment = {
+    secrets = {
+      "ristretto-signing-key" = {
+        destination = "/run/keys/ristretto.signing-key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "zkapissuer.service"];
+      };
+      "stripe-secret-key" = {
+        destination = "/run/keys/stripe.secret-key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "zkapissuer.service"];
+      };
+
+      "monitoringvpn-secret-key" = {
+        destination = "/run/keys/monitoringvpn/client.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+      "monitoringvpn-preshared-key" = {
+        destination = "/run/keys/monitoringvpn/preshared.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+    };
+  };
+
+  imports = [
+    ../../nixos/modules/issuer.nix
+    ../../nixos/modules/monitoring/vpn/client.nix
+    ../../nixos/modules/monitoring/exporters/node.nix
+  ];
+
+  services.private-storage-issuer = {
+    enable = true;
+    tls = true;
+    ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
+    stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination;
+    database = "SQLite3";
+    databasePath = "/var/db/vouchers.sqlite3";
+  };
+}
diff --git a/morph/lib/make-grid.nix b/morph/lib/make-grid.nix
deleted file mode 100644
index de10df1e9a62ee0ac7fde98070743ee4a9cf484b..0000000000000000000000000000000000000000
--- a/morph/lib/make-grid.nix
+++ /dev/null
@@ -1,19 +0,0 @@
-# Define a function for making a morph configuration for a storage grid.  It
-# takes two arguments.  A string like "Production" giving the name of the grid
-# and a function that takes the grid configuration as an argument and returns
-# a set of nodes specifying the addresses and NixOS configurations for each
-# server in the morph network.
-{ name, config, nodes }:
-let
-  pkgs = import <nixpkgs> { };
-  # Load our JSON configuration for later use.
-  cfg = pkgs.lib.trivial.importJSON config;
-in
-{
-  network =  {
-    # Make all of the hosts in this network use the nixpkgs we pinned above.
-    inherit pkgs;
-    # This is just for human consumption as far as I can tell.
-    description = "PrivateStorage.io ${name} Grid";
-  };
-} // (nodes cfg)
diff --git a/morph/lib/make-issuer.nix b/morph/lib/make-issuer.nix
deleted file mode 100644
index bbdf0cebbf770738e9ccb997daec75e58df021b5..0000000000000000000000000000000000000000
--- a/morph/lib/make-issuer.nix
+++ /dev/null
@@ -1,91 +0,0 @@
-{ hardware
-, ristrettoSigningKeyPath
-, stripeSecretKeyPath
-, issuerDomains
-, letsEncryptAdminEmail
-, allowedChargeOrigins
-, sshUsers
-, stateVersion
-, publicIPv4
-, monitoringvpnKeyDir ? null
-, monitoringvpnIPv4 ? null
-, monitoringvpnEndpoint ? null
-, ...
-}: let
-
-  enableVpn = monitoringvpnKeyDir != null &&
-              monitoringvpnIPv4 != null &&
-              monitoringvpnEndpoint != null;
-
-  vpnSecrets = if !enableVpn then {} else {
-    "monitoringvpn-secret-key" = {
-      source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key";
-      destination = "/run/keys/monitoringvpn/client.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-    "monitoringvpn-preshared-key" = {
-      source = monitoringvpnKeyDir + "/preshared.key";
-      destination = "/run/keys/monitoringvpn/preshared.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-  };
-
-in rec {
-  deployment = {
-    targetHost = publicIPv4;
-
-    secrets = {
-      "ristretto-signing-key" = {
-        source = ristrettoSigningKeyPath;
-        destination = "/run/keys/ristretto.signing-key";
-        owner.user = "root";
-        owner.group = "root";
-        permissions = "0400";
-        action = ["sudo" "systemctl" "restart" "zkapissuer.service"];
-      };
-      "stripe-secret-key" = {
-        source = stripeSecretKeyPath;
-        destination = "/run/keys/stripe.secret-key";
-        owner.user = "root";
-        owner.group = "root";
-        permissions = "0400";
-        action = ["sudo" "systemctl" "restart" "zkapissuer.service"];
-      };
-    } // vpnSecrets;
-  };
-
-  imports = [
-    hardware
-    ../../nixos/modules/issuer.nix
-    ../../nixos/modules/monitoring/vpn/client.nix
-    ../../nixos/modules/monitoring/exporters/node.nix
-  ];
-
-  services.private-storage.sshUsers = sshUsers;
-  services.private-storage-issuer = {
-    enable = true;
-    tls = true;
-    ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
-    stripeSecretKeyPath = deployment.secrets.stripe-secret-key.destination;
-    database = "SQLite3";
-    databasePath = "/var/db/vouchers.sqlite3";
-    inherit letsEncryptAdminEmail;
-    domains = issuerDomains;
-    inherit allowedChargeOrigins;
-  };
-
-  system.stateVersion = stateVersion;
-
-  services.private-storage.monitoring.vpn.client = if !enableVpn then {} else {
-    enable = true;
-    ip = monitoringvpnIPv4;
-    endpoint = monitoringvpnEndpoint;
-    endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub";
-  };
-}
diff --git a/morph/lib/make-monitoring.nix b/morph/lib/make-monitoring.nix
deleted file mode 100644
index 592a859657e624e8fdf5632f8144c5acc6919e8c..0000000000000000000000000000000000000000
--- a/morph/lib/make-monitoring.nix
+++ /dev/null
@@ -1,77 +0,0 @@
-{ publicIPv4
-, hardware
-, publicStoragePort
-, ristrettoSigningKeyPath
-, passValue
-, sshUsers
-, stateVersion
-, monitoringvpnIPv4 ? null
-, monitoringvpnKeyDir ? null
-, vpnClientIPs ? null
-, nodeExporterTargets ? []
-, nginxExporterTargets ? []
-, hostsMap ? {}
-, ... }: let
-
-  enableVpn = monitoringvpnKeyDir != null &&
-              monitoringvpnIPv4 != null &&
-              vpnClientIPs != null;
-
-  vpnSecrets = if !enableVpn then {} else {
-    "monitoringvpn-private-key" = {
-      source = monitoringvpnKeyDir + "/server.key";
-      destination = "/run/keys/monitoringvpn/server.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-    "monitoringvpn-preshared-key" = {
-      source = monitoringvpnKeyDir + "/preshared.key";
-      destination = "/run/keys/monitoringvpn/preshared.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-  };
-
-in rec {
-
-  deployment = {
-    targetHost = publicIPv4;
-    secrets = vpnSecrets;
-  };
-
-  imports = [
-    hardware
-    ../../nixos/modules/monitoring/vpn/server.nix
-    ../../nixos/modules/monitoring/server/grafana.nix
-    ../../nixos/modules/monitoring/server/prometheus.nix
-    ../../nixos/modules/monitoring/exporters/node.nix
-    # Loki 0.3.0 from Nixpkgs 19.09 is too old and does not work:
-    # ../../nixos/modules/monitoring/server/loki.nix
-  ];
-
-  services.private-storage.monitoring.vpn.server = if !enableVpn then {} else {
-    enable = true;
-    ip = monitoringvpnIPv4;
-    inherit vpnClientIPs;
-    pubKeysPath = monitoringvpnKeyDir;
-  };
-
-  services.private-storage.monitoring.grafana = {
-    domain = "monitoring.private.storage";
-    prometheusUrl = "http://localhost:9090/";
-    lokiUrl = "http://localhost:3100/";
-  };
-
-  services.private-storage.monitoring.prometheus = {
-    inherit nodeExporterTargets;
-    inherit nginxExporterTargets;
-  };
-
-  system.stateVersion = stateVersion;
-
-  networking.hosts = hostsMap;
-}
diff --git a/morph/lib/make-storage.nix b/morph/lib/make-storage.nix
deleted file mode 100644
index 6619336d758f69a677e9178592357480aed3f0c8..0000000000000000000000000000000000000000
--- a/morph/lib/make-storage.nix
+++ /dev/null
@@ -1,109 +0,0 @@
-# Define the function that defines the node.
-{ cfg                        # Get the configuration that's specific to this node.
-, hardware                   # The path to the hardware configuration for this node.
-, publicStoragePort          # The storage port number on which to accept connections.
-, ristrettoSigningKeyPath    # The *local* path to the Ristretto signing key file.
-, passValue                  # Bytes component of size×time value of passes.
-, sshUsers                   # Users for which to configure SSH access to this node.
-, stateVersion               # The value for system.stateVersion on this node.
-                             # This value determines the NixOS release with
-                             # which your system is to be compatible, in order
-                             # to avoid breaking some software such as
-                             # database servers. You should change this only
-                             # after NixOS release notes say you should.
-, monitoringvpnKeyDir ? null # The directory that holds the VPN keys.
-, monitoringvpnIPv4 ? null   # This node's IP in the monitoring VPN.
-, monitoringvpnEndpoint ? null # The VPN server and port.
-, ...
-}: let
-
-  enableVpn = monitoringvpnKeyDir != null &&
-              monitoringvpnIPv4 != null &&
-              monitoringvpnEndpoint != null;
-
-  vpnSecrets = if !enableVpn then {} else {
-    "monitoringvpn-secret-key" = {
-      source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key";
-      destination = "/run/keys/monitoringvpn/client.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-    "monitoringvpn-preshared-key" = {
-      source = monitoringvpnKeyDir + "/preshared.key";
-      destination = "/run/keys/monitoringvpn/preshared.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-  };
-
-in rec {
-  deployment = {
-    targetHost = cfg.publicIPv4;
-
-    secrets = {
-      "ristretto-signing-key" = {
-        source = ristrettoSigningKeyPath;
-        destination = "/run/keys/ristretto.signing-key";
-        owner.user = "root";
-        owner.group = "root";
-        permissions = "0400";
-        # Service name here matches the name defined by our tahoe-lafs nixos
-        # module.  It would be nice to not have to hard-code it here.  Can we
-        # extract it from the tahoe-lafs nixos module somehow?
-        action = ["sudo" "systemctl" "restart" "tahoe.storage.service"];
-      };
-    } // vpnSecrets;
-  };
-
-  # Any extra NixOS modules to load on this server.
-  imports = [
-    # Include the results of the hardware scan.
-    hardware
-    # Configure it as a system operated by 100TB.
-    ../../nixos/modules/100tb.nix
-    # Bring in our module for configuring the Tahoe-LAFS service and other
-    # Private Storage-specific things.
-    ../../nixos/modules/private-storage.nix
-    # Connect to the monitoringvpn.
-    ../../nixos/modules/monitoring/vpn/client.nix
-    # Expose base system metrics over the monitoringvpn.
-    ../../nixos/modules/monitoring/exporters/node.nix
-  ];
-
-  # Pass the configuration specific to this host to the 100TB module to be
-  # expanded into a complete system configuration.  See the 100tb module for
-  # handling of this value.
-  #
-  # The module name is quoted because `1` makes `100tb` look an awful lot like
-  # it should be a number.
-  "100tb".config = cfg;
-
-  # Turn on the Private Storage (Tahoe-LAFS) service.
-  services.private-storage = {
-    # Yep.  Turn it on.
-    enable = true;
-    # Get the public IPv4 address from the node configuration.
-    inherit (cfg) publicIPv4;
-    # And the port to operate on is specified via parameter.
-    inherit publicStoragePort;
-    # Give it the Ristretto signing key, too, to support authorization.
-    ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
-    # Assign the configured pass value.
-    inherit passValue;
-    # It gets the users, too.
-    inherit sshUsers;
-  };
-
-  system.stateVersion = stateVersion;
-
-  services.private-storage.monitoring.vpn.client = if !enableVpn then {} else {
-    enable = true;
-    ip = monitoringvpnIPv4;
-    endpoint = monitoringvpnEndpoint;
-    endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub";
-  };
-}
diff --git a/morph/lib/make-testing.nix b/morph/lib/make-testing.nix
deleted file mode 100644
index 3f6e767db5ee734a8ca2314b216d4fa602c01907..0000000000000000000000000000000000000000
--- a/morph/lib/make-testing.nix
+++ /dev/null
@@ -1,80 +0,0 @@
-{ publicIPv4
-, hardware
-, publicStoragePort
-, ristrettoSigningKeyPath
-, passValue
-, sshUsers
-, stateVersion
-, monitoringvpnKeyDir ? null
-, monitoringvpnIPv4 ? null
-, monitoringvpnEndpoint ? null
-, ... }: let
-
-  enableVpn = monitoringvpnKeyDir != null &&
-              monitoringvpnIPv4 != null &&
-              monitoringvpnEndpoint != null;
-
-  vpnSecrets = if !enableVpn then {} else {
-    "monitoringvpn-secret-key" = {
-      source = monitoringvpnKeyDir + "/${monitoringvpnIPv4}.key";
-      destination = "/run/keys/monitoringvpn/client.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-    "monitoringvpn-preshared-key" = {
-      source = monitoringvpnKeyDir + "/preshared.key";
-      destination = "/run/keys/monitoringvpn/preshared.key";
-      owner.user = "root";
-      owner.group = "root";
-      permissions = "0400";
-      action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
-    };
-  };
-
-in rec {
-
-  deployment = {
-    targetHost = publicIPv4;
-
-    secrets = {
-      "ristretto-signing-key" = {
-        source = ristrettoSigningKeyPath;
-        destination = "/run/keys/ristretto.signing-key";
-        owner.user = "root";
-        owner.group = "root";
-        permissions = "0400";
-        # Service name here matches the name defined by our tahoe-lafs nixos
-        # module.  It would be nice to not have to hard-code it here.  Can we
-        # extract it from the tahoe-lafs nixos module somehow?
-        action = ["sudo" "systemctl" "restart" "tahoe.storage.service"];
-      };
-    } // vpnSecrets;
-  };
-
-  imports = [
-    hardware
-    ../../nixos/modules/private-storage.nix
-    ../../nixos/modules/monitoring/vpn/client.nix
-    ../../nixos/modules/monitoring/exporters/node.nix
-  ];
-
-  services.private-storage =
-  { enable = true;
-    inherit publicIPv4;
-    inherit publicStoragePort;
-    ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
-    inherit passValue;
-    inherit sshUsers;
-  };
-
-  system.stateVersion = stateVersion;
-
-  services.private-storage.monitoring.vpn.client = if !enableVpn then {} else {
-    enable = true;
-    ip = monitoringvpnIPv4;
-    endpoint = monitoringvpnEndpoint;
-    endpointPublicKeyFile = monitoringvpnKeyDir + "/server.pub";
-  };
-}
diff --git a/morph/lib/monitoring.nix b/morph/lib/monitoring.nix
new file mode 100644
index 0000000000000000000000000000000000000000..b48820f0941694869fdda06e724ba1ae714b5993
--- /dev/null
+++ b/morph/lib/monitoring.nix
@@ -0,0 +1,37 @@
+# Similar to ``issuer.nix`` but for a "monitoring"-type system.  Holes are
+# filled by ``customize-monitoring.nix``.
+rec {
+  deployment = {
+    secrets = {
+      "monitoringvpn-private-key" = {
+        destination = "/run/keys/monitoringvpn/server.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+      "monitoringvpn-preshared-key" = {
+        destination = "/run/keys/monitoringvpn/preshared.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+    };
+  };
+
+  imports = [
+    ../../nixos/modules/monitoring/vpn/server.nix
+    ../../nixos/modules/monitoring/server/grafana.nix
+    ../../nixos/modules/monitoring/server/prometheus.nix
+    ../../nixos/modules/monitoring/exporters/node.nix
+    # Loki 0.3.0 from Nixpkgs 19.09 is too old and does not work:
+    # ../../nixos/modules/monitoring/server/loki.nix
+  ];
+
+  services.private-storage.monitoring.grafana = {
+    domain = "monitoring.private.storage";
+    prometheusUrl = "http://localhost:9090/";
+    lokiUrl = "http://localhost:3100/";
+  };
+}
diff --git a/morph/lib/storage.nix b/morph/lib/storage.nix
new file mode 100644
index 0000000000000000000000000000000000000000..1cac51b43aa38fb90a535fd34ba53363fc0cdbaa
--- /dev/null
+++ b/morph/lib/storage.nix
@@ -0,0 +1,51 @@
+# Similar to ``issuer.nix`` but for a "storage"-type system.  Holes are filled
+# by ``customize-storage.nix``.
+rec {
+  deployment = {
+    secrets = {
+      "ristretto-signing-key" = {
+        destination = "/run/keys/ristretto.signing-key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        # Service name here matches the name defined by our tahoe-lafs nixos
+        # module.  It would be nice to not have to hard-code it here.  Can we
+        # extract it from the tahoe-lafs nixos module somehow?
+        action = ["sudo" "systemctl" "restart" "tahoe.storage.service"];
+      };
+      "monitoringvpn-secret-key" = {
+        destination = "/run/keys/monitoringvpn/client.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+      "monitoringvpn-preshared-key" = {
+        destination = "/run/keys/monitoringvpn/preshared.key";
+        owner.user = "root";
+        owner.group = "root";
+        permissions = "0400";
+        action = ["sudo" "systemctl" "restart" "wireguard-monitoringvpn.service"];
+      };
+    };
+  };
+
+  # Any extra NixOS modules to load on this server.
+  imports = [
+    # Bring in our module for configuring the Tahoe-LAFS service and other
+    # Private Storage-specific things.
+    ../../nixos/modules/private-storage.nix
+    # Connect to the monitoringvpn.
+    ../../nixos/modules/monitoring/vpn/client.nix
+    # Expose base system metrics over the monitoringvpn.
+    ../../nixos/modules/monitoring/exporters/node.nix
+  ];
+
+  # Turn on the Private Storage (Tahoe-LAFS) service.
+  services.private-storage = {
+    # Yep.  Turn it on.
+    enable = true;
+    # Give it the Ristretto signing key to support authorization.
+    ristrettoSigningKeyPath = deployment.secrets.ristretto-signing-key.destination;
+  };
+}
diff --git a/morph/grid/local/vagrant-guest.nix b/morph/lib/vagrant-guest.nix
similarity index 100%
rename from morph/grid/local/vagrant-guest.nix
rename to morph/lib/vagrant-guest.nix
diff --git a/nixos/modules/private-storage.nix b/nixos/modules/private-storage.nix
index 52720e618973c57b41aade87585c7ab758abff22..38e224709e783b9590de73d728e4d6ca134e5adb 100644
--- a/nixos/modules/private-storage.nix
+++ b/nixos/modules/private-storage.nix
@@ -18,6 +18,12 @@ let
   # NOTE: This is promised by the service privacy policy.  It *may not* be
   # raised without following the process for updating the privacy policy.
   max-incident-age = "29d";
+
+  fqdn = "${
+    assert config.networking.hostName != null; config.networking.hostName
+    }.${
+    assert config.networking.domain != null; config.networking.domain
+    }";
 in
 {
   imports = [
@@ -38,12 +44,13 @@ in
         The package to use for the Tahoe-LAFS daemon.
       '';
     };
-    services.private-storage.publicIPv4 = lib.mkOption
-    { default = "127.0.0.1";
+    services.private-storage.publicAddress = lib.mkOption
+    { default = "${fqdn}";
       type = lib.types.str;
-      example = lib.literalExample "192.0.2.0";
+      example = lib.literalExample "storage.example.invalid";
       description = ''
-        An IPv4 address to advertise for this storage service.
+        A publicly-visible address to use in Tahoe-LAFS advertisements for
+        this storage service.
       '';
     };
     services.private-storage.introducerFURL = lib.mkOption
@@ -63,7 +70,7 @@ in
       '';
     };
     services.private-storage.issuerRootURL = lib.mkOption
-    { default = "https://issuer.privatestorage.io/";
+    { default = "https://issuer.${config.networking.domain}/";
       type = lib.types.str;
       example = lib.literalExample "https://example.invalid/";
       description = ''
@@ -122,7 +129,7 @@ in
           # First, in the syntax which it uses to listen.
           "tub.port" = "tcp:${toString cfg.publicStoragePort}";
           # Second, in the syntax it advertises to in the fURL.
-          "tub.location" = "tcp:${cfg.publicIPv4}:${toString cfg.publicStoragePort}";
+          "tub.location" = "tcp:${cfg.publicAddress}:${toString cfg.publicStoragePort}";
         };
         storage =
         { enabled = true;
diff --git a/nixos/modules/tests/private-storage.nix b/nixos/modules/tests/private-storage.nix
index cbf4c5937ca6780ce9e931d6ceec91c29643fbc3..353abc891fafd1cc988e47a1befa530a012470dc 100644
--- a/nixos/modules/tests/private-storage.nix
+++ b/nixos/modules/tests/private-storage.nix
@@ -115,7 +115,7 @@ in {
         ];
         services.private-storage = {
           enable = true;
-          publicIPv4 = "storage";
+          publicAddress = "storage";
           introducerFURL = introducerFURL;
           issuerRootURL = issuerURL;
           inherit ristrettoSigningKeyPath;