From 15dbd89a28b4e172699a4a6d82b9a832e74c9113 Mon Sep 17 00:00:00 2001
From: Julien Moutinho <julm@sourcephile.fr>
Date: Tue, 23 Jun 2020 19:16:49 +0200
Subject: [PATCH] nix: add module security.pass

---
 nixos/defaults.nix                            |   1 +
 nixos/modules.nix                             |   3 +-
 nixos/modules/install.nix                     |  46 +++++
 nixos/modules/security/install.nix            |  11 --
 nixos/modules/security/pass.nix               | 158 ++++++++++++++++++
 servers/losurdo.nix                           |  29 +---
 servers/losurdo/acme/autogeree.net.nix        |  52 +++---
 servers/losurdo/acme/sourcephile.fr.nix       |  53 +++---
 servers/losurdo/fail2ban.nix                  |   1 +
 servers/losurdo/keys.nix                      |   7 -
 servers/losurdo/networking.nix                |   2 +-
 servers/losurdo/postgresql/openconcerto.nix   |  34 ++--
 servers/losurdo/system.nix                    |  35 ++++
 servers/losurdo/users.nix                     |   6 +-
 servers/mermet.nix                            |  25 +--
 servers/mermet/dovecot.nix                    |   1 -
 servers/mermet/gitolite.nix                   |   1 -
 servers/mermet/knot.nix                       |   4 +-
 servers/mermet/knot/autogeree.net.nix         |  26 +--
 servers/mermet/knot/sourcephile.fr.nix        |  26 +--
 servers/mermet/networking.nix                 |   2 +-
 servers/mermet/nginx.nix                      |   1 -
 servers/mermet/postfix.nix                    |   1 -
 servers/mermet/rspamd.nix                     |  24 ++-
 servers/mermet/rspamd/autogeree.net.nix       |  26 ++-
 servers/mermet/rspamd/sourcephile.fr.nix      |  30 ++--
 servers/mermet/system.nix                     |   2 +
 shell.nix                                     |  15 +-
 .../development/libraries/nix-plugins.nix     |   1 +
 shell/modules/tools/security/gnupg.nix        |  37 ++--
 shell/openpgp.nix                             |  44 ++++-
 31 files changed, 468 insertions(+), 236 deletions(-)
 create mode 100644 nixos/modules/install.nix
 delete mode 100644 nixos/modules/security/install.nix
 create mode 100644 nixos/modules/security/pass.nix
 delete mode 100644 servers/losurdo/keys.nix

diff --git a/nixos/defaults.nix b/nixos/defaults.nix
index 5e53a7f..d97874d 100644
--- a/nixos/defaults.nix
+++ b/nixos/defaults.nix
@@ -108,6 +108,7 @@ environment = {
     binutils
     #dnsutils
     dstat
+    gnupg
     htop
     inetutils
     iotop
diff --git a/nixos/modules.nix b/nixos/modules.nix
index 0222728..c28cd7f 100644
--- a/nixos/modules.nix
+++ b/nixos/modules.nix
@@ -3,7 +3,8 @@
 # its clearer, safer and more flexible if not quicker.
 {
 imports = [
-  modules/security/install.nix
+  modules/install.nix
+  modules/security/pass.nix
   modules/services/networking/domains.nix
   #modules/services/networking/knot.nix
   modules/services/databases/openldap.nix
diff --git a/nixos/modules/install.nix b/nixos/modules/install.nix
new file mode 100644
index 0000000..80e5bfe
--- /dev/null
+++ b/nixos/modules/install.nix
@@ -0,0 +1,46 @@
+{ pkgs, lib, config, ... }:
+let
+  inherit (builtins) listToAttrs;
+  inherit (lib) types;
+  inherit (config) networking;
+  cfg = config.install;
+in
+{
+options.install = {
+  enable = lib.mkEnableOption "Install";
+  shellHook = lib.mkOption {
+    type = types.lines;
+    default = "";
+  };
+  shellScript = lib.mkOption {
+    type = types.lines;
+    default = "";
+    apply = pkgs.writeShellScriptBin "bash";
+  };
+  target = lib.mkOption {
+    type = types.str;
+    default = "root@${networking.hostName}.${networking.domain}";
+  };
+  generations = lib.mkOption {
+    type = types.str;
+    default = "+10";
+  };
+  profile = lib.mkOption {
+    type = types.str;
+    default = "/nix/var/nix/profiles/system";
+  };
+};
+config = lib.mkIf cfg.enable {
+  install.shellScript =
+    let nixos = config.system.build.toplevel; in ''
+    PATH="$PATH:${with pkgs; lib.makeBinPath [nix openssh]}"
+    set -x
+    nix ''${TRACE:+-L} copy \
+     --to ssh://${cfg.target} --substitute-on-destination \
+     ${nixos}
+    ssh ${cfg.target} nix-env --profile "${cfg.profile}" --set "${nixos}" \
+     '&&' nix-env --profile "${cfg.profile}" --delete-generations "${cfg.generations}" \
+     '&&' "${cfg.profile}"/bin/switch-to-configuration "''${switch:-switch}"
+  '';
+};
+}
diff --git a/nixos/modules/security/install.nix b/nixos/modules/security/install.nix
deleted file mode 100644
index 898c89e..0000000
--- a/nixos/modules/security/install.nix
+++ /dev/null
@@ -1,11 +0,0 @@
-{ pkgs, lib, config, ... }:
-let inherit (lib) types; in
-{
-options.security.install = {
-  shellHook = lib.mkOption {
-    type = types.lines;
-    default = "";
-  };
-  # TODO: more structured options, like NixOps' deployment.keys
-};
-}
diff --git a/nixos/modules/security/pass.nix b/nixos/modules/security/pass.nix
new file mode 100644
index 0000000..2a9fd1d
--- /dev/null
+++ b/nixos/modules/security/pass.nix
@@ -0,0 +1,158 @@
+{ pkgs, lib, config, ... }:
+let
+  inherit (builtins) head listToAttrs match split;
+  inherit (lib) types;
+  inherit (config.security) pass;
+  dirname = p:
+    let dir = match "^(.+)/[^/]*$" p; in
+    if dir == [] then "." else head dir;
+  escapeUnitName = name:
+    lib.concatMapStrings (s: if lib.isList s then "-" else s)
+    (split "[^a-zA-Z0-9_.\\-]+" name);
+in
+{
+options.security.pass = {
+  store = lib.mkOption {
+    type = types.path;
+    description = ''
+      Default path to the password-store of the orchestrating system.
+    '';
+  };
+  secrets = lib.mkOption {
+    default = {};
+    type = types.attrsOf (types.submodule ({name, config, ...}: {
+      options = {
+        gpg = lib.mkOption {
+          type = types.path;
+          default = builtins.path {
+            path = toString pass.store + "/${name}.gpg";
+            name = "${escapeUnitName name}.gpg";
+          };
+          description = ''
+            The path to the gnupg-encrypted secret.
+            It will be copied into the Nix store of the orchestrating and of the target system.
+            It must be decipherable by an OpenPGP key within <literal>gnupgHome</literal>,
+            whose passhrase is on the target system into <literal>passphraseFile</literal>.
+            Defaults to the name of the secret, prefixed by <literal>passwordStore</literal>
+            and suffixed by <literal>.gpg</literal>.
+          '';
+        };
+        gnupgHome = lib.mkOption {
+          type = types.str;
+          default = "/root/.gnupg";
+          description = ''
+            The directory on the target system to the <literal>gnupg</literal> home
+            used to decrypt the secret.
+          '';
+        };
+        passphraseFile = lib.mkOption {
+          type = types.str;
+          default = "/root/key.pass";
+          description = ''
+            The directory on the target system to a file containing
+            the password of an OpenPGP key in <literal>gnupgHome</literal>,
+            to which <literal>gpg</literal> secret is encrypted to.
+          '';
+        };
+        mode = lib.mkOption {
+          type = types.str;
+          default = "400";
+          description = ''
+            Permission mode of the secret <literal>path</literal>.
+          '';
+        };
+        user = lib.mkOption {
+          type = types.str;
+          default = "root";
+          description = ''
+            Owner of the secret <literal>path</literal>.
+          '';
+        };
+        group = lib.mkOption {
+          type = types.str;
+          default = "root";
+          description = ''
+            Group of the secret <literal>path</literal>.
+          '';
+        };
+        pipe = lib.mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Shell command taking the deciphered secret on its standard input
+            and which must put on its standard output
+            the actual material to be installed.
+            This allows to decorate the secret with non-secret bits.
+          '';
+        };
+        path = lib.mkOption {
+          type = types.str;
+          default = name;
+          apply = p: if match "^/.*" p == null then "/run/pass-secrets/"+p+"/file" else p;
+          description = ''
+            The path on the target system where the secret is installed to.
+            Any non-absolute path is relative to <filename>/run/pass-secrets</filename>.
+            Default to the name of the secret.
+          '';
+        };
+        service = lib.mkOption {
+          type = types.str;
+          default = "secret-" + escapeUnitName name + ".service";
+          description = ''
+            The name of the systemd service.
+            Useful to put constraints like <literal>after</literal> or <literal>wants</wants>
+            into services requiring this secret.
+          '';
+        };
+        postStart = lib.mkOption {
+          type = types.lines;
+          default = "";
+          example = "systemctl reload nginx.service";
+          description = ''
+            Commands to run after new secrets go live. Typically
+            the web server and other servers using secrets need to
+            be reloaded.
+          '';
+        };
+      };
+    }));
+  };
+};
+config = lib.mkIf (pass.secrets != {}) {
+  #systemd.tmpfiles.rules = [ "d /run/secrets 0755 root root -" ];
+  systemd.services =
+    lib.mapAttrs' (target: secret:
+      lib.nameValuePair (lib.removeSuffix ".service" secret.service) {
+        description = "Install secret ${secret.path}";
+        after = [ "network.target" "network-online.target" ];
+        wantedBy = lib.mkIf (!config.boot.isContainer) [ "multi-user.target" ];
+        script = ''
+          set -o pipefail
+          set -eux
+          decrypt() {
+            {
+            ${pkgs.gnupg}/bin/gpg --batch --pinentry-mode loopback \
+             --homedir '${secret.gnupgHome}' \
+             --passphrase-file '${secret.passphraseFile}' \
+             --decrypt '${secret.gpg}' \
+            ${lib.optionalString (secret.pipe != null) (" | "+secret.pipe)}
+            } |
+            install -D -m '${secret.mode}' -o '${secret.user}' -g '${secret.group}' /dev/stdin \
+             '${secret.path}'
+          }
+          while ! decrypt; do sleep 1; done
+        '';
+        inherit (secret) postStart;
+        serviceConfig = {
+          Type = "oneshot";
+          PrivateTmp = true;
+          WorkingDirectory = dirname secret.gnupgHome;
+        } // lib.optionalAttrs (match "^/.*" target == null) {
+          RuntimeDirectory = dirname secret.path;
+          RuntimeDirectoryMode = "711";
+          RuntimeDirectoryPreserve = false; # FIXME: when does the removal actualy occur with with Type=oneshot?
+        };
+      }
+    ) pass.secrets;
+};
+}
diff --git a/servers/losurdo.nix b/servers/losurdo.nix
index fe92994..a71b771 100644
--- a/servers/losurdo.nix
+++ b/servers/losurdo.nix
@@ -2,8 +2,12 @@
 #
 # Show configuration options with, for example:
 #   nix-instantiate servers/losurdo.nix --eval -A config.networking.hostName
+# or:
+#  nix eval servers.losurdo.config.networking.hostName
 # Install/upgrade with:
 #   nix run install -f servers/losurdo.nix
+# or:
+#   nix run servers.losurdo.install
 let
   ipv4 = "80.67.180.251";
   system = import <nixpkgs/nixos/lib/eval-config.nix> {
@@ -31,26 +35,7 @@ let
       servers = import ../servers.nix;
     };
   };
-  inherit (system.config) networking;
-  lib = system.pkgs.lib;
-in with system; system // {
-inherit ipv4;
-install =
-  let target = "root@${networking.hostName}.${networking.domain}";
-      profile = "/nix/var/nix/profiles/system";
-      generations = "+10";
-      nixos = config.system.build.toplevel;
-  in
-  pkgs.writeShellScriptBin "bash" ''
-  PATH="$PATH:${with pkgs; lib.makeBinPath [nix openssh pass]}"
-  set -eux
-  nix ''${TRACE:+-L} copy \
-   --to ssh://${target} --substitute-on-destination \
-   ${nixos}
-  target="${target}"
-  ${config.security.install.shellHook}
-  ssh ${target} nix-env --profile "${profile}" --set "${nixos}" \
-   '&&' nix-env --profile "${profile}" --delete-generations "${generations}" \
-   '&&' "${profile}"/bin/switch-to-configuration "''${switch:-switch}"
-'';
+in system // {
+  inherit ipv4;
+  install = system.config.install.shellScript;
 }
diff --git a/servers/losurdo/acme/autogeree.net.nix b/servers/losurdo/acme/autogeree.net.nix
index bd95905..23dee6b 100644
--- a/servers/losurdo/acme/autogeree.net.nix
+++ b/servers/losurdo/acme/autogeree.net.nix
@@ -2,17 +2,14 @@
 let
   domain = "autogeree.net";
   domainID = lib.replaceStrings ["."] ["_"] domain;
-  credentialsFile = "/var/lib/acme/.lego/${domain}/rfc2136";
+  inherit (config.security) pass;
   inherit (config.users) users groups;
 in
 {
-systemd.services."acme-${domain}".after = [
-  "unbound.service"
-];
 networking.nftables.ruleset = ''
   # for lego to update ACME DNS-01 challenge
-  add rule inet filter fw2net ip daddr ${servers.mermet.ipv4} tcp dport 53 counter accept comment "DNS"
-  add rule inet filter fw2net ip daddr ${servers.mermet.ipv4} udp dport 53 counter accept comment "DNS"
+  add rule inet filter fw2net ip daddr ${servers.mermet.ipv4} tcp dport 53 counter accept comment "ACME DNS-01"
+  add rule inet filter fw2net ip daddr ${servers.mermet.ipv4} udp dport 53 counter accept comment "ACME DNS-01"
   # for lego to check DNS propagation on ns6.gandi.net
   add rule inet filter fw2net ip daddr 217.70.177.40 tcp dport 53 skuid ${users.root.name} counter accept comment "DNS gandi"
   add rule inet filter fw2net ip daddr 217.70.177.40 udp dport 53 skuid ${users.root.name} counter accept comment "DNS gandi"
@@ -33,23 +30,30 @@ security.acme.certs."${domain}" = {
   # ns6.gandi.net takes roughly 5min to update
   # hence lego's RFC2136_PROPAGATION_TIMEOUT=1000
   #dnsPropagationCheck = false;
-  inherit credentialsFile;
+  credentialsFile = pass.secrets."lego/${domain}/rfc2136".path;
+};
+security.pass.secrets."lego/${domain}/rfc2136" = {
+  pipe = ''
+    cat - ${pkgs.writeText "env" ''
+    RFC2136_NAMESERVER=ns.${domain}:53
+    RFC2136_TSIG_ALGORITHM=hmac-sha256.
+    RFC2136_TSIG_KEY=acme_${domainID}
+    RFC2136_PROPAGATION_TIMEOUT=1000
+    RFC2136_POLLING_INTERVAL=30
+    RFC2136_SEQUENCE_INTERVAL=30
+    RFC2136_DNS_TIMEOUT=1000
+    RFC2136_TTL=1
+    ''}
+  '';
+};
+systemd.services."acme-${domain}" = {
+  after = [
+    "unbound.service"
+    pass.secrets."lego/${domain}/rfc2136".service
+  ];
+  wants = [
+    "unbound.service"
+    pass.secrets."lego/${domain}/rfc2136".service
+  ];
 };
-security.install.shellHook = ''
-  {
-  cat <<-EOF
-  RFC2136_NAMESERVER=ns.${domain}:53
-  RFC2136_TSIG_ALGORITHM=hmac-sha256.
-  RFC2136_TSIG_KEY=acme_${domainID}
-  RFC2136_PROPAGATION_TIMEOUT=1000
-  RFC2136_POLLING_INTERVAL=30
-  RFC2136_SEQUENCE_INTERVAL=30
-  RFC2136_DNS_TIMEOUT=1000
-  RFC2136_TTL=1
-  EOF
-  pass "servers/losurdo/lego/${domain}/rfc2136"
-  } |
-  ssh "$target" install -D -m 0400 -o root -g root /dev/stdin \
-   ${credentialsFile}
-'';
 }
diff --git a/servers/losurdo/acme/sourcephile.fr.nix b/servers/losurdo/acme/sourcephile.fr.nix
index a71f9ea..d1b431b 100644
--- a/servers/losurdo/acme/sourcephile.fr.nix
+++ b/servers/losurdo/acme/sourcephile.fr.nix
@@ -2,17 +2,15 @@
 let
   domain = "sourcephile.fr";
   domainID = lib.replaceStrings ["."] ["_"] domain;
-  credentialsFile = "/var/lib/acme/.lego/${domain}/rfc2136";
+  inherit (config) install;
+  inherit (config.security) pass;
   inherit (config.users) users groups;
 in
 {
-systemd.services."acme-${domain}".after = [
-  "unbound.service"
-];
 networking.nftables.ruleset = ''
   # for lego to update ACME DNS-01 challenge
-  add rule inet filter fw2net tcp dport 53 ip daddr ${servers.mermet.ipv4} counter accept comment "DNS"
-  add rule inet filter fw2net udp dport 53 ip daddr ${servers.mermet.ipv4} counter accept comment "DNS"
+  add rule inet filter fw2net tcp dport 53 ip daddr ${servers.mermet.ipv4} counter accept comment "ACME DNS-01"
+  add rule inet filter fw2net udp dport 53 ip daddr ${servers.mermet.ipv4} counter accept comment "ACME DNS-01"
   # for lego to check DNS propagation on ns6.gandi.net
   add rule inet filter fw2net ip daddr 217.70.177.40 tcp dport 53 skuid ${users.root.name} counter accept comment "DNS gandi"
   add rule inet filter fw2net ip daddr 217.70.177.40 udp dport 53 skuid ${users.root.name} counter accept comment "DNS gandi"
@@ -30,23 +28,30 @@ security.acme.certs."${domain}" = {
   # ns6.gandi.net takes roughly 5min to update
   # hence lego's RFC2136_PROPAGATION_TIMEOUT=1000
   #dnsPropagationCheck = false;
-  inherit credentialsFile;
+  credentialsFile = pass.secrets."lego/${domain}/rfc2136".path;
+};
+security.pass.secrets."lego/${domain}/rfc2136" = {
+  pipe = ''
+    cat - ${pkgs.writeText "env" ''
+    RFC2136_NAMESERVER=ns.${domain}:53
+    RFC2136_TSIG_ALGORITHM=hmac-sha256.
+    RFC2136_TSIG_KEY=acme_${domainID}
+    RFC2136_PROPAGATION_TIMEOUT=1000
+    RFC2136_POLLING_INTERVAL=30
+    RFC2136_SEQUENCE_INTERVAL=30
+    RFC2136_DNS_TIMEOUT=1000
+    RFC2136_TTL=1
+    ''}
+  '';
+};
+systemd.services."acme-${domain}" = {
+  after = [
+    "unbound.service"
+    pass.secrets."lego/${domain}/rfc2136".service
+  ];
+  wants = [
+    "unbound.service"
+    pass.secrets."lego/${domain}/rfc2136".service
+  ];
 };
-security.install.shellHook = ''
-  {
-  cat <<-EOF
-  RFC2136_NAMESERVER=ns.${domain}:53
-  RFC2136_TSIG_ALGORITHM=hmac-sha256.
-  RFC2136_TSIG_KEY=acme_${domainID}
-  RFC2136_PROPAGATION_TIMEOUT=1000
-  RFC2136_POLLING_INTERVAL=30
-  RFC2136_SEQUENCE_INTERVAL=30
-  RFC2136_DNS_TIMEOUT=1000
-  RFC2136_TTL=1
-  EOF
-  pass "servers/losurdo/lego/${domain}/rfc2136"
-  } |
-  ssh "$target" install -D -m 0400 -o root -g root /dev/stdin \
-   ${credentialsFile}
-'';
 }
diff --git a/servers/losurdo/fail2ban.nix b/servers/losurdo/fail2ban.nix
index f0f8018..2f8d109 100644
--- a/servers/losurdo/fail2ban.nix
+++ b/servers/losurdo/fail2ban.nix
@@ -20,6 +20,7 @@ services.fail2ban = {
     servers.mermet.ipv4
     servers.losurdo.ipv4
     "198.252.154.1" # wren.riseup.net
+    "90.78.73.73" # openconcerto user
   ];
   jails = {
     DEFAULT = ''
diff --git a/servers/losurdo/keys.nix b/servers/losurdo/keys.nix
deleted file mode 100644
index 6a11b1f..0000000
--- a/servers/losurdo/keys.nix
+++ /dev/null
@@ -1,7 +0,0 @@
-{ pkgs, lib, config, ... }:
-let
-  inherit (builtins) readFile;
-  inherit (builtins.extraBuiltins) pass;
-in
-{
-}
diff --git a/servers/losurdo/networking.nix b/servers/losurdo/networking.nix
index e98a452..3dde6a7 100644
--- a/servers/losurdo/networking.nix
+++ b/servers/losurdo/networking.nix
@@ -1,7 +1,7 @@
 { pkgs, lib, config, nodes, ... }:
 with builtins;
 let
-  inherit (builtins.extraBuiltins) pass pass-to-file;
+  inherit (builtins.extraBuiltins) pass-to-file;
   inherit (config) networking users;
   lanIPv4        = "192.168.1.215";
   lanNet         = "192.168.1.0/24";
diff --git a/servers/losurdo/postgresql/openconcerto.nix b/servers/losurdo/postgresql/openconcerto.nix
index d4a9e04..d88c14f 100644
--- a/servers/losurdo/postgresql/openconcerto.nix
+++ b/servers/losurdo/postgresql/openconcerto.nix
@@ -5,6 +5,8 @@ let
     url = "https://www.openconcerto.org/fr/telechargement/1.6/OpenConcerto-1.6.3.sql.zip";
     sha256 = "02h35ni9xknzrjsra56c3zhlhs0ji9qc61kcgi7vgcpylqjw0s6n";
   };
+  inherit (config.security) pass;
+  inherit (config.users) users groups;
   inherit (config) networking;
   # Example of ~/.config/OpenConcerto/main.properties
   # DOC: https://code.openconcerto.org/filedetails.php?repname=OpenConcerto&path=%2Ftrunk%2FOpenConcerto%2Fsrc%2Forg%2Fopenconcerto%2Fsql%2FPropsConfiguration.java
@@ -30,7 +32,21 @@ let
   '';
 in
 {
+services.postgresql = {
+  authentication = lib.mkForce ''
+    # CONNECTION  DATABASE USER      AUTH  OPTIONS
+    # FIXME: using scram-sha-256 instead of md5 requires postfix >= 11
+    hostssl       ${db}    ${owner}  all   md5
+  '';
+  identMap = ''
+    # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+    user       root             ${owner}
+  '';
+};
+security.pass.secrets."postgresql/pass/${owner}" = {};
 systemd.services.postgresql = {
+  after = [ pass.secrets."postgresql/pass/${owner}".service ];
+  wants = [ pass.secrets."postgresql/pass/${owner}".service ];
   postStart = lib.mkAfter ''
     sed -e 's/ \(TO\|FROM\) \+openconcerto/ \1 ${owner}/g' \
      ${sql}/OpenConcerto-1.6.3.sql |
@@ -39,7 +55,7 @@ systemd.services.postgresql = {
     lc_collate=fr_FR.UTF-8 \
     lc_type=fr_FR.UTF-8 \
     owner=${owner} \
-    pass=$(cat /run/keys/postgresql_pass_${owner}) \
+    pass=$(cat ${pass.secrets."postgresql/pass/${owner}".path}) \
     pg_createdb ${db} >/dev/null
     
     $PSQL -d "${db}" -AqtX --set ON_ERROR_STOP=1 -f - <<EOF
@@ -54,20 +70,4 @@ systemd.services.postgresql = {
     EOF
   '';
 };
-services.postgresql = {
-  authentication = lib.mkForce ''
-    # CONNECTION  DATABASE USER      AUTH  OPTIONS
-    # FIXME: using scram-sha-256 instead of md5 requires postfix >= 11
-    hostssl       ${db}    ${owner}  all   md5
-  '';
-  identMap = ''
-    # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
-    user       root             ${owner}
-  '';
-};
-security.install.shellHook = ''
-  pass "servers/losurdo/postgresql/pass/${owner}" |
-  ssh "$target" install -D -m 0400 -o root -g root /dev/stdin \
-   /run/keys/postgresql_pass_${owner}
-'';
 }
diff --git a/servers/losurdo/system.nix b/servers/losurdo/system.nix
index 8b0f87f..3e955b1 100644
--- a/servers/losurdo/system.nix
+++ b/servers/losurdo/system.nix
@@ -1,4 +1,8 @@
 { pkgs, lib, config, ... }:
+let
+  inherit (config) networking;
+  inherit (config.security) pass;
+in
 {
 # 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.
@@ -9,6 +13,36 @@ system.stateVersion = "19.09"; # Did you read the comment?
 # and let mosh work smoothly.
 services.logind.killUserProcesses = false;
 
+install = {
+  enable = true;
+  shellScript = lib.mkBefore ''
+    PATH="$PATH:${with pkgs; lib.makeBinPath [gnupg openssh]}"
+    set -x
+    gpg --decrypt '${pass.store}/root/key.pass.gpg' |
+    ssh '${config.install.target}' install -D -m 400 -o root -g root /dev/stdin /root/key.pass
+  '';
+};
+security.pass = {
+  store = ../../../sec/pass/servers/losurdo;
+  secrets."root/key" = {
+    postStart = ''
+      set -x
+      ${pkgs.gnupg}/bin/gpg --batch --pinentry-mode loopback \
+       --homedir /root/.gnupg \
+       --passphrase-file /root/key.pass \
+       --import '${pass.secrets."root/key".path}'
+      shred -u '${pass.secrets."root/key".path}'
+    '';
+  };
+};
+systemd.services = lib.mapAttrs' (target: secret:
+  lib.nameValuePair (lib.removeSuffix ".service" secret.service)
+    (lib.optionalAttrs (target != "root/key") {
+      after = [ pass.secrets."root/key".service ];
+      wants = [ pass.secrets."root/key".service ];
+    })
+  ) pass.secrets;
+
 services.unbound.enable = true;
 
 environment.systemPackages = with pkgs; [
@@ -27,5 +61,6 @@ environment.systemPackages = with pkgs; [
   socat
   sanoid
   #iptables-nftables-compat
+  gnupg
 ];
 }
diff --git a/servers/losurdo/users.nix b/servers/losurdo/users.nix
index a9ecb91..766277d 100644
--- a/servers/losurdo/users.nix
+++ b/servers/losurdo/users.nix
@@ -39,9 +39,5 @@ users = {
   };
 };
 
-security.install.shellHook = ''
-  pass "servers/losurdo/root/ssh/id_ed25519" |
-  ssh "$target" install -m 0400 -o root -g root /dev/stdin \
-   /root/.ssh/id_ed25519
-'';
+security.pass.secrets."/root/.ssh/id_ed25519" = {};
 }
diff --git a/servers/mermet.nix b/servers/mermet.nix
index 2c602b5..0c2ada8 100644
--- a/servers/mermet.nix
+++ b/servers/mermet.nix
@@ -41,26 +41,7 @@ let
       servers = import ../servers.nix;
     };
   };
-  inherit (system.config) networking;
-  lib = system.pkgs.lib;
-in with system; system // {
-inherit ipv4;
-install =
-  let target = "root@${networking.hostName}.${networking.domain}";
-      profile = "/nix/var/nix/profiles/system";
-      generations = "+10";
-      nixos = config.system.build.toplevel;
-  in
-  pkgs.writeShellScriptBin "bash" ''
-  PATH="$PATH:${with pkgs; lib.makeBinPath [nix openssh pass]}"
-  set -eux
-  nix ''${TRACE:+-L} copy \
-   --to ssh://${target} --substitute-on-destination \
-   ${nixos}
-  target="${target}"
-  ${config.security.install.shellHook}
-  ssh ${target} nix-env --profile "${profile}" --set "${nixos}" \
-   '&&' nix-env --profile "${profile}" --delete-generations "${generations}" \
-   '&&' "${profile}"/bin/switch-to-configuration "''${switch:-switch}"
-'';
+in system // {
+  inherit ipv4;
+  install = system.config.install.shellScript;
 }
diff --git a/servers/mermet/dovecot.nix b/servers/mermet/dovecot.nix
index 2488e3c..06e5566 100644
--- a/servers/mermet/dovecot.nix
+++ b/servers/mermet/dovecot.nix
@@ -1,7 +1,6 @@
 { pkgs, lib, config, system, ... }:
 let
   inherit (builtins) toString toFile readFile;
-  inherit (builtins.extraBuiltins) pass;
   inherit (lib) types;
   inherit (pkgs.lib) loadFile unlines unlinesAttrs unlinesValues unwords;
   inherit (config) networking;
diff --git a/servers/mermet/gitolite.nix b/servers/mermet/gitolite.nix
index 28b877c..8ad0fc8 100644
--- a/servers/mermet/gitolite.nix
+++ b/servers/mermet/gitolite.nix
@@ -1,7 +1,6 @@
 { pkgs, lib, config, ... }:
 let
   inherit (builtins) readFile;
-  inherit (builtins.extraBuiltins) pass;
   inherit (lib) types;
   inherit (config) networking;
   inherit (config.services) gitolite;
diff --git a/servers/mermet/knot.nix b/servers/mermet/knot.nix
index 9ec00c2..87f1ea8 100644
--- a/servers/mermet/knot.nix
+++ b/servers/mermet/knot.nix
@@ -12,8 +12,8 @@ imports = [
 options.services.knot = {
   zones = lib.mkOption {
     default = {};
-    type = types.attrsOf (types.submodule ({domain, ...}: {
-      #config.domain = lib.mkDefault domain;
+    type = types.attrsOf (types.submodule ({name, ...}: {
+      #config.domain = lib.mkDefault name;
       options = {
         conf = lib.mkOption {
           type = types.lines;
diff --git a/servers/mermet/knot/autogeree.net.nix b/servers/mermet/knot/autogeree.net.nix
index de8a75a..02a6e06 100644
--- a/servers/mermet/knot/autogeree.net.nix
+++ b/servers/mermet/knot/autogeree.net.nix
@@ -3,28 +3,17 @@ let
   domain = "autogeree.net";
   domainID = lib.replaceStrings ["."] ["_"] domain;
   inherit (builtins) attrValues;
-  inherit (builtins.extraBuiltins) pass git;
+  inherit (builtins.extraBuiltins) git;
   inherit (config) networking;
   inherit (config.services) knot;
+  inherit (config.security) pass;
   inherit (config.users) users groups;
   # Use the Git commit time of the ${domain}.nix file to set the serial number.
   # WARNING: the ${domain}.nix must be committed into Git for this to work.
   # WARNING: this does not take other .nix into account, though they may contribute to the zone's data.
   serial = domain: toString (git ./. [ "log" "-1" "--format=%ct" "--" (domain + ".nix") ]);
-  includes = {
-    "${domain}/acme.conf" = "/var/lib/knot/tsig/${domain}/acme.conf";
-  };
 in
 {
-security.install.shellHook = ''
-  # Generated with: keymgr -t acme_${domain}
-  pass "servers/mermet/knot/${domain}/acme.conf" |
-  ssh "$target" install -D -m 0400 -o ${users."knot".name} -g root /dev/stdin \
-    ${includes."${domain}/acme.conf"}
-'';
-services.knot = {
-  keyFiles = attrValues includes;
-};
 services.knot.zones."${domain}" = {
   conf = ''
     acl:
@@ -108,6 +97,17 @@ services.knot.zones."${domain}" = {
     @ CAA 128 issue "letsencrypt.org"
   '';
 };
+services.knot = {
+  keyFiles = [ pass.secrets."knot/tsig/${domain}/acme.conf".path ];
+};
+security.pass.secrets."knot/tsig/${domain}/acme.conf" = {
+  # Generated with: keymgr -t acme_${domainID}
+  user = users.knot.name;
+};
+systemd.services.knot = {
+  after = [ pass.secrets."knot/tsig/${domain}/acme.conf".service ];
+  wants = [ pass.secrets."knot/tsig/${domain}/acme.conf".service ];
+};
 /* Useless since the zone is public
 services.unbound.extraConfig = ''
   stub-zone:
diff --git a/servers/mermet/knot/sourcephile.fr.nix b/servers/mermet/knot/sourcephile.fr.nix
index 571a5b2..3f61556 100644
--- a/servers/mermet/knot/sourcephile.fr.nix
+++ b/servers/mermet/knot/sourcephile.fr.nix
@@ -3,28 +3,17 @@ let
   domain = "sourcephile.fr";
   domainID = lib.replaceStrings ["."] ["_"] domain;
   inherit (builtins) attrValues;
-  inherit (builtins.extraBuiltins) pass git;
+  inherit (builtins.extraBuiltins) git;
   inherit (config) networking;
+  inherit (config.security) pass;
   inherit (config.services) knot;
   inherit (config.users) users groups;
   # Use the Git commit time of the ${domain}.nix file to set the serial number.
   # WARNING: the ${domain}.nix must be committed into Git for this to work.
   # WARNING: this does not take other .nix into account, though they may contribute to the zone's data.
   serial = domain: toString (git ./. [ "log" "-1" "--format=%ct" "--" (domain + ".nix") ]);
-  includes = {
-    "${domain}/acme.conf" = "/var/lib/knot/tsig/${domain}/acme.conf";
-  };
 in
 {
-security.install.shellHook = ''
-  # Generated with: keymgr -t acme_${domainID}
-  pass "servers/mermet/knot/${domain}/acme.conf" |
-  ssh "$target" install -D -m 0400 -o ${users."knot".name} -g root /dev/stdin \
-    ${includes."${domain}/acme.conf"}
-'';
-services.knot = {
-  keyFiles = attrValues includes;
-};
 services.knot.zones."${domain}" = {
   conf = ''
     acl:
@@ -128,6 +117,17 @@ services.knot.zones."${domain}" = {
     @ CAA 128 issue "letsencrypt.org"
   '';
 };
+services.knot = {
+  keyFiles = [ pass.secrets."knot/tsig/${domain}/acme.conf".path ];
+};
+security.pass.secrets."knot/tsig/${domain}/acme.conf" = {
+  # Generated with: keymgr -t acme_${domainID}
+  user = users.knot.name;
+};
+systemd.services.knot = {
+  after = [ pass.secrets."knot/tsig/${domain}/acme.conf".service ];
+  wants = [ pass.secrets."knot/tsig/${domain}/acme.conf".service ];
+};
 /* Useless since the zone is public
 services.unbound.extraConfig = ''
   stub-zone:
diff --git a/servers/mermet/networking.nix b/servers/mermet/networking.nix
index 7951bc7..d6787a6 100644
--- a/servers/mermet/networking.nix
+++ b/servers/mermet/networking.nix
@@ -1,7 +1,7 @@
 { pkgs, lib, config, ipv4, ... }:
 with builtins;
 let
-  inherit (builtins.extraBuiltins) pass pass-to-file;
+  inherit (builtins.extraBuiltins) pass-to-file;
   inherit (config) networking users;
   netIPv4        = ipv4;
   netIPv4Gateway = "80.67.180.134";
diff --git a/servers/mermet/nginx.nix b/servers/mermet/nginx.nix
index 89d2f81..8a359be 100644
--- a/servers/mermet/nginx.nix
+++ b/servers/mermet/nginx.nix
@@ -1,7 +1,6 @@
 {pkgs, lib, config, system, ...}:
 let
   inherit (builtins) readFile;
-  inherit (builtins.extraBuiltins) pass;
   inherit (lib) types;
   inherit (pkgs.lib) loadFile;
   inherit (config) networking;
diff --git a/servers/mermet/postfix.nix b/servers/mermet/postfix.nix
index 9803640..179db16 100644
--- a/servers/mermet/postfix.nix
+++ b/servers/mermet/postfix.nix
@@ -1,7 +1,6 @@
 { pkgs, lib, config, ... }:
 let
   inherit (builtins) attrNames concatStringsSep readFile toPath;
-  inherit (builtins.extraBuiltins) pass;
   inherit (lib) types;
   inherit (pkgs.lib) loadFile unlines unwords unlinesAttrs;
   inherit (config) networking users;
diff --git a/servers/mermet/rspamd.nix b/servers/mermet/rspamd.nix
index 3ed3ef2..129c076 100644
--- a/servers/mermet/rspamd.nix
+++ b/servers/mermet/rspamd.nix
@@ -1,9 +1,9 @@
 { pkgs, lib, config, ... }:
 let
   inherit (builtins) attrNames listToAttrs readFile;
-  inherit (builtins.extraBuiltins) pass pass-chomp;
   inherit (lib) types;
   inherit (pkgs.lib) unlinesAttrs;
+  inherit (config.security) pass;
   inherit (config.services) postfix rspamd dovecot2 redis;
   inherit (config.users) users;
 in
@@ -22,7 +22,6 @@ options = {
 };
 config = {
 users.users."${rspamd.user}".extraGroups = [
-  "keys"
   users.redis.group
 ];
 services.rspamd = {
@@ -32,12 +31,12 @@ services.rspamd = {
   locals = {
     "dkim_signing.conf".text = ''
       selector_map = ${rspamd.dkimSelectorMap};
-      path = "/run/keys/dkim.$domain.$selector.key";
+      path = "/run/pass-secrets/rspamd/dkim/$domain/$selector.key";
       allow_username_mismatch = true;
     '';
     "arc.conf".text = ''
       selector_map = ${rspamd.dkimSelectorMap};
-      path = "/run/keys/dkim.$domain.$selector.key";
+      path = "/run/pass-secrets/rspamd/dkim/$domain/$selector.key";
       allow_username_mismatch = true;
     '';
     "redis.conf".text = ''
@@ -95,19 +94,30 @@ services.rspamd = {
       '';
     };
     controller = {
-      includes = [ "$CONFDIR/worker-controller.inc" ];
+      includes = [
+        "$CONFDIR/worker-controller.inc"
+        pass.secrets."rspamd/controller/hashedPassword".path
+      ];
       bindSockets = [
         "127.0.0.1:11334"
       ];
       extraConfig = ''
         #count = 1;
         #static_dir = "''${WWWDIR}";
-        # USE: rspamadm pw
-        password = "${pass-chomp "servers/mermet/rspamd/controller/hashedPassword"}";
       '';
     };
   };
 };
+security.pass.secrets."rspamd/controller/hashedPassword" = {
+  # Generated with: rspamadm pw
+  user = rspamd.user;
+  pipe = ''sed -e '/.*/password = "\\0"/' '';
+  postRun = "systemctl reload rspamd";
+};
+systemd.services.rspamd = {
+  wants = [ pass.secrets."rspamd/controller/hashedPassword".service ];
+  after = [ pass.secrets."rspamd/controller/hashedPassword".service ];
+};
 /*
 services.postfix.extraConfig = ''
   smtpd_milters = unix:/run/rspamd.sock
diff --git a/servers/mermet/rspamd/autogeree.net.nix b/servers/mermet/rspamd/autogeree.net.nix
index b072e06..17ec9d2 100644
--- a/servers/mermet/rspamd/autogeree.net.nix
+++ b/servers/mermet/rspamd/autogeree.net.nix
@@ -1,18 +1,16 @@
 { domain, ... }:
 { pkgs, lib, config, ... }:
 let
-  inherit (builtins.extraBuiltins) pass;
+  inherit (config.security) pass;
   inherit (config.services) rspamd;
   selector = "20200101";
 in
 {
-systemd.services.rspamd.after =
- [ "dkim.${domain}.${selector}.key-key.service" ];
 services.rspamd.dkimSelectorMap = ''
   ${domain} ${selector}
 '';
 # rspamadm dkim_keygen -d autogeree.net -s 20200101 -b 4096 -t rsa -k /proc/self/fd/3 3>&1 >>servers/mermet/rspamd/autogeree.net.nix |
-# pass insert -m dkim/autogeree.net/20200101.key
+# pass insert -m servers/mermet/rspamd/dkim/autogeree.net/20200101.key
 services.knot.zones."${domain}".data = ''
   20200101._domainkey IN TXT ( "v=DKIM1; k=rsa; "
     "p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAk15FhAquBY4pcb6HsCqyxK6Sm9AnScsyw7yAOPGQc+26mUKUYTBwywsjAR0zG58tZaCVXZ5EzaRAK/MsKShZ5kwGLzyZoBkexjepcJkP0DuB6WhBQeLhLvdXQVeBuosbqnklW7UHJw0EkNMbThxUrpjwd6P6tmLCFI9pNl2LC3VxfPNu7o8EVgHcuHm4+UCFRUAeHisWasEtD0kVj"
@@ -20,16 +18,12 @@ services.knot.zones."${domain}".data = ''
     "+hH+Mr/4V1wnKtdosk/7+3VIQ6clTIfWhD6PlnWd78Uo5lfWnYxTem7EMc2q7j6tzGwj+Q+b4Li9fdhLqxGuD0V64/nVZit90b0HyfiV5srln2lK6Hczrwqr0gOEBGQ4YeLjOF6ldaV01mFWR9ddr9a5/gVCqw8vw7vhqXvU7yK8VHW2rdsvkNZ0bDOa66MCveD7pH2vyljrfZq9k0T/NLHrsu8CAwEAAQ=="
   )
 '';
-services.nsd.zones."${domain}".data = ''
-  20200101._domainkey IN TXT ( "v=DKIM1; k=rsa; "
-    "p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAk15FhAquBY4pcb6HsCqyxK6Sm9AnScsyw7yAOPGQc+26mUKUYTBwywsjAR0zG58tZaCVXZ5EzaRAK/MsKShZ5kwGLzyZoBkexjepcJkP0DuB6WhBQeLhLvdXQVeBuosbqnklW7UHJw0EkNMbThxUrpjwd6P6tmLCFI9pNl2LC3VxfPNu7o8EVgHcuHm4+UCFRUAeHisWasEtD0kVj"
-    "vDOoFvLEJ/KNI7jBZYFd8Q6dDL8NF28A3LUpKm/Fk73aW7cLAeigT6wiyuW94gIdU4Co0mXLVbakgiofYNC32L4FsbgFw+UN0XuBJwMZQskD6AkQHhZ0T7wYXCAcPGrbjmrqtPfV9YZSOB6lob3EMcPuZgpikWiT1bgsR7LBAA5KsZpRpuWjnpH4fgay3biEc2kXBvvzh4baozJvhF32vV9bSVc5z0jR9rZjR/qgJKSce8xQa0RfbZLJsVI9TgJ"
-    "+hH+Mr/4V1wnKtdosk/7+3VIQ6clTIfWhD6PlnWd78Uo5lfWnYxTem7EMc2q7j6tzGwj+Q+b4Li9fdhLqxGuD0V64/nVZit90b0HyfiV5srln2lK6Hczrwqr0gOEBGQ4YeLjOF6ldaV01mFWR9ddr9a5/gVCqw8vw7vhqXvU7yK8VHW2rdsvkNZ0bDOa66MCveD7pH2vyljrfZq9k0T/NLHrsu8CAwEAAQ=="
-  )
-'';
-security.install.shellHook = ''
-  pass "dkim/${domain}/${selector}.key" |
-  ssh "$target" install -D -m 0400 -o ${rspamd.user} -g root /dev/stdin \
-   /run/keys/"dkim.${domain}.${selector}.key"
-'';
+security.pass.secrets."rspamd/dkim/${domain}/${selector}.key" = {
+  user = rspamd.user;
+  postRun = "systemctl reload rspamd";
+};
+systemd.services.rspamd = {
+  wants = [ pass.secrets."rspamd/dkim/${domain}/${selector}.key".service ];
+  after = [ pass.secrets."rspamd/dkim/${domain}/${selector}.key".service ];
+};
 }
diff --git a/servers/mermet/rspamd/sourcephile.fr.nix b/servers/mermet/rspamd/sourcephile.fr.nix
index 9307d12..f700c42 100644
--- a/servers/mermet/rspamd/sourcephile.fr.nix
+++ b/servers/mermet/rspamd/sourcephile.fr.nix
@@ -1,18 +1,17 @@
 { domain, ... }:
 { pkgs, lib, config, ... }:
 let
-  inherit (builtins.extraBuiltins) pass;
-  inherit (lib) types;
+  inherit (config.security) pass;
   inherit (config.services) rspamd;
   selector = "20200101";
 in
 {
-systemd.services.rspamd.after =
- [ "dkim.${domain}.${selector}.key-key.service" ];
 services.rspamd.dkimSelectorMap = ''
   mermet    ${selector}
   ${domain} ${selector}
 '';
+# rspamadm dkim_keygen -d sourcephile.fr -s 20200101 -b 4096 -t rsa -k /proc/self/fd/3 3>&1 >>servers/mermet/rspamd/sourcephile.fr.nix |
+# pass insert -m servers/mermet/rspamd/dkim/sourcephile.fr/20200101.key
 services.knot.zones."${domain}".data = ''
   20200101._domainkey IN TXT ( "v=DKIM1; k=rsa; "
     "p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7EKzverbG+5JF+yFjH3MrxLyauiHyLqBbV/8LEMunoKXF8sqhBpQtAQXruLqsyUkxR/4CAyPMyzmcdrU43boMj9yFqLrg/kEz2RIvai9jXBqRoWRW1y7F0LbZmdtOTncuDSP8Zzo02XUzsOC4f/C3tEQHS5rc"
@@ -23,19 +22,12 @@ services.knot.zones."${domain}".data = ''
     "rWWtSTdO8DilDqN8CAwEAAQ=="
   )
 '';
-services.nsd.zones."${domain}".data = ''
-  20200101._domainkey IN TXT ( "v=DKIM1; k=rsa; "
-    "p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7EKzverbG+5JF+yFjH3MrxLyauiHyLqBbV/8LEMunoKXF8sqhBpQtAQXruLqsyUkxR/4CAyPMyzmcdrU43boMj9yFqLrg/kEz2RIvai9jXBqRoWRW1y7F0LbZmdtOTncuDSP8Zzo02XUzsOC4f/C3tEQHS5rc"
-    "hzfhU5FY1CeO6eBMV79qKBOvGMKahQTrrtU6olAAJxOhn6wRuwSf"
-    "+m3on1OqiuXYYIgNHKdRhJ8gDwIm/3LEpYMD0gTgJiyclCLoLGHGtKZy1Wf9xV9/7V6fHE4JW5SDivwslVTL+KPXOlIpo5NDHpMxPYOcIg2K4Rj/j7jhavo+fG43q1LhwaPkEMQMbplgnjeMY8300odRiklTkMMpH0m35ZNeHQJSRpEtV8y5xUNxVaGzfqX5iStwV/mQ1Kn"
-    "ZSe8ORTNq+eTTFnDk6zdUXjagcf0wO6QsSTeAz/G8CqOBbwmrU+q"
-    "F8WbGAeRnhz51mH6fTTfsQ1nwjAiF4ou+eQGTkTMN23KkCKpuozJnxqx4DCEr6J1bL83fhXw7CgcfgKgTOk/HFJpeiGhqodw18r4DWBA6G57z9utm7Mr/9SoVnMq6iK9iEcbCllLR8Sz4viatLSRzhodbk7hfvXS3jmCFjILAjFmA7aMTemDMBDQhpAGF9F8sjFUbEJIZjK"
-    "rWWtSTdO8DilDqN8CAwEAAQ=="
-  )
-'';
-security.install.shellHook = ''
-  pass "dkim/${domain}/${selector}.key" |
-  ssh "$target" install -D -m 0400 -o ${rspamd.user} -g root /dev/stdin \
-   /run/keys/"dkim.${domain}.${selector}.key"
-'';
+security.pass.secrets."rspamd/dkim/${domain}/${selector}.key" = {
+  user = rspamd.user;
+  postRun = "systemctl reload rspamd";
+};
+systemd.services.rspamd = {
+  after = [ pass.secrets."rspamd/dkim/${domain}/${selector}.key".service ];
+  wants = [ pass.secrets."rspamd/dkim/${domain}/${selector}.key".service ];
+};
 }
diff --git a/servers/mermet/system.nix b/servers/mermet/system.nix
index 91cffc9..3146672 100644
--- a/servers/mermet/system.nix
+++ b/servers/mermet/system.nix
@@ -7,6 +7,8 @@ system.stateVersion = "19.09"; # Did you read the comment?
 
 services.unbound.enable = true;
 
+security.passwordStore = ../../../sec/pass/servers/mermet;
+
 environment.systemPackages = with pkgs; [
   cryptsetup
   direnv
diff --git a/shell.nix b/shell.nix
index b5693d1..281180c 100644
--- a/shell.nix
+++ b/shell.nix
@@ -58,6 +58,7 @@ let
   # to expand shellHook and buildInputs of this shell.nix
   configuration = {config, ...}: {
     imports = [
+      shell/openpgp.nix
     ];
     nix = {
       nixConf = ''
@@ -70,7 +71,6 @@ let
     gnupg = {
       enable = true;
       gnupgHome = toString ../sec/gnupg;
-      keys = import shell/openpgp.nix;
       gpgExtraConf = ''
         # julm@sourcephile.fr
         trusted-key 0xB2450D97085B7B8C
@@ -185,9 +185,17 @@ pkgs.mkShell {
   shellHook = ''
     echo >&2 "nix: running shellHook"
 
+    # password-store
+    export PASSWORD_STORE_DIR="$PWD"/../sec/pass
+
     # Nix
     PATH=$NIX_SHELL_PATH:$PATH
-    export NIX_PATH="servers=$PWD/servers.nix:nixpkgs=${toString pkgs.path}:nixpkgs-overlays=$PWD/nixpkgs/overlays.nix"
+    export NIX_PATH="${lib.concatStringsSep ":" [
+      "servers=$PWD/servers.nix"
+      #"pass=$PASSWORD_STORE_DIR"
+      "nixpkgs=${toString pkgs.path}"
+      "nixpkgs-overlays=$PWD/nixpkgs/overlays.nix"
+    ]}"
 
     # Since the .envrc calls this shellHook
     # the EXIT trap cannot be freely used
@@ -197,9 +205,6 @@ pkgs.mkShell {
 
     ${modules.nix-shell.shellHook}
 
-    # password-store
-    export PASSWORD_STORE_DIR="$PWD"/../sec/pass
-
     # gpg
     export GPG_TTY=$(tty)
     gpg-connect-agent updatestartuptty /bye >/dev/null
diff --git a/shell/modules/development/libraries/nix-plugins.nix b/shell/modules/development/libraries/nix-plugins.nix
index 1b6ada8..ee3d69b 100644
--- a/shell/modules/development/libraries/nix-plugins.nix
+++ b/shell/modules/development/libraries/nix-plugins.nix
@@ -71,6 +71,7 @@ in
         pass-to-file = path: name: exec [ "${nix-pass-to-file}" path name ];
         git          = dir: args: exec ([ "${nix-git}" dir ] ++ args);
         git-time     = dir: path: exec [ "${nix-git}" dir "log" "-1" "--format=%ct" "--" path ];
+        gpg          = args: exec ([ "${pkgs.gnupg}/bin/gpg" ] ++ args);
       '';
       description = ''
         Content put in extra-builtins.nix for nix-plugins.
diff --git a/shell/modules/tools/security/gnupg.nix b/shell/modules/tools/security/gnupg.nix
index 4c2f3b1..f096017 100644
--- a/shell/modules/tools/security/gnupg.nix
+++ b/shell/modules/tools/security/gnupg.nix
@@ -14,13 +14,14 @@ let
    , expire ? "-"
    , passPath
    , subKeys ? {}
+   , postRun ? ""
    , ...
    }@primary:
     ''
     info "generateKey uid=\"${uid}\""
     if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
@@ -35,6 +36,7 @@ let
     ''
     + unlines (map (generateSubKey primary) subKeys)
     + generateBackupKey "$fpr" primary
+    + postRun
     ;
   generateSubKey =
    primary:
@@ -47,7 +49,7 @@ let
     info "  generateSubKey usage=[${unwords usage}]"
     if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
      then
-      ${pkgs.pass}/bin/pass "${primary.passPath}" |
+      ${if primary.passPath != "" then "${pkgs.pass}/bin/pass '${primary.passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
@@ -74,9 +76,10 @@ let
      fi
     '' + (if backupRecipients == [""] then
     ''
-    if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc"
+    if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" &&
+     ${gpg-with-home}/bin/gpg-with-home --list-secret-keys "${fpr}" | grep -q "sec "
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --pinentry-mode loopback --passphrase-fd 0 \
         --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \
@@ -84,7 +87,7 @@ let
      fi
     if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \
@@ -93,7 +96,7 @@ let
      fi
     if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \
@@ -103,7 +106,7 @@ let
     '' else ''
     if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --pinentry-mode loopback --passphrase-fd 0 \
         --armor --gen-revoke "${fpr}" |
@@ -112,7 +115,7 @@ let
      fi
     if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --armor --export-options export-backup \
@@ -122,7 +125,7 @@ let
      fi
     if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
      then
-      ${pkgs.pass}/bin/pass "${passPath}" |
+      ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
       ${gpg-with-home}/bin/gpg-with-home \
         --batch --pinentry-mode loopback --passphrase-fd 0 \
         --armor --export-options export-backup \
@@ -298,13 +301,12 @@ options.gnupg = {
           backupRecipients = ["@john@doe.pro"];
         };
       };
-    type = types.attrsOf (types.submodule ({uid, ...}: {
-      #config.uid = lib.mkDefault uid;
+    type = types.attrsOf (types.submodule ({name, ...}: {
       options = {
         uid = lib.mkOption {
           type        = types.str;
           example     = "John Doe <john.doe@example.coop>";
-          default     = uid;
+          default     = name;
           description = ''
             User ID.
           '';
@@ -319,7 +321,7 @@ options.gnupg = {
         };
         expire = lib.mkOption {
           type        = types.str;
-          default     = "1y";
+          default     = "0";
           example     = "1y";
           description = ''
             Expiration timeout.
@@ -353,7 +355,7 @@ options.gnupg = {
               };
               expire = lib.mkOption {
                 type        = types.str;
-                default     = "1y";
+                default     = "0";
                 example     = "1y";
                 description = ''
                   Expiration timeout.
@@ -378,6 +380,13 @@ options.gnupg = {
             Backup keys used to encrypt the a backup copy of the secret keys.
           '';
         };
+        postRun = lib.mkOption {
+          type = types.lines;
+          default = "";
+          description = ''
+            Shell code to run after the key has been generated or tested to exist.
+          '';
+        };
       };
     }));
   };
diff --git a/shell/openpgp.nix b/shell/openpgp.nix
index 01937ae..692c6f3 100644
--- a/shell/openpgp.nix
+++ b/shell/openpgp.nix
@@ -1,4 +1,6 @@
+{ pkgs, lib, config, ... }:
 {
+gnupg.keys = {
 "Julien Moutinho <julm@sourcephile.fr>" = {
   uid = "Julien Moutinho <julm@sourcephile.fr>";
   algo   = "rsa4096";
@@ -6,10 +8,10 @@
   usage  = ["cert" "sign"];
   passPath = "members/julm/gpg/password";
   subKeys = [
-    { algo = "rsa4096"; expire = "3y"; usage = ["sign"];}
-    { algo = "rsa4096"; expire = "3y"; usage = ["encrypt"];}
-    { algo = "rsa4096"; expire = "3y"; usage = ["auth"];}
-    ];
+    { algo = "rsa4096"; expire = "3y"; usage = ["sign"]; }
+    { algo = "rsa4096"; expire = "3y"; usage = ["encrypt"]; }
+    { algo = "rsa4096"; expire = "3y"; usage = ["auth"]; }
+  ];
   backupRecipients = [""];
 };
 "Julien Moutinho <julm@mermet>" = {
@@ -19,10 +21,36 @@
   usage  = ["cert" "sign"];
   passPath = "members/julm/gpg/password";
   subKeys = [
-    { algo = "rsa4096"; expire = "3y"; usage = ["sign"];}
-    { algo = "rsa4096"; expire = "3y"; usage = ["encrypt"];}
-    { algo = "rsa4096"; expire = "3y"; usage = ["auth"];}
-    ];
+    { algo = "rsa4096"; expire = "3y"; usage = ["sign"]; }
+    { algo = "rsa4096"; expire = "3y"; usage = ["encrypt"]; }
+    { algo = "rsa4096"; expire = "3y"; usage = ["auth"]; }
+  ];
   backupRecipients = [""];
 };
+"root@losurdo.sourcephile.fr" = let srv = "losurdo"; in {
+  uid = "root@${srv}.sourcephile.fr";
+  algo   = "rsa4096";
+  expire = "0";
+  usage  = ["cert" "sign"];
+  passPath = "servers/${srv}/root/key.pass";
+  subKeys = [
+    { algo = "rsa4096"; expire = "0"; usage = ["encrypt"]; }
+  ];
+  backupRecipients = [""];
+  # This subkey is put into a root/key.gpg, and then on losurdo's Nix store,
+  # to decrypt servers.losurdo.config.security.secrets
+  # Its passphrase in root/key.pass is decrypted and sent by ssh before each call to nix copy.
+  postRun = ''
+    info "  generate $PASSWORD_STORE_DIR/servers/${srv}/root/key.gpg"
+    test -s "$PASSWORD_STORE_DIR/servers/${srv}/root/key.gpg" || {
+      ${pkgs.gnupg}/bin/gpg --batch --pinentry-mode loopback --export-secret-keys --armor \
+       --passphrase-fd 3 3< <(${pkgs.gnupg}/bin/gpg --decrypt "$PASSWORD_STORE_DIR/servers/${srv}/root/key.pass.gpg") \
+       --export-options export-minimal @root@${srv}.sourcephile.fr |
+      ${pkgs.gnupg}/bin/gpg --symmetric --batch --pinentry-mode loopback \
+       --passphrase-fd 3 3< <(${pkgs.gnupg}/bin/gpg --decrypt "$PASSWORD_STORE_DIR/servers/${srv}/root/key.pass.gpg") \
+       --output "$PASSWORD_STORE_DIR/servers/${srv}/root/key.gpg"
+    }
+  '';
+};
+};
 }
-- 
2.49.0