diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 17ee05953bcaa70cfb69c036d3710944b3a0ed3e..75a903ad3f8637ac5952c0fcb81bbddb72ca3314 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,7 +23,7 @@ unit-tests:
 vulnerability-scan:
   stage: "test"
   script:
-    - "sed -i 's/undefined/\"unundefined\"/' morph/grid/local/secrets/users.nix"
+    - "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:
diff --git a/docs/source/ops/generating-keys.rst b/docs/source/ops/generating-keys.rst
index 47a1f4e91a876ac1919252c099654886f0bd128a..c2f7028f2bc263c9e5bac40f78ca0adfb4861415 100644
--- a/docs/source/ops/generating-keys.rst
+++ b/docs/source/ops/generating-keys.rst
@@ -1,9 +1,10 @@
 Generating keys
 ===============
 
-There's an example ``secrets`` repo in ``morph/grid/local/secrets``.
+There are example ``public-keys`` and ``private-keys`` repos in ``morph/grid/local/``.
 ``<grid>/config.json`` has the paths for the key files for the respective grid.
-Create a symlink named ``secrets`` to your secret key repository for the deployment you are working on.
+Create a symlink ``private-keys`` to your secret key repositories for the deployment you are working on.
+Create a directory named ``public-keys`` containing the corresponding public keys for the deployment.
 
 
 Stripe
@@ -55,22 +56,6 @@ Move the three .pem files into the payment's server ``/var/lib/letsencrypt/live/
 Monitoring VPN
 ``````````````
 
-Create Wireguard VPN key pairs in ``secrets/monitoringvpn/`` or where you have them.
-
-``tools/create-vpn-keys.sh`` holds a script to rotate all VPN keys at once::
+Create all of the Wireguard VPN keys for a grid::
 
   ./tools/create-vpn-keys.sh morph/grid/testing/grid.nix
-
-Or do it manually::
-
-  cd secrets/monitoringvpn
-  for i in 1 11 12 13 ; do
-    wg genkey | tee 172.23.23.${i}.key | wg pubkey > 172.23.23.${i}.pub
-  done
-
-  ln -s 172.23.23.1.key server.key
-  ln -s 172.23.23.1.pub server.pub
-
-And a shared VPN key for "post-quantum resistance"::
-
-  wg genpsk > preshared.key
diff --git a/morph/README.rst b/morph/README.rst
index 12472518ad8e061764d6812694c306e87553c843..f43926119b6609fcd0c746dcc1113822b5244e5a 100644
--- a/morph/README.rst
+++ b/morph/README.rst
@@ -42,8 +42,8 @@ grid
 
 Specific grid definitions live in subdirectories beneath this directory.
 
-secrets
-~~~~~~~
+private-keys
+~~~~~~~~~~~~
 
 This must be created and populated before the grid can be built or deployed.
 
@@ -55,10 +55,18 @@ This path is **ignored** by git.
 The intended workflow is that the secrets will be maintained on secure storage and a symlink to the correct location created here.
 This keeps the secrets themselves out of the git working tree as an extra protection against unintentionally committing them.
 
-An exception is the ``secrets`` directory in the ``local`` morph grid:
+An exception is the ``private-keys`` directory in the ``local`` morph grid:
 That directory is fully populated, provided as an example, and mostly: not very secret.
 Do not deploy these keys to machines reachable via the internet.
 
+public-keys
+~~~~~~~~~~~
+
+This must be created and populated before the grid can be built or deployed.
+
+This directory contains any public key material necessary for operation of the grid.
+This includes the public keys corresponding to any private keys held in ``private-keys``.
+
 config.json
 ~~~~~~~~~~~
 
diff --git a/morph/grid/local/README.rst b/morph/grid/local/README.rst
index f899c4975bab131e969acc79e8b39dee139b1554..d30d8766a4ef5a8db228ef38374330734e69cba7 100644
--- a/morph/grid/local/README.rst
+++ b/morph/grid/local/README.rst
@@ -37,7 +37,7 @@ Use the local development environment
 
 6. Add your SSH key to ``users.nix`` so you'll be able to log in after deploying the new configuration::
 
-    $EDITOR secrets/users.nix
+    $EDITOR public-keys/users.nix
 
 7. Then, build and deploy our software to the Vagrant VMs::
 
diff --git a/morph/grid/local/Vagrantfile b/morph/grid/local/Vagrantfile
index fd374a2cc26da63262a16fdf462e235fc5523fd1..a2890b8a63304d37002076b5804f2b322207160e 100644
--- a/morph/grid/local/Vagrantfile
+++ b/morph/grid/local/Vagrantfile
@@ -15,7 +15,7 @@ Vagrant.configure("2") do |config|
     config.vm.box_check_update = false
     config.vm.network "private_network", ip: "192.168.67.21"
     # Add self signed SSL key for zkap-issuer:
-    config.vm.provision "file", source: "secrets/payments-localdev-ssl", destination: "/tmp/payments-localdev-ssl"
+    config.vm.provision "file", source: "private-keys/payments-localdev-ssl", destination: "/tmp/payments-localdev-ssl"
     config.vm.provision "shell", inline: "sudo mkdir -p /var/lib/letsencrypt/live/payments.localdev/"
     config.vm.provision "shell", inline: "sudo mv /tmp/payments-localdev-ssl/* /var/lib/letsencrypt/live/payments.localdev/"
   end
diff --git a/morph/grid/local/config.json b/morph/grid/local/config.json
index 3d377cc0e1ebbdec0dff421c806c901e2e5ce06d..9a929d2cf4613874379fdcc7a52f241c10f63f18 100644
--- a/morph/grid/local/config.json
+++ b/morph/grid/local/config.json
@@ -1,8 +1,7 @@
 { "domain": "localdev"
 , "publicStoragePort": 8898
-, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
-, "stripeSecretKeyPath": "./secrets/stripe.secret"
-, "monitoringvpnKeyDir": "./secrets/monitoringvpn"
+, "publicKeyPath": "./public-keys"
+, "privateKeyPath": "./private-keys"
 , "monitoringvpnEndpoint": "192.168.67.24:51820"
 , "passValue": 1000000
 , "issuerDomains": ["payments.localdev"]
diff --git a/morph/grid/local/grid.nix b/morph/grid/local/grid.nix
index 5345a16198e79dd8c91c8566fb62480ce5cea51a..55b7587c5bbfb6169b8cc1fda506a8d6c07d4dcd 100644
--- a/morph/grid/local/grid.nix
+++ b/morph/grid/local/grid.nix
@@ -4,10 +4,12 @@ let
   gridlib = import ../../lib;
   rawConfig = pkgs.lib.trivial.importJSON ./config.json;
   config = rawConfig // {
-    sshUsers = import ./secrets/users.nix;
+    sshUsers = import ./public-keys/users.nix;
 
-    # Get absolute vpn key directory path, as a string:
-    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+    # Convert relative paths to absolute so library code can resolve names
+    # correctly.
+    publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}";
+    privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
   payments = {
@@ -48,7 +50,7 @@ let
       (gridlib.hardware-virtual ({ publicIPv4 = "192.168.67.24"; }))
       (gridlib.customize-monitoring {
         inherit hostsMap vpnClientIPs nodeExporterTargets;
-        inherit (config) domain monitoringvpnKeyDir;
+        inherit (config) domain publicKeyPath privateKeyPath;
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.11.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.11.key
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.11.key
rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.11.key
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.12.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.12.key
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.12.key
rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.12.key
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.13.key b/morph/grid/local/private-keys/monitoringvpn/172.23.23.13.key
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.13.key
rename to morph/grid/local/private-keys/monitoringvpn/172.23.23.13.key
diff --git a/morph/grid/local/secrets/monitoringvpn/preshared.key b/morph/grid/local/private-keys/monitoringvpn/preshared.key
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/preshared.key
rename to morph/grid/local/private-keys/monitoringvpn/preshared.key
diff --git a/morph/grid/local/secrets/monitoringvpn/server.key b/morph/grid/local/private-keys/monitoringvpn/server.key
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/server.key
rename to morph/grid/local/private-keys/monitoringvpn/server.key
diff --git a/morph/grid/local/secrets/payments-localdev-ssl/cert.pem b/morph/grid/local/private-keys/payments-localdev-ssl/cert.pem
similarity index 100%
rename from morph/grid/local/secrets/payments-localdev-ssl/cert.pem
rename to morph/grid/local/private-keys/payments-localdev-ssl/cert.pem
diff --git a/morph/grid/local/secrets/payments-localdev-ssl/chain.pem b/morph/grid/local/private-keys/payments-localdev-ssl/chain.pem
similarity index 100%
rename from morph/grid/local/secrets/payments-localdev-ssl/chain.pem
rename to morph/grid/local/private-keys/payments-localdev-ssl/chain.pem
diff --git a/morph/grid/local/secrets/payments-localdev-ssl/privkey.pem b/morph/grid/local/private-keys/payments-localdev-ssl/privkey.pem
similarity index 100%
rename from morph/grid/local/secrets/payments-localdev-ssl/privkey.pem
rename to morph/grid/local/private-keys/payments-localdev-ssl/privkey.pem
diff --git a/morph/grid/local/secrets/ristretto.signing-key b/morph/grid/local/private-keys/ristretto.signing-key
similarity index 100%
rename from morph/grid/local/secrets/ristretto.signing-key
rename to morph/grid/local/private-keys/ristretto.signing-key
diff --git a/morph/grid/local/secrets/stripe.secret b/morph/grid/local/private-keys/stripe.secret
similarity index 100%
rename from morph/grid/local/secrets/stripe.secret
rename to morph/grid/local/private-keys/stripe.secret
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.11.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.11.pub
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.11.pub
rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.11.pub
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.12.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.12.pub
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.12.pub
rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.12.pub
diff --git a/morph/grid/local/secrets/monitoringvpn/172.23.23.13.pub b/morph/grid/local/public-keys/monitoringvpn/172.23.23.13.pub
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/172.23.23.13.pub
rename to morph/grid/local/public-keys/monitoringvpn/172.23.23.13.pub
diff --git a/morph/grid/local/secrets/monitoringvpn/server.pub b/morph/grid/local/public-keys/monitoringvpn/server.pub
similarity index 100%
rename from morph/grid/local/secrets/monitoringvpn/server.pub
rename to morph/grid/local/public-keys/monitoringvpn/server.pub
diff --git a/morph/grid/local/secrets/users.nix b/morph/grid/local/public-keys/users.nix
similarity index 83%
rename from morph/grid/local/secrets/users.nix
rename to morph/grid/local/public-keys/users.nix
index 93a8b660c78fa12b1e20c6d560f78efb1b5684c7..412077c0d5d6d98024036e369dfa552604f2dc57 100644
--- a/morph/grid/local/secrets/users.nix
+++ b/morph/grid/local/public-keys/users.nix
@@ -1,4 +1,4 @@
-# Add your public key. Example: 
+# Add your public key. Example:
 # let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHx7wJQNqKn8jOC4AxySRL2UxidNp7uIK9ad3pMb1ifF flo@fs-la";
 let key = undefined;
 in { "root" = key; "vagrant" = key; }
diff --git a/morph/grid/production/.gitignore b/morph/grid/production/.gitignore
index db2fc0de62d01d6d7eec83f8f3e8c3b13b20392a..e3b6111c86090b06c38b9e5afd1fcd16838ddf47 100644
--- a/morph/grid/production/.gitignore
+++ b/morph/grid/production/.gitignore
@@ -1 +1 @@
-secrets
+private-keys
diff --git a/morph/grid/production/config.json b/morph/grid/production/config.json
index 21e080d587ae2713a73f756b2b7e078d843b2a95..092e4dff7b4c026c816afdd85b2a454089204141 100644
--- a/morph/grid/production/config.json
+++ b/morph/grid/production/config.json
@@ -1,8 +1,7 @@
 { "domain": "private.storage"
 , "publicStoragePort": 8898
-, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
-, "stripeSecretKeyPath": "./secrets/stripe.secret"
-, "monitoringvpnKeyDir": "./secrets/monitoringvpn"
+, "privateKeyPath": "./private-keys"
+, "publicKeyPath": "./public-keys"
 , "monitoringvpnEndpoint": "monitoring.private.storage:51820"
 , "passValue": 1000000
 , "issuerDomains": [
diff --git a/morph/grid/production/grid.nix b/morph/grid/production/grid.nix
index ae51174b4f15a72ca0c1d1798b067ecb1db64bb3..fb680338a08b0006e166b13066199d20f6836e44 100644
--- a/morph/grid/production/grid.nix
+++ b/morph/grid/production/grid.nix
@@ -5,10 +5,12 @@ let
   gridlib = import ../../lib;
   rawConfig = pkgs.lib.trivial.importJSON ./config.json;
   config = rawConfig // {
-    sshUsers = import ./secrets/users.nix;
+    sshUsers = import ./public-keys/users.nix;
 
-    # Get absolute vpn key directory path, as a string:
-    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+    # Convert relative paths to absolute so library code can resolve names
+    # correctly.
+    publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}";
+    privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
   payments = {
@@ -27,7 +29,7 @@ let
       gridlib.hardware-aws
       (gridlib.customize-monitoring {
         inherit hostsMap vpnClientIPs nodeExporterTargets;
-        inherit (config) domain monitoringvpnKeyDir;
+        inherit (config) domain publicKeyPath privateKeyPath;
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub
new file mode 100644
index 0000000000000000000000000000000000000000..79248b8afc2e5d58ce0e2829c34266d377e2ffa5
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.1.pub
@@ -0,0 +1 @@
+f4PF38t1ZRneFCV+12irDbMuG81WK6jiH0Ba+P+XtXM=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub
new file mode 100644
index 0000000000000000000000000000000000000000..c085058430258c7c5a4c3fe6a2a2e87ebce56543
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.11.pub
@@ -0,0 +1 @@
+yBdp154+SjyjTJM6ag1mbdnXORWrv/mJ01NJdkEe9VY=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub
new file mode 100644
index 0000000000000000000000000000000000000000..5c6351937d9d746d6c1e0ebca3439dc49a1f4574
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.21.pub
@@ -0,0 +1 @@
+G0//oetsCGa75x8rLsg98c9GT9a0ncf1yG9w2+5JV0M=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub
new file mode 100644
index 0000000000000000000000000000000000000000..1ec8fbe3f88c3d126b1c7a19a3c80ff55cedbe0c
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.22.pub
@@ -0,0 +1 @@
+Zq4OsMOTJ2NsVi00hB0x20mMqvoCrDUfleoI5rzIeEc=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub
new file mode 100644
index 0000000000000000000000000000000000000000..a5ce0ad526a0a0b949488304c05f0cc055695634
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.23.pub
@@ -0,0 +1 @@
+9ThSUgSNrykQEULj70QQyjlvtvGTmMPqsRMz8hc9xHA=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub
new file mode 100644
index 0000000000000000000000000000000000000000..c54c728a732d7ca083f9f5ac9e1cb7d82475101f
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.24.pub
@@ -0,0 +1 @@
+fPUnFOzBZRJDBdSR6iS5AaC40KKy/2REiM16hx+woxk=
diff --git a/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub b/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub
new file mode 100644
index 0000000000000000000000000000000000000000..0ae6bb2adee18a318237aa020ab222be0b240aa9
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/172.23.23.25.pub
@@ -0,0 +1 @@
+qS4rT+zjWrbXDhtEF4oyGv8/5oCIE1ZU9FF+O6AL8V4=
diff --git a/morph/grid/production/public-keys/monitoringvpn/server.pub b/morph/grid/production/public-keys/monitoringvpn/server.pub
new file mode 120000
index 0000000000000000000000000000000000000000..0e74cbd09e33c4771cfecb7efea12650c8bd3b51
--- /dev/null
+++ b/morph/grid/production/public-keys/monitoringvpn/server.pub
@@ -0,0 +1 @@
+172.23.23.1.pub
\ No newline at end of file
diff --git a/morph/grid/production/public-keys/users.nix b/morph/grid/production/public-keys/users.nix
new file mode 100644
index 0000000000000000000000000000000000000000..8b586703740765b7a3d462e74ca3ef3cced68da7
--- /dev/null
+++ b/morph/grid/production/public-keys/users.nix
@@ -0,0 +1,2 @@
+let key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGN4VQm3BIQKEFTw6aPrEwNuShf640N+Py2LOKznFCRT exarkun@bottom";
+in { "root" = key; "jcalderone" = key; }
diff --git a/morph/grid/testing/.gitignore b/morph/grid/testing/.gitignore
index db2fc0de62d01d6d7eec83f8f3e8c3b13b20392a..e3b6111c86090b06c38b9e5afd1fcd16838ddf47 100644
--- a/morph/grid/testing/.gitignore
+++ b/morph/grid/testing/.gitignore
@@ -1 +1 @@
-secrets
+private-keys
diff --git a/morph/grid/testing/config.json b/morph/grid/testing/config.json
index c069bbed531e63a425e16a1838dedbfd2f17374d..8b94959557364d8af8f1f4aa61c5647b46db9932 100644
--- a/morph/grid/testing/config.json
+++ b/morph/grid/testing/config.json
@@ -1,8 +1,7 @@
 { "domain": "privatestorage-staging.com"
 , "publicStoragePort": 8898
-, "ristrettoSigningKeyPath": "./secrets/ristretto.signing-key"
-, "stripeSecretKeyPath": "./secrets/stripe.secret"
-, "monitoringvpnKeyDir": "./secrets/monitoringvpn"
+, "privateKeyPath": "./private-keys"
+, "publicKeyPath": "./public-keys"
 , "monitoringvpnEndpoint": "monitoring.privatestorage-staging.com:51820"
 , "passValue": 1000000
 , "issuerDomains": [
diff --git a/morph/grid/testing/grid.nix b/morph/grid/testing/grid.nix
index 19eefd9d50d49094a0bcf87698f8c8091032e1fa..0cdfe5ae755c88baa128eddbafb14f1b19d6edbf 100644
--- a/morph/grid/testing/grid.nix
+++ b/morph/grid/testing/grid.nix
@@ -5,10 +5,12 @@ let
   gridlib = import ../../lib;
   rawConfig = pkgs.lib.trivial.importJSON ./config.json;
   config = rawConfig // {
-    sshUsers = import ./secrets/users.nix;
+    sshUsers = import ./public-keys/users.nix;
 
-    # Get absolute vpn key directory path, as a string:
-    monitoringvpnKeyDir = toString ./. + "/${rawConfig.monitoringvpnKeyDir}";
+    # Convert relative paths to absolute so library code can resolve names
+    # correctly.
+    publicKeyPath = toString ./. + "/${rawConfig.publicKeyPath}";
+    privateKeyPath = toString ./. + "/${rawConfig.privateKeyPath}";
   };
 
   payments = {
@@ -38,7 +40,7 @@ let
       gridlib.hardware-aws
       (gridlib.customize-monitoring {
         inherit hostsMap vpnClientIPs nodeExporterTargets;
-        inherit (config) domain monitoringvpnKeyDir;
+        inherit (config) domain publicKeyPath privateKeyPath;
         monitoringvpnIPv4 = "172.23.23.1";
         stateVersion = "19.09";
       })
diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub
new file mode 100644
index 0000000000000000000000000000000000000000..94e7f1592034419c8a561531811bd6e63241271c
--- /dev/null
+++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.1.pub
@@ -0,0 +1 @@
+iVS3L2DkH/pHAhiPpuduBMKlICPYmchHFfCg6n2ReUI=
diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub
new file mode 100644
index 0000000000000000000000000000000000000000..ed5b6822bd633df6b704fa0eda0e9250d4b198e2
--- /dev/null
+++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.11.pub
@@ -0,0 +1 @@
+sGUEH9+Mli1E1BFBMAHgPsnVlaD1EJKFaYOJ+dpyLy0=
diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub
new file mode 100644
index 0000000000000000000000000000000000000000..0c79d3a917db9f5caed071eabeae9d4974d660db
--- /dev/null
+++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.12.pub
@@ -0,0 +1 @@
+wvpkXigLG2zvmLhxsV2cmN/IgF+nLednV6uENvI6fh0=
diff --git a/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub
new file mode 100644
index 0000000000000000000000000000000000000000..31fd40caf83b95fd8e4566af21b9d6e59a70e629
--- /dev/null
+++ b/morph/grid/testing/public-keys/monitoringvpn/172.23.23.13.pub
@@ -0,0 +1 @@
+5t9t6DOcYMQJNtnsG5/Ek+OmSX1mZgbMAHSWlJQKuxc=
diff --git a/morph/grid/testing/public-keys/monitoringvpn/server.pub b/morph/grid/testing/public-keys/monitoringvpn/server.pub
new file mode 120000
index 0000000000000000000000000000000000000000..0e74cbd09e33c4771cfecb7efea12650c8bd3b51
--- /dev/null
+++ b/morph/grid/testing/public-keys/monitoringvpn/server.pub
@@ -0,0 +1 @@
+172.23.23.1.pub
\ No newline at end of file
diff --git a/morph/grid/testing/public-keys/users.nix b/morph/grid/testing/public-keys/users.nix
new file mode 100644
index 0000000000000000000000000000000000000000..d6a965011065cfe39713adfb797c190eb8dd1ecd
--- /dev/null
+++ b/morph/grid/testing/public-keys/users.nix
@@ -0,0 +1,9 @@
+let
+  jcalderone = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN4GenAY/YLGuf1WoMXyyVa3S9i4JLQ0AG+pt7nvcLlQ exarkun@baryon";
+  flo = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHx7wJQNqKn8jOC4AxySRL2UxidNp7uIK9ad3pMb1ifF flo@fs-la";
+in
+  {
+    "root" = jcalderone;
+    inherit jcalderone;
+    inherit flo;
+  }
diff --git a/morph/lib/customize-issuer.nix b/morph/lib/customize-issuer.nix
index 28edb72e7e0b74879e9e676113c327f50b040d40..1c0d668fbd4ae59bab115c2116b7fa377395dcfc 100644
--- a/morph/lib/customize-issuer.nix
+++ b/morph/lib/customize-issuer.nix
@@ -1,23 +1,15 @@
 # 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 of a directory containing all of the
+  # public keys for the system.  For example, this holds Wireguard public keys
+  # for the VPN configuration and SSH public keys to configure SSH
+  # authentication.
+  publicKeyPath
 
-  # 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 path on the deployment system of a directory containing all of the
+  # corresponding private keys for the system.
+, privateKeyPath
 
   # A string giving the IP address and port number (":"-separated) of the VPN
   # server.
@@ -63,10 +55,24 @@
   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";
+    # 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.
+    "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key";
+
+    # 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.
+    "stripe-secret-key".source = "${privateKeyPath}/stripe.secret";
+
+    # ``.../monitoringvpn`` is a path on the deployment system of 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.
+    "monitoringvpn-secret-key".source = "${privateKeyPath}/monitoringvpn/${monitoringvpnIPv4}.key";
+    "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key";
   };
 
   networking.domain = domain;
@@ -76,7 +82,7 @@
     enable = true;
     ip = monitoringvpnIPv4;
     endpoint = monitoringvpnEndpoint;
-    endpointPublicKeyFile = "${monitoringvpnKeyDir}/server.pub";
+    endpointPublicKeyFile = "${publicKeyPath}/monitoringvpn/server.pub";
   };
 
   services.private-storage-issuer = {
diff --git a/morph/lib/customize-monitoring.nix b/morph/lib/customize-monitoring.nix
index c50eb5062b35480d0b3d296cfaea8abd999f36c5..f77d26bd817ebb556c1d22d01e290d2838ab9485 100644
--- a/morph/lib/customize-monitoring.nix
+++ b/morph/lib/customize-monitoring.nix
@@ -9,7 +9,8 @@
   hostsMap
 
   # See ``customize-issuer.nix``.
-, monitoringvpnKeyDir
+, publicKeyPath
+, privateKeyPath
 , monitoringvpnIPv4
 , domain
 
@@ -34,8 +35,8 @@
   deployment.targetHost = "${config.networking.hostName}.${config.networking.domain}";
 
   deployment.secrets = {
-    "monitoringvpn-private-key".source = "${monitoringvpnKeyDir}/server.key";
-    "monitoringvpn-preshared-key".source = "${monitoringvpnKeyDir}/preshared.key";
+    "monitoringvpn-private-key".source = "${privateKeyPath}/monitoringvpn/server.key";
+    "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key";
   };
 
   networking.domain = domain;
@@ -45,7 +46,7 @@
     enable = true;
     ip = monitoringvpnIPv4;
     inherit vpnClientIPs;
-    pubKeysPath = monitoringvpnKeyDir;
+    pubKeysPath = "${publicKeyPath}/monitoringvpn";
   };
 
   services.private-storage.monitoring.prometheus = {
diff --git a/morph/lib/customize-storage.nix b/morph/lib/customize-storage.nix
index 0a08743633126b5898e61a877e62a7b58314b34e..68655874efd9ba39b52dacfdddaedb54863ed769 100644
--- a/morph/lib/customize-storage.nix
+++ b/morph/lib/customize-storage.nix
@@ -2,8 +2,8 @@
 # ``storage.nix``.
 {
   # See ``customize-issuer.nix``
-  ristrettoSigningKeyPath
-, monitoringvpnKeyDir
+  privateKeyPath
+, publicKeyPath
 , monitoringvpnEndpoint
 , monitoringvpnIPv4
 , sshUsers
@@ -25,9 +25,9 @@
   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";
+    "ristretto-signing-key".source = "${privateKeyPath}/ristretto.signing-key";
+    "monitoringvpn-secret-key".source = "${privateKeyPath}/monitoringvpn/${monitoringvpnIPv4}.key";
+    "monitoringvpn-preshared-key".source = "${privateKeyPath}/monitoringvpn/preshared.key";
   };
 
   networking.domain = domain;
@@ -40,7 +40,7 @@
     enable = true;
     ip = monitoringvpnIPv4;
     endpoint = monitoringvpnEndpoint;
-    endpointPublicKeyFile = "${monitoringvpnKeyDir}/server.pub";
+    endpointPublicKeyFile = "${publicKeyPath}/monitoringvpn/server.pub";
   };
 
   system.stateVersion = stateVersion;
diff --git a/nixos/modules/monitoring/vpn/client.nix b/nixos/modules/monitoring/vpn/client.nix
index dbd50b82ef5b09495e332e6fbb7ac5676f5ac322..ed1933e34d715fba0933f32d606e989b4d1ed4ec 100644
--- a/nixos/modules/monitoring/vpn/client.nix
+++ b/nixos/modules/monitoring/vpn/client.nix
@@ -48,7 +48,7 @@ in {
     };
     endpointPublicKeyFile = lib.mkOption {
       type = lib.types.path;
-      example = lib.literalExample ../PrivateStorageSecrets/monitoringvpn/server.pub;
+      example = lib.literalExample ./monitoringvpn/server.pub;
       description = ''
         File with base64 public key generated by <command>cat private.key | wg pubkey > pubkey.pub</command>.
       '';
@@ -71,4 +71,3 @@ in {
     };
   };
 }
-
diff --git a/nixos/modules/monitoring/vpn/server.nix b/nixos/modules/monitoring/vpn/server.nix
index 2374ddc8657fb299fb83155cbabe328cd54c1aaf..3c41e0209bb7fe18f1a81a44ab509c8442372bbf 100644
--- a/nixos/modules/monitoring/vpn/server.nix
+++ b/nixos/modules/monitoring/vpn/server.nix
@@ -51,7 +51,7 @@ in {
     };
     pubKeysPath = lib.mkOption {
       type = lib.types.path;
-      example = lib.literalExample ../PrivateStorageSecrets/monitoringvpn;
+      example = lib.literalExample ./monitoringvpn;
       description = ''
         The path to the directory that holds the public keys.
       '';
@@ -69,4 +69,3 @@ in {
     };
   };
 }
-
diff --git a/tools/create-vpn-keys.sh b/tools/create-vpn-keys.sh
index e092a8ced698bd3a3bb2d4acc3ca07a3a8e6032d..c81225ec340db38d551e9d0c6c13d7bd44e21a4d 100755
--- a/tools/create-vpn-keys.sh
+++ b/tools/create-vpn-keys.sh
@@ -4,7 +4,7 @@
 # Parameters:
 #   file: path to grid.nix of morph deployment
 #
-# Output: Key files for all monitoring VPN hosts in secrets/monitoringvpn
+# Output: Key files for all monitoring VPN hosts in {private,public}-keys/monitoringvpn
 #         relative to the grid.nix
 #
 # The server key will also be symlinked to server.{key,pub}.
@@ -19,7 +19,8 @@ if [[ $# -ne 1 ]]; then
 fi
 
 SRC=$(dirname $0)
-VPN_SECRETS=$(dirname $1)/secrets/monitoringvpn
+VPN_SECRETS=$(dirname $1)/private-keys/monitoringvpn
+VPN_PUBLIC=$(dirname $1)/public-keys/monitoringvpn
 
 CONFIG=$(nix-instantiate --strict --json --eval "${SRC}"/get-vpn-config.nix --arg pathToGrid "${1}")
 
@@ -27,14 +28,15 @@ MONITORING_IPS=$(echo $CONFIG | jp --unquoted "join(' ', clientIPs)")
 VPNSERVER_IP=$(echo $CONFIG | jp --unquoted "serverIP")
 
 mkdir -p "${VPN_SECRETS}"
+mkdir -p "${VPN_PUBLIC}"
 
 for i in $MONITORING_IPS $VPNSERVER_IP; do
-  wg genkey | tee "${VPN_SECRETS}"/${i}.key | wg pubkey > "${VPN_SECRETS}"/${i}.pub
+  wg genkey | tee "${VPN_SECRETS}"/${i}.key | wg pubkey > "${VPN_PUBLIC}"/${i}.pub
 done
 
 wg genpsk > "${VPN_SECRETS}"/preshared.key
 
 ln -fs $VPNSERVER_IP.key "${VPN_SECRETS}"/server.key
-ln -fs $VPNSERVER_IP.pub "${VPN_SECRETS}"/server.pub
+ln -fs $VPNSERVER_IP.pub "${VPN_PUBLIC}"/server.pub
 
 # EOF