diff --git a/default.nix b/default.nix
index 6441675a243e22e6154267c656652c8d8575940e..8b605281715be3f74c375a6b5532a4a87a6b4993 100644
--- a/default.nix
+++ b/default.nix
@@ -1,4 +1,4 @@
-{ pkgs ? import ./nixpkgs-2105.nix { } }:
+{ pkgs ? import ./nixpkgs.nix { } }:
 {
   # Render the project documentation source to some presentation format (ie,
   # html) with Sphinx.
diff --git a/morph/lib/default.nix b/morph/lib/default.nix
index 88c83bc2211da2e00e69f95d7d2110d3ee636cc3..c43fa6ea1ebc6b3159f3f2f6872c23f0da62b776 100644
--- a/morph/lib/default.nix
+++ b/morph/lib/default.nix
@@ -17,7 +17,7 @@
   # installed, as well as the NixOS module set that is used.
   # This is intended to be used in a grid definition like:
   #     network = { ... ; inherit (gridlib) pkgs; ... }
-  pkgs = import ../../nixpkgs-2105.nix {
+  pkgs = import ../../nixpkgs.nix {
     # Ensure that configuration of the system where this runs
     # doesn't leak into what we build.
     # See https://github.com/NixOS/nixpkgs/issues/62513
diff --git a/nixos/modules/tahoe.nix b/nixos/modules/tahoe.nix
index e0b6eb4d8be3c5359de1d391c42b2ba83f7a1ba4..44c381e6b6dfa6039d1dd6a49d44f1afaf51ab10 100644
--- a/nixos/modules/tahoe.nix
+++ b/nixos/modules/tahoe.nix
@@ -156,6 +156,10 @@ in
           nameValuePair "tahoe.introducer-${node}" {
             description = "Tahoe node user for introducer ${node}";
             isSystemUser = true;
+            group = "tahoe.introducer-${node}";
+          });
+        users.groups = flip mapAttrs' cfg.introducers (node: _:
+            nameValuePair "tahoe.introducer-${node}" {
           });
       })
       (mkIf (cfg.nodes != {}) {
@@ -287,6 +291,10 @@ in
           nameValuePair "tahoe.${node}" {
             description = "Tahoe node user for node ${node}";
             isSystemUser = true;
+            group = "tahoe.${node}";
+          });
+        users.groups = flip mapAttrs' cfg.introducers (node: _:
+            nameValuePair "tahoe.${node}" {
           });
       })
     ];
diff --git a/nixos/modules/update-deployment b/nixos/modules/update-deployment
index 1c8960588f418e57eeaadb7ad29db4285369cbdd..a8efffa062ad8f8dc6b6dc22827e4f0087b4d618 100755
--- a/nixos/modules/update-deployment
+++ b/nixos/modules/update-deployment
@@ -72,14 +72,14 @@ EOF
 ssh -o StrictHostKeyChecking=no "$(hostname).$(domainname)" ":"
 
 # Set nixpkgs to our preferred version for the morph build.  Annoyingly, we
-# can't just use nixpkgs-2105.nix as our nixpkgs because some code (in morph,
+# can't just use nixpkgs.nix as our nixpkgs because some code (in morph,
 # at least) wants <nixpkgs> to be a fully-resolved path to a nixpkgs tree.
 # For example, morph evaluated `import <nixpkgs/lib>` which would turn into
-# something like `import nixpkgs-2105.nix/lib` which is nonsense.
+# something like `import nixpkgs.nix/lib` which is nonsense.
 #
 # So instead, import our nixpkgs which forces it to be instantiated in the
 # store, then ask for its path, then set NIX_PATH to that.
-export NIX_PATH="nixpkgs=$(nix eval "(import ${CHECKOUT}/nixpkgs-2105.nix { }).path")"
+export NIX_PATH="nixpkgs=$(nix eval "(import ${CHECKOUT}/nixpkgs.nix { }).path")"
 
 # 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
diff --git a/nixos/pkgs/privatestorage/default.nix b/nixos/pkgs/privatestorage/default.nix
index bd487af32941f6db920ea2d43ec89e9eded38201..3bbbd3dbcf0b974e6e1997e20773cddbd9ea59c0 100644
--- a/nixos/pkgs/privatestorage/default.nix
+++ b/nixos/pkgs/privatestorage/default.nix
@@ -2,7 +2,7 @@
 let
   repo-data = lib.importJSON ./repo.json;
   repo = fetchFromGitHub (builtins.removeAttrs repo-data [ "branch" ]);
-  privatestorage = callPackage repo {};
+  privatestorage = callPackage repo { python = "python39"; };
 in
   privatestorage.privatestorage
 
diff --git a/nixos/pkgs/privatestorage/repo.json b/nixos/pkgs/privatestorage/repo.json
index 4fc6b9fbf1254c81fd03d0cf3646fce7d97841bc..20fb749ffdbdcdb86e2af21c63ac1c756c6157a1 100644
--- a/nixos/pkgs/privatestorage/repo.json
+++ b/nixos/pkgs/privatestorage/repo.json
@@ -2,7 +2,7 @@
   "owner": "PrivateStorageio",
   "branch": "main",
   "repo": "ZKAPAuthorizer",
-  "rev": "94cd16ce2d3ffb0aa18e7dfd58765eebda372117",
+  "rev": "a263d171aa20d6b34926a6af51b849cd127b7190",
   "outputHashAlgo": "sha512",
-  "outputHash": "1vgq438nhhzlylha2yysl7mf1ddafghv9yap6y8fi4b8djdpy58k5xx9hj7ws08hb0yhrly2axv003pxm5gy1zyaghfnwwp4491cqfg"
-}
\ No newline at end of file
+  "outputHash": "0sxxhmrfbag5ksis0abvwnlqzfiqlwdyfnhav2h7hpn62r81l7k2gk4jhs3blw8r6bv6di978lx6rv56r1vmjnpmlyx2l33afzf9bf3"
+}
diff --git a/nixos/pkgs/zkap-spending-service/repo.json b/nixos/pkgs/zkap-spending-service/repo.json
index 69f7a30053de661f2c7829384e9496e49077cfd9..eafc2e2e96592926a40535ee370db9bda9f10cc4 100644
--- a/nixos/pkgs/zkap-spending-service/repo.json
+++ b/nixos/pkgs/zkap-spending-service/repo.json
@@ -1,9 +1,9 @@
-{
-  "owner": "privatestorage",
-  "repo": "zkap-spending-service",
-  "rev": "cbf7509f429ffd6e6cf37a73e4ff84a9c5ce1141",
-  "branch": "main",
-  "domain": "whetstone.privatestorage.io",
-  "outputHash": "04g7pcykc2525cg3z7wg5834s7vqn82xaqjvf52l6dnxv3mb9xr93kk505dvxcwhgfbqpim5i479s9kqd8gi7q3lq5wn5fq7rf7lkrj",
-  "outputHashAlgo": "sha512"
-}
+{
+  "owner": "privatestorage",
+  "repo": "zkap-spending-service",
+  "rev": "66fd395268b466d4c7bb0a740fb758a5acccd1c4",
+  "branch": "main",
+  "domain": "whetstone.privatestorage.io",
+  "outputHash": "1nryvsccncrka25kzrwqkif4x68ib0cs2vbw1ngfmzw86gjgqx01a7acgspmrpfs62p4q8zw0f2ynl8jr3ygyypjrl8v7w8g49y0y0y",
+  "outputHashAlgo": "sha512"
+}
diff --git a/nixpkgs-2105.json b/nixpkgs-2105.json
deleted file mode 100644
index f7d74ca46466a3c97e9a21318dd23d137d37b13b..0000000000000000000000000000000000000000
--- a/nixpkgs-2105.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "name": "release2105",
-  "url": "https://releases.nixos.org/nixos/21.05/nixos-21.05.4717.df123677560/nixexprs.tar.xz",
-  "sha256": "02zkhiwl3lwhk9fkmcbcfr927w135xdrgp6z7g804symbd1jcwal"
-}
\ No newline at end of file
diff --git a/nixpkgs-2105.nix b/nixpkgs-2105.nix
index 536d913b89ba6a57d8d683381ea1c8f40e026b4f..fbd7ca592bfd4e9d1b941f437758f8da1b8bcb5a 100644
--- a/nixpkgs-2105.nix
+++ b/nixpkgs-2105.nix
@@ -1 +1,6 @@
-import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs-2105.json)))
+# This actually imports nixos-21.11 but we need to keep this file around so that
+# upgrades work, as the on-node deployment script expects this file in the checkout.
+# See https://whetstone.privatestorage.io/privatestorage/PrivateStorageio/-/merge_requests/222#note_18600
+# This file can be removed once all nodes have been updated to point to the new file.
+
+import ./nixpkgs.nix
diff --git a/nixpkgs.json b/nixpkgs.json
new file mode 100644
index 0000000000000000000000000000000000000000..3066c2260d204d86ef3b50ef3deac3619cd14145
--- /dev/null
+++ b/nixpkgs.json
@@ -0,0 +1,5 @@
+{
+  "name": "source",
+  "url": "https://releases.nixos.org/nixos/21.11/nixos-21.11.335130.386234e2a61/nixexprs.tar.xz",
+  "sha256": "05lw8w4mbpzxsam09s22q7fm21ayhzh9w8g74vhhhmigr18ggxc7"
+}
diff --git a/nixpkgs.nix b/nixpkgs.nix
new file mode 100644
index 0000000000000000000000000000000000000000..a49c447874e45bea0804185636468568f5bd5035
--- /dev/null
+++ b/nixpkgs.nix
@@ -0,0 +1 @@
+import (builtins.fetchTarball (builtins.fromJSON (builtins.readFile ./nixpkgs.json)))
diff --git a/shell.nix b/shell.nix
index a5741377eec5ebd4b8862a0ea47e15edfdac2731..b8be3a3a6088a987468329ad29919a6957313c6a 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,7 +1,7 @@
 let
-  release2105 = import ./nixpkgs-2105.nix { };
+  pinned-pkgs = import ./nixpkgs.nix { };
 in
-{ pkgs ? release2105, lib ? pkgs.lib, python ? pkgs.python3 }:
+{ pkgs ? pinned-pkgs, lib ? pkgs.lib, python ? pkgs.python3 }:
 let
   tools = pkgs.callPackage ./tools {};
 in
@@ -10,7 +10,7 @@ pkgs.mkShell {
   # first adds that path to the store, and then interpolates the store path
   # into the string.  We use `builtins.toString` to convert the path to a
   # string without copying it to the store before interpolating. Either the
-  # path is already in the store (e.g. when `pkgs` is `release2105`) so we
+  # path is already in the store (e.g. when `pkgs` is `pinned-pkgs`) so we
   # avoid making a second copy with a longer name, or the user passed in local
   # path (e.g. a checkout of nixpkgs) and we point at it directly, rather than
   # a snapshot of it.
diff --git a/tools/update-nixpkgs b/tools/update-nixpkgs
index 09c823b0a419b5937d4953337b94a26c4b502e32..3c6832c95cee09632318f7a4ef4efc7099e317e1 100755
--- a/tools/update-nixpkgs
+++ b/tools/update-nixpkgs
@@ -10,7 +10,7 @@ from ps_tools import get_url_hash
 # We pass this to builtins.fetchTarball which only supports sha256
 HASH_TYPE = "sha256"
 
-DEFAULT_CHANNEL = "nixos-21.05"
+DEFAULT_CHANNEL = "nixos-21.11"
 CHANNEL_URL_TEMPLATE = "https://channels.nixos.org/{channel}/nixexprs.tar.xz"
 
 
@@ -37,7 +37,7 @@ def main():
         "repo_file",
         metavar="repo-file",
         nargs="?",
-        default=Path(__file__).parent.with_name("nixpkgs-2105.json"),
+        default=Path(__file__).parent.with_name("nixpkgs.json"),
         type=Path,
         help="JSON file with pinned configuration.",
     )