diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 75a903ad3f8637ac5952c0fcb81bbddb72ca3314..ba6c8ad1e99f0ed9dc44ee59a74d4ad8bf102208 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,10 +20,42 @@ unit-tests:
   script:
     - "nix-shell --run 'nix-build nixos/unit-tests.nix' && cat result"
 
+.morph-build: &MORPH_BUILD
+  stage: "test"
+  timeout: "3 hours"
+
+  script:
+    - |
+      # GRID is set in one of the "instantiations" of this job template.
+      nix-shell --run "morph build --show-trace morph/grid/${GRID}/grid.nix"
+
+
+morph-build-localdev:
+  <<: *MORPH_BUILD
+  variables:
+    GRID: "local"
+
+  before_script:
+    - |
+      # The local grid configuration is *almost* complete enough to build.  It
+      # just needs this tweak.
+      sed -i 's/undefined/\"unundefined\"/' morph/grid/${GRID}/public-keys/users.nix
+
+morph-build-testing:
+  <<: *MORPH_BUILD
+  variables:
+    GRID: "testing"
+
+
+morph-build-production:
+  <<: *MORPH_BUILD
+  variables:
+    GRID: "production"
+
+
 vulnerability-scan:
   stage: "test"
   script:
-    - "sed -i 's/undefined/\"unundefined\"/' morph/grid/local/public-keys/users.nix"
     - "ci-tools/vulnerability-scan security-report.json"
     - "ci-tools/count-vulnerabilities <security-report.json"
   artifacts:
@@ -38,24 +70,49 @@ system-tests:
   script:
     - "nix-shell --run 'nix-build nixos/system-tests.nix'"
 
-deploy-to-staging:
+# A template for a job that can update one of the grids.
+.update-grid: &UPDATE_GRID
   stage: "deploy"
+  script:
+    # Announce our intentions.
+    - |
+      echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE"
+      echo "and I am deploying the $CI_COMMIT_BRANCH branch to the $CI_ENVIRONMENT_NAME environment."
+
+    # Copy the deploy key from the environment to a file so we can actually
+    # tell ssh to use it.
+    - |
+      # The environment variable is configured with GitLab using Terraform so
+      # we can retain some bare minimum level of confidentiality.
+      KEY_DIR="$(mktemp -d -p "${XDG_RUNTIME_DIR}-deploy_key")"
+      KEY_PATH="${KEY_DIR}/deploy_key"
+      base64 --decode "${PRIVATESTORAGEIO_STAGING_SSH_DEPLOY_KEY}" > "${KEY_PATH}"
+
+    # Update the deployment
+    - |
+      ./ci-tools/update-grid-servers "${KEY_PATH}" ${name}
+
+    # Remove the key from the filesystem to reduce the chance of unintentional
+    # disclosure.  Overall our handling of this key is still not *particulary*
+    # safe or secure but that's why the key is only authorized to perform a
+    # single very specific operation.
+    - |
+      rm -v "${KEY_PATH}"
+
+# Update the staging deployment - only on a merge to the staging branch.
+update-staging:
+  <<: *UPDATE_GRID
   only:
     - "staging"
   environment:
     name: "staging"
     url: "https://privatestorage-staging.com/"
-  script:
-    - echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE "
-    - echo "and would like to deploy the $CI_COMMIT_BRANCH branch to the $CI_ENVIRONMENT_NAME environment."
 
+# Update the production deployment - only on a merge to the production branch.
 deploy-to-production:
-  stage: "deploy"
+  <<: *UPDATE_GRID
   only:
     - "production"
   environment:
     name: "production"
     url: "https://privatestorage.io/"
-  script:
-    - echo "Hello $GITLAB_USER_LOGIN from $CI_JOB_NAME. I was triggered by $CI_PIPELINE_SOURCE "
-    - echo "and would like to deploy the $CI_COMMIT_BRANCH branch to the $CI_ENVIRONMENT_NAME environment."
diff --git a/ci-tools/update-grid-servers b/ci-tools/update-grid-servers
new file mode 100755
index 0000000000000000000000000000000000000000..61b4cf477ca963d8177ab7d990a9ea88e7f67013
--- /dev/null
+++ b/ci-tools/update-grid-servers
@@ -0,0 +1,56 @@
+#!/usr/bin/env nix-shell
+#!nix-shell -i bash -p jp
+
+#
+# Tell all servers belonging to a certain grid that they should update
+# themselves to the latest configuration associated with that grid.
+#
+
+set -euxo pipefail
+
+# Get the path to the ssh key which authorizes us to deliver this
+# notification.
+DEPLOY_KEY=$1
+shift
+
+# Get the name of the grid to which we're going to deliver notification.  This
+# corresponds to the name of one of the directories in the top-level `morph`
+# directory.
+GRIDNAME=$1
+shift
+
+# Tell one server to update itself.
+update_one_node() {
+    deploy_key=$1
+    shift
+
+    node=$1
+    shift
+
+    ssh -i "${deploy_key}" "deployment@${node}"
+}
+
+# Tell all servers belonging to one grid to update themselves.
+update_grid_nodes() {
+    deploy_key=$1
+    shift
+
+    gridname=$1
+    shift
+
+    # Find the names of all hosts that belong to this grid.  This list includes
+    # one extra string, "network", which is morph configuration stuff and we need
+    # to filter out later.
+    nodes=$(nix eval --json '(builtins.attrNames (import ./morph/${gridname}/grid.nix))')
+
+    # Tell every server in the network to update itself.
+    for node in ${nodes}; do
+	if [ "${node}" = "network" ]; then
+	    # This isn't a server, it's part of the morph configuration.
+	    continue
+	fi
+	update_one_node "${deploy_key}" "${node}"
+    fi
+}
+
+update_grid_nodes "${DEPLOY_KEY}" "${GRIDNAME}"
diff --git a/ci-tools/vulnerability-scan b/ci-tools/vulnerability-scan
index 3162e49511697ed0ea13e0121a67336405ce5225..48bf51e071a398f37565717a22b2066d3f905fbe 100755
--- a/ci-tools/vulnerability-scan
+++ b/ci-tools/vulnerability-scan
@@ -21,7 +21,7 @@ OUTPUT=$1
 [ -e scan-target ] && rm -v scan-target
 nix-shell --run '
 set -x
-if morph_result=$(morph build morph/grid/local/grid.nix 2>&1); then
+if morph_result=$(morph build morph/grid/testing/grid.nix 2>&1); then
   object=$(echo "$morph_result" | tail -n 1)
   ln -s "$object" scan-target
 else
diff --git a/morph/grid/local/Vagrantfile b/morph/grid/local/Vagrantfile
index a2890b8a63304d37002076b5804f2b322207160e..a871cbbe72e410de88a19596ff528391e32ff811 100644
--- a/morph/grid/local/Vagrantfile
+++ b/morph/grid/local/Vagrantfile
@@ -13,6 +13,16 @@ Vagrant.configure("2") do |config|
     config.vm.box = "esselius/nixos"
     config.vm.box_version = "20.09"
     config.vm.box_check_update = false
+
+    # To use the self-updating deployment system you need more memory.  Giving
+    # all of the VMs enough memory for this is rather taxing, though, and the
+    # self-updating deployment system is not particularly useful for local
+    # dev.  But should you want to:
+    #
+    # config.vm.provider "virtualbox" do |v|
+    #   v.memory = 4096
+    # end
+
     config.vm.network "private_network", ip: "192.168.67.21"
     # Add self signed SSL key for zkap-issuer:
     config.vm.provision "file", source: "private-keys/payments-localdev-ssl", destination: "/tmp/payments-localdev-ssl"
diff --git a/morph/grid/local/grid.nix b/morph/grid/local/grid.nix
index bca902f20440e60e7d71162f273dd65d67317545..4e82fa29db5df7da2c9e977f978e67228e527be5 100644
--- a/morph/grid/local/grid.nix
+++ b/morph/grid/local/grid.nix
@@ -12,6 +12,14 @@ let
     privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
+  # Configure deployment management authorization for all systems in the grid.
+  deployment = {
+    services.private-storage.deployment = {
+      authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub";
+      gridName = "local";
+    };
+  };
+
   payments = {
     imports = [
       gridlib.issuer
@@ -19,6 +27,7 @@ let
       (gridlib.customize-issuer (config // {
           monitoringvpnIPv4 = "172.23.23.11";
       }))
+      deployment
     ];
   };
 
@@ -30,6 +39,7 @@ let
         monitoringvpnIPv4 = "172.23.23.12";
         stateVersion = "19.09";
       }))
+      deployment
     ];
   };
 
@@ -41,6 +51,7 @@ let
         monitoringvpnIPv4 = "172.23.23.13";
         stateVersion = "19.09";
       }))
+      deployment
     ];
   };
 
@@ -54,6 +65,7 @@ let
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
+      deployment
     ];
   };
 
diff --git a/morph/grid/local/private-keys/deploy_key b/morph/grid/local/private-keys/deploy_key
new file mode 100644
index 0000000000000000000000000000000000000000..5c8807906eca2a818d2807e2512d03642d038edb
--- /dev/null
+++ b/morph/grid/local/private-keys/deploy_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACADU1IBThyH0blWBG8afIA/h/bmVdUQkFAuAIQgAWE+ewAAAJjsA+8c7APv
+HAAAAAtzc2gtZWQyNTUxOQAAACADU1IBThyH0blWBG8afIA/h/bmVdUQkFAuAIQgAWE+ew
+AAAED6aLiQi/K2qG8sLsvV8Xar9PjJeFxKfb+GUvmseu8TqQNTUgFOHIfRuVYEbxp8gD+H
+9uZV1RCQUC4AhCABYT57AAAADmV4YXJrdW5AYmFyeW9uAQIDBAUGBw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/morph/grid/local/public-keys/deploy_key.pub b/morph/grid/local/public-keys/deploy_key.pub
new file mode 100644
index 0000000000000000000000000000000000000000..15d38cefef61e5f2008efa949ff36245677f8426
--- /dev/null
+++ b/morph/grid/local/public-keys/deploy_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIANTUgFOHIfRuVYEbxp8gD+H9uZV1RCQUC4AhCABYT57 exarkun@baryon
diff --git a/morph/grid/production/grid.nix b/morph/grid/production/grid.nix
index ec21972cfd92bbf191312df3631d403c47bf9068..52a96040b54cbf89d757628f3ca4db8477395351 100644
--- a/morph/grid/production/grid.nix
+++ b/morph/grid/production/grid.nix
@@ -13,6 +13,14 @@ let
     privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
+  # Configure deployment management authorization for all systems in the grid.
+  deployment = {
+    services.private-storage.deployment = {
+      authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub";
+      gridName = "production";
+    };
+  };
+
   payments = {name, ...}: {
     networking.hostName = name;
     imports = [
@@ -21,6 +29,7 @@ let
       (gridlib.customize-issuer (config // {
         monitoringvpnIPv4 = "172.23.23.11";
       }))
+      deployment
     ];
   };
 
@@ -35,6 +44,7 @@ let
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
+      deployment
     ];
   };
 
@@ -60,6 +70,9 @@ let
         monitoringvpnIPv4 = vpnIP;
         inherit stateVersion;
       }))
+
+      # Also configure deployment management authorization
+      deployment
     ];
 
     # And supply configuration for those hardware / network / bootloader
diff --git a/morph/grid/production/public-keys/deploy_key.pub b/morph/grid/production/public-keys/deploy_key.pub
new file mode 100644
index 0000000000000000000000000000000000000000..3d9ea022d26654ba7b18bd3426a464049b58c9ea
--- /dev/null
+++ b/morph/grid/production/public-keys/deploy_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK50RwXncelNB4JAazoXEhCxXbJZ79qWcQMAWeX14H+W exarkun@baryon
diff --git a/morph/grid/testing/grid.nix b/morph/grid/testing/grid.nix
index bc4252fcae49f05e533562bb7c891984d904f095..1534f08c765513da04a263c916f48ef4e859cb76 100644
--- a/morph/grid/testing/grid.nix
+++ b/morph/grid/testing/grid.nix
@@ -13,6 +13,14 @@ let
     privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
+  # Configure deployment management authorization for all systems in the grid.
+  deployment = {
+    services.private-storage.deployment = {
+      authorizedKey = builtins.readFile "${config.publicKeyPath}/deploy_key.pub";
+      gridName = "testing";
+    };
+  };
+
   payments = {name, ...}: {
     networking.hostName = name;
     imports = [
@@ -21,6 +29,7 @@ let
       (gridlib.customize-issuer (config // {
         monitoringvpnIPv4 = "172.23.23.11";
       }))
+      deployment
     ];
   };
 
@@ -33,6 +42,7 @@ let
         monitoringvpnIPv4 = "172.23.23.12";
         stateVersion = "19.03";
       }))
+      deployment
     ];
   };
 
@@ -47,6 +57,7 @@ let
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
+      deployment
     ];
   };
 
diff --git a/morph/grid/testing/public-keys/deploy_key.pub b/morph/grid/testing/public-keys/deploy_key.pub
new file mode 100644
index 0000000000000000000000000000000000000000..2dafd3cce2c83b8e4a32815c37c51ee890ba846c
--- /dev/null
+++ b/morph/grid/testing/public-keys/deploy_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB88qfLdoR5Pq9Us7vOVc6wBWmIDxme9MXYQSxxO+8/X exarkun@baryon
diff --git a/morph/grid/testing/testing001-hardware.nix b/morph/grid/testing/testing001-hardware.nix
index dd2f9733796875bb9d8a549538cba5743a8727a3..b57f193a6155a88be0af804fd6edd9a98043368a 100644
--- a/morph/grid/testing/testing001-hardware.nix
+++ b/morph/grid/testing/testing001-hardware.nix
@@ -13,7 +13,7 @@
   networking.hostId = "10000000";
 
   # Manually created using:
-  #   zpool create -m legacy -o ashift=12 root raidz /dev/disk/by-id/{nvme-nvme.1d0f-766f6c3038623133353836383465643436363430-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3034653531383066303134633436653034-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3062333164633831386366623231373730-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001,nvme-nvme.1d0f-766f6c3061353939623438336661353933636664-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001}
+  #   zpool create -m legacy -o ashift=12 root /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol047fc3ea6bafdcc58
   fileSystems."/storage" = {
     device = "root";
     fsType = "zfs";
diff --git a/morph/lib/issuer.nix b/morph/lib/issuer.nix
index 417ef7965ea0120322995059fcca7a5a9afe2543..51046b436e297cdc5034134e3503556e8030588c 100644
--- a/morph/lib/issuer.nix
+++ b/morph/lib/issuer.nix
@@ -40,6 +40,9 @@ rec {
   };
 
   imports = [
+    # Allow us to remotely trigger updates to this system.
+    ../../nixos/modules/deployment.nix
+
     ../../nixos/modules/issuer.nix
     ../../nixos/modules/monitoring/vpn/client.nix
     ../../nixos/modules/monitoring/exporters/node.nix
diff --git a/morph/lib/monitoring.nix b/morph/lib/monitoring.nix
index b48820f0941694869fdda06e724ba1ae714b5993..fa769d5ebcb32d893310136291064a85c09beee2 100644
--- a/morph/lib/monitoring.nix
+++ b/morph/lib/monitoring.nix
@@ -21,6 +21,9 @@ rec {
   };
 
   imports = [
+    # Allow us to remotely trigger updates to this system.
+    ../../nixos/modules/deployment.nix
+
     ../../nixos/modules/monitoring/vpn/server.nix
     ../../nixos/modules/monitoring/server/grafana.nix
     ../../nixos/modules/monitoring/server/prometheus.nix
diff --git a/morph/lib/storage.nix b/morph/lib/storage.nix
index 1cac51b43aa38fb90a535fd34ba53363fc0cdbaa..ebad3d17e17e0098f6e098d61d7c614fde91b31e 100644
--- a/morph/lib/storage.nix
+++ b/morph/lib/storage.nix
@@ -32,6 +32,8 @@ rec {
 
   # Any extra NixOS modules to load on this server.
   imports = [
+    # Allow us to remotely trigger updates to this system.
+    ../../nixos/modules/deployment.nix
     # Bring in our module for configuring the Tahoe-LAFS service and other
     # Private Storage-specific things.
     ../../nixos/modules/private-storage.nix
diff --git a/nixos/modules/deployment.nix b/nixos/modules/deployment.nix
new file mode 100755
index 0000000000000000000000000000000000000000..2615659519469c9c1c2712382b178bc4fd1c323b
--- /dev/null
+++ b/nixos/modules/deployment.nix
@@ -0,0 +1,130 @@
+# A NixOS module which enables remotely-triggered deployment updates.
+{ config, lib, pkgs, ... }:
+let
+  # A handy alias for our part of the configuration.
+  cfg = config.services.private-storage.deployment;
+
+  # Compute an authorized_keys line that allows the holder of a certain key to
+  # execute a certain command *only*.
+  restrictedKey =
+    { authorizedKey, command, gridName }:
+    # `restrict` means "disable all the things" then `command` means "but
+    # enable running this one command" (the client does not have to supply the
+    # command; if they authenticate, this is the command that will run).
+    # environment lets us pass an environment variable into the process
+    # started by the given command.  It only works because we configured our
+    # sshd to allow this particular variable through.  By passing this value,
+    # we can pin nixpkgs in the executed command to the same version
+    # configured for use here.  It might be better if we just had a channel
+    # the system could be configured with ... but we don't at the moment.
+    "restrict,environment=\"NIXPKGS_FOR_MORPH=${pkgs.path}\",command=\"${command} ${gridName}\" ${authorizedKey}";
+in {
+  options = {
+    services.private-storage.deployment.authorizedKey = lib.mkOption {
+      type = lib.types.str;
+      example = lib.literalExample ''
+        ssh-ed25519 AAAAC3N...
+      '';
+      description = ''
+        The SSH public key to authorize to trigger a deployment update.
+      '';
+    };
+    services.private-storage.deployment.gridName = lib.mkOption {
+      type = lib.types.str;
+      example = lib.literalExample "staging";
+      description = ''
+        The name of the grid configuration to use to update this deployment.
+      '';
+    };
+  };
+
+  config = {
+    # Configure the system to use our binary cache so that deployment updates
+    # only require downloading pre-built software, not building it ourselves.
+    nix = {
+      binaryCachePublicKeys = [
+        "saxtons.private.storage:MplOcEH8G/6mRlhlKkbA8GdeFR3dhCFsSszrspE/ZwY="
+      ];
+      binaryCaches = [
+        "http://saxtons.private.storage"
+      ];
+    };
+
+    services.openssh.extraConfig = ''
+      PermitUserEnvironment=NIXPKGS_FOR_MORPH
+    '';
+
+    # Create a one-time service that will set up an ssh key that allows the
+    # deployment user to authorize as root to perform the system update with
+    # `morph deploy`.
+    systemd.services.authorize-morph-as-root = {
+      enable = true;
+      serviceConfig = {
+        # Tell systemd that the service is a process that runs and then exits.
+        # By being "oneshot" instead of "simple" any dependencies are not
+        # started until after the process exits.  We have no dependencies yet
+        # but if we did it would be more correct for them to wait until we are
+        # done.
+        #
+        # It is not clear that "oneshot" means "run once" though (maybe it
+        # does, I can't tell) so the script is robust in the face of repeated
+        # runs even though it should only ever need to be run once.
+        Type = "oneshot";
+      };
+      wantedBy = [
+        # Run this to reach the multi-user target, a good target that is
+        # reached in the typical course of system startup.
+        "multi-user.target"
+      ];
+      # Here's the program to run for this unit.  It's a shell script that
+      # creates an ssh key that authorized root access via ssh and give it to
+      # the deployment user.  If such a key appears to exist already, do
+      # nothing.
+      script = ''
+        KEY=~deployment/.ssh/morph_key
+        TMP="$KEY"_tmp
+        if [ ! -e "$KEY" ]; then
+          mkdir -p ~deployment/.ssh ~root/.ssh
+          chown deployment ~deployment/.ssh
+          ${pkgs.openssh}/bin/ssh-keygen -f "$TMP"
+          cat "$TMP".pub >> ~root/.ssh/authorized_keys
+          mv "$TMP".pub "$KEY".pub
+          mv "$TMP" "$KEY"
+          chown deployment "$KEY"
+        fi
+      '';
+    };
+
+    # Raise the hard-limit on the size of $XDG_RUNTIME_DIR (ie
+    # /run/user/<uid>).  The default of 10% is too small on some systems for
+    # the temporary state morph creates to do the self-update.
+    services.logind.extraConfig = ''
+      RuntimeDirectorySize=50%
+    '';
+
+    # Configure the deployment user.
+    users.users.deployment = {
+      # Without some shell no login is possible at all, even to execute our
+      # single non-restricted command.
+      useDefaultShell = true;
+
+      # Without a home directory, lots of tools break.
+      createHome = true;
+      home = "/home/deployment";
+
+      packages = [
+        # update-deployment dependencies
+        pkgs.morph
+        pkgs.git
+      ];
+
+      # Authorize the supplied key to run the deployment update command.
+      openssh.authorizedKeys.keys = [
+        (restrictedKey {
+          inherit (cfg) authorizedKey gridName;
+          command = ./update-deployment;
+        })
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/ssh.nix b/nixos/modules/ssh.nix
index 667bdd26215b4e0978781244741dd4c5313cefbd..3e90528322c153d6b96679af5d914c4e753b49bf 100644
--- a/nixos/modules/ssh.nix
+++ b/nixos/modules/ssh.nix
@@ -40,12 +40,6 @@
         # Agent forwarding is fraught.  It can be used by an attacker to
         # leverage one compromised system into more.  Discourage its use.
         AllowAgentForwarding no
-
-        # Only allow authentication as one of the configured users, not random
-        # other (often system-managed) users.  Possibly this is also
-        # superfluous!  NixOS system users have nologin as their shell ... so they
-        # cannot log in anyway.
-        AllowUsers ${builtins.concatStringsSep " " (builtins.attrNames cfg.sshUsers)}
       '';
     };
 
diff --git a/nixos/modules/update-deployment b/nixos/modules/update-deployment
new file mode 100755
index 0000000000000000000000000000000000000000..19599a4aa7ac2ec7bbb7e160ec1b37a8493b4a62
--- /dev/null
+++ b/nixos/modules/update-deployment
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+
+set -euxo pipefail
+
+# Accept the name of the grid this system is part of as a parameter.  This
+# lets us pick the correct morph grid source file later on.
+GRIDNAME=$1
+shift
+
+# Determine the right branch name to use for the particular grid we've been
+# told we belong to.  The grid name is a parameter to this script we can
+# re-use it across all of our grids.  See deployment.nix for the ssh
+# configuration that controls what value is actually passed when an update is
+# triggered.
+case "${GRIDNAME}" in
+    "local")
+	BRANCH="develop"
+	;;
+
+    "testing")
+	BRANCH="staging"
+	;;
+
+    "production")
+	BRANCH="production"
+	;;
+
+    *)
+	echo "Unknown grid: ${GRIDNAME}"
+	exit 1
+esac
+
+# This is where we will maintain a checkout of PrivateStorageio for morph to
+# use to compute the desired state.
+CHECKOUT="${HOME}/PrivateStorageio"
+
+# This is the address of the git remote where we can get the latest
+# PrivateStorageio.
+REPO="https://whetstone.privatestorage.io/privatestorage/PrivateStorageio.git"
+
+if [ -e "${CHECKOUT}" ]; then
+    # It exists already so just make sure it contains the latest changes from
+    # the canonical repository.
+    git -C "${CHECKOUT}" fetch
+else
+    # It doesn't exist so clone it.
+    git clone "${REPO}" "${CHECKOUT}"
+fi
+
+# Get us to a pristine checkout of the right branch.
+git -C "${CHECKOUT}" reset --hard "origin/${BRANCH}"
+
+# If we happen to be on the local grid then fix the undefined key.
+if [ "${GRIDNAME}" = "local" ]; then
+    KEY="$(cat /etc/ssh/authorized_keys.d/vagrant)"
+    sed -i "s_undefined_\"${KEY}\"_" "${CHECKOUT}"/morph/grid/${GRIDNAME}/public-keys/users.nix
+fi
+
+# Compute a log message explaining what we're doing.
+LOG_MESSAGE="$(date --iso-8601=seconds) $(git -C "${CHECKOUT}" rev-parse HEAD)"
+
+# Make sure we use the right credentials and ask for the right account when
+# morph makes the connection.  morph's deployment target for each host is the
+# full domain name (even though the host is only named with the unqualified
+# hostname in the morph grid definition) so compute an ssh config section that
+# matches that.  Regardless, point this effort at localhost because we *know*
+# it's just us we want to update.
+cat > ~/.ssh/config <<EOF
+Host $(hostname).$(domainname)
+  HostName 127.0.0.1
+  IdentityFile ~/.ssh/morph_key
+  User root
+EOF
+
+# Make sure known_hosts has the host key in it.
+ssh -o StrictHostKeyChecking=no "$(hostname).$(domainname)" ":"
+
+# Set nixpkgs to our preferred version for the morph build.  The NIX_PATH
+# environment variable itself receives special treatment by some parts of the
+# system (especially those parts leading up to the execution of this script)
+# so we pass the desired information through a different variable and then
+# shuffle it into the right place here, just before it is needed.
+export NIX_PATH="nixpkgs=$NIXPKGS_FOR_MORPH"
+
+# Attempt to update just this host.  Choose the morph grid definition matching
+# the grid we belong to and limit the morph deployment update to the host
+# matching our name.  morph uses just the bare hostname without the domain
+# part.
+if morph deploy "${CHECKOUT}"/morph/grid/"${GRIDNAME}"/grid.nix switch --on "$(hostname)"; then
+    # The deployment succeeded.  Record success along with context we pre-computed.
+    echo "${LOG_MESSAGE} OK" >> ${HOME}/updates.txt
+else
+    # Oops.  Not so fortunate.  Record failure.
+    echo "${LOG_MESSAGE} FAIL" >> ${HOME}/updates.txt
+fi
diff --git a/nixpkgs-ps.json b/nixpkgs-ps.json
index c5b01313cacdfc86f06e3baa82fb1d605c35da56..97449535de722fdaf89d8551c6bd6d124e3818c7 100644
--- a/nixpkgs-ps.json
+++ b/nixpkgs-ps.json
@@ -1,4 +1,4 @@
-{ "name": "nixpkgs-ps"
-, "url": "https://github.com/PrivateStorageio/nixpkgs/archive/788cc5806d46b89013ddd59db589b748bc20435e.tar.gz"
-, "sha256": "1mjznn4i4524gl5aiapjpy2jzpac1fzp7jvnkamrh9090ndalhar"
+{ "name": "nixpkgs"
+, "url": "https://github.com/PrivateStorageio/nixpkgs/archive/a5cbaadd9676e8c568061e92bbf5ad6a5d884ded.tar.gz"
+, "sha256": "0q5zknsp0qb25ag9zr9bw1ap7pb3f76bxsw81ahxkzj4z5dw6k2f"
 }