nix: polish comments
[sourcephile-nix.git] / nixos / modules / services / security / tor.nix
index 0d3153e2c72833751fd9de1e07ab4ea51a6da87d..76a8302b8aafb1af8a531d7b1e2009262eca9926 100644 (file)
@@ -7,16 +7,15 @@ let
   cfg = config.services.tor;
   stateDir = "/var/lib/tor";
   runDir = "/run/tor";
-  rootDir = "${runDir}/mnt-root";
   descriptionGeneric = option: ''
     See <link xlink:href="https://2019.www.torproject.org/docs/tor-manual.html.en#${option}"/>.
   '';
   bindsPrivilegedPort =
     any (p0:
       let p1 = if p0 ? "port" then p0.port else p0; in
-      p1 == "auto" || (
-      let p2 = if isInt p1 then p1 else toInt p1;
-      in p1 != null && 0 < p2 && p2 < 1024))
+      if p1 == "auto" then false
+      else let p2 = if isInt p1 then p1 else toInt p1; in
+        p1 != null && 0 < p2 && p2 < 1024)
     (flatten [
       cfg.settings.ORPort
       cfg.settings.DirPort
@@ -188,9 +187,9 @@ let
     settings));
   torrc = pkgs.writeText "torrc" (
     genTorrc cfg.settings +
-    concatStrings (mapAttrsToList (name: config:
-      "HiddenServiceDir ${config.path}\n" +
-      genTorrc config.settings) cfg.relay.hiddenServices)
+    concatStrings (mapAttrsToList (name: onion:
+      "HiddenServiceDir ${onion.path}\n" +
+      genTorrc onion.settings) cfg.relay.onionServices)
   );
 in
 {
@@ -219,7 +218,7 @@ in
     (mkRemovedOptionModule [ "services" "tor" "client" "dns" "listenAddress" ] "Use services.tor.settings.DNSPort instead.")
     (mkRemovedOptionModule [ "services" "tor" "client" "dns" "isolationOptions" ] "Use services.tor.settings.DNSPort instead.")
     (mkRenamedOptionModule [ "services" "tor" "client" "dns" "automapHostsSuffixes" ] [ "services" "tor" "settings" "AutomapHostsSuffixes" ])
-    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "hiddenServices" ])
+    (mkRenamedOptionModule [ "services" "tor" "hiddenServices" ] [ "services" "tor" "relay" "onionServices" ])
   ];
 
   options = {
@@ -264,7 +263,7 @@ in
           to route HTTP traffic is set over a faster, but less isolated port (9063).
         '';
 
-        hiddenServices = mkOption {
+        onionServices = mkOption {
           description = descriptionGeneric "HiddenServiceDir";
           default = {};
           example = {
@@ -431,7 +430,7 @@ in
           '';
         };
 
-        hiddenServices = mkOption {
+        onionServices = mkOption {
           description = descriptionGeneric "HiddenServiceDir";
           default = {};
           example = {
@@ -445,18 +444,25 @@ in
           type = types.attrsOf (types.submodule ({name, config, ...}: {
             options.path = mkOption {
               type = types.path;
-              default = stateDir + "/onion/${name}";
-              description = "Path where to store the data files of the hidden service.";
+              description = ''
+                Path where to store the data files of the hidden service.
+                If the <option>secretKey</option> is null
+                this defaults to <literal>${stateDir}/onion/$onion</literal>,
+                otherwise to <literal>${runDir}/onion/$onion</literal>.
+              '';
             };
-            /*
-            options.hostname = mkOption {
-              type = types.str;
+            options.secretKey = mkOption {
+              type = with types; nullOr path;
               default = null;
+              example = "/run/keys/tor/onion/expyuzz4wqqyqhjn/hs_ed25519_secret_key";
               description = ''
-                Path where to store the data files of the hidden service.
+                Secret key of the onion service.
+                If null, Tor reuses any preexisting secret key (in <option>path</option>)
+                or generates a new one.
+                The associated public key and hostname are deterministically regenerated
+                from this file if they do not exist.
               '';
             };
-            */
             options.authorizeClient = mkOption {
               description = descriptionGeneric "HiddenServiceAuthorizeClient";
               default = null;
@@ -471,7 +477,7 @@ in
                     '';
                   };
                   clientNames = mkOption {
-                    type = types.nonEmptyListOf (types.strMatching "[A-Za-z0-9+-_]+");
+                    type = with types; nonEmptyListOf (strMatching "[A-Za-z0-9+-_]+");
                     description = ''
                       Only clients that are listed here are authorized to access the hidden service.
                       Generated authorization data can be found in <filename>${stateDir}/onion/$name/hostname</filename>.
@@ -548,6 +554,7 @@ in
               };
             };
             config = {
+              path = mkDefault ((if config.secretKey == null then stateDir else runDir) + "/onion/${name}");
               settings.HiddenServiceVersion = config.version;
               settings.HiddenServiceAuthorizeClient =
                 if config.authorizeClient != null then
@@ -682,10 +689,7 @@ in
                 };
               }))
             ]);
-            apply = p:
-              if isInt p || isString p
-              then { port = p; }
-              else p;
+            apply = p: if isInt p || isString p then { port = p; } else p;
           };
           options.ExtORPortCookieAuthFile = optionPath "ExtORPortCookieAuthFile";
           options.ExtORPortCookieAuthFileGroupReadable = optionBool "ExtORPortCookieAuthFileGroupReadable";
@@ -710,12 +714,11 @@ in
               (submodule ({config, ...}: {
                 options = {
                   onion = mkOption {
-                    # FIXME: is the regexp correctly written?
-                    type = strMatching "[a-z2-7]\\{16\\}\\(\\.onion\\)?";
+                    type = strMatching "[a-z2-7]{16}(\\.onion)?";
                     example = "xxxxxxxxxxxxxxxx.onion";
                   };
                   auth = mkOption {
-                    type = strMatching "[A-Za-z0-9+/]\\{22\\}";
+                    type = strMatching "[A-Za-z0-9+/]{22}";
                   };
                 };
               }))
@@ -816,7 +819,7 @@ in
     # Not sure if `cfg.relay.role == "private-bridge"` helps as tor
     # sends a lot of stats
     warnings = optional (cfg.settings.BridgeRelay &&
-      flatten (mapAttrsToList (n: h: h.map) cfg.relay.hiddenServices) != [])
+      flatten (mapAttrsToList (n: o: o.map) cfg.relay.onionServices) != [])
       ''
         Running Tor hidden services on a public relay makes the
         presence of hidden services visible through simple statistical
@@ -827,24 +830,24 @@ in
         actually hide your hidden services. In either case, you can
         always create a container/VM with a separate Tor daemon instance.
       '' ++
-      flatten (mapAttrsToList (n: h:
-        optional (h.settings.HiddenServiceVersion == 2) [
-          (optional (h.settings.HiddenServiceExportCircuitID != null) ''
+      flatten (mapAttrsToList (n: o:
+        optional (o.settings.HiddenServiceVersion == 2) [
+          (optional (o.settings.HiddenServiceExportCircuitID != null) ''
             HiddenServiceExportCircuitID is used in the HiddenService: ${n}
             but this option is only for v3 hidden services.
           '')
         ] ++
-        optional (h.settings.HiddenServiceVersion != 2) [
-          (optional (h.settings.HiddenServiceAuthorizeClient != null) ''
+        optional (o.settings.HiddenServiceVersion != 2) [
+          (optional (o.settings.HiddenServiceAuthorizeClient != null) ''
             HiddenServiceAuthorizeClient is used in the HiddenService: ${n}
             but this option is only for v2 hidden services.
           '')
-          (optional (h.settings.RendPostPeriod != null) ''
+          (optional (o.settings.RendPostPeriod != null) ''
             RendPostPeriod is used in the HiddenService: ${n}
             but this option is only for v2 hidden services.
           '')
         ]
-      ) cfg.relay.hiddenServices);
+      ) cfg.relay.onionServices);
 
     users.groups.tor.gid = config.ids.gids.tor;
     users.users.tor =
@@ -877,6 +880,16 @@ in
           PublishServerDescriptor = false;
         }
       ))
+      (mkIf (!cfg.relay.enable) {
+        # Avoid surprises when leaving ORPort/DirPort configurations in cfg.settings,
+        # because it would still enable Tor as a relay,
+        # which can trigger all sort of problems when not carefully done,
+        # like the blocklisting of the machine's IP addresses
+        # by some hosting providers...
+        DirPort = mkForce [];
+        ORPort = mkForce [];
+        PublishServerDescriptor = mkForce false;
+      })
       (mkIf cfg.client.enable (
         { SOCKSPort = [
             { addr = "127.0.0.1"; port = 9050; IsolateDestAddr = true; }
@@ -888,7 +901,7 @@ in
           DNSPort = [{ addr = "127.0.0.1"; port = 9053; }];
           AutomapHostsOnResolve = true;
           AutomapHostsSuffixes = cfg.client.dns.automapHostsSuffixes;
-        } // optionalAttrs (flatten (mapAttrsToList (n: h: h.clientAuthorizations) cfg.client.hiddenServices) != []) {
+        } // optionalAttrs (flatten (mapAttrsToList (n: o: o.clientAuthorizations) cfg.client.onionServices) != []) {
           ClientOnionAuthDir = runDir + "/ClientOnionAuthDir";
         }
       ))
@@ -896,7 +909,7 @@ in
 
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPorts =
-        map (o: optional (isInt o && o > 0 || o ? "port" && isInt o.port && o.port > 0) o.port)
+        concatMap (o: optional (isInt o && o > 0 || o ? "port" && isInt o.port && o.port > 0) o.port)
         (flatten [
           cfg.settings.ORPort
           cfg.settings.DirPort
@@ -917,54 +930,66 @@ in
         Group = "tor";
         ExecStartPre = [
           "${cfg.package}/bin/tor -f ${torrc} --verify-config"
-        ] ++
-        # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
-        [("+" + pkgs.writeShellScript "auth" (concatStringsSep "\n" (flatten (
-          mapAttrsToList (name: h:
-            optionals (h.authorizedClients != []) ([''
-              out="${h.path}/authorized_clients"
-              rm -rf "$out"
-              install -d -o tor -g tor -m 0700 "$out"
-            ''] ++ imap0 (i: pubKey: ''
-              echo ${pubKey} |
-              install -o tor -g tor -m 0400 /dev/stdin $out/${toString i}.auth
-            '') h.authorizedClients
-            )
-          ) cfg.relay.hiddenServices ++
-          mapAttrsToList (name: h: imap0 (i: prvKeyPath:
-            let onion = removeSuffix ".onion" name; in ''
-            printf "${onion}:" | cat - '${prvKeyPath}' |
-            install -o tor -g tor -m 0700 /dev/stdin \
-             ${runDir}/ClientOnionAuthDir/${onion}.${toString i}.auth_private
-          '') h.clientAuthorizations)
-          cfg.client.hiddenServices
-        ))))];
+          # DOC: Appendix G of https://spec.torproject.org/rend-spec-v3
+          ("+" + pkgs.writeShellScript "ExecStartPre" (concatStringsSep "\n" (flatten (["set -eu"] ++
+            mapAttrsToList (name: onion:
+              optional (onion.authorizedClients != []) ''
+                rm -rf '${onion.path}/authorized_clients'
+                install -d -o tor -g tor -m 0700 '${onion.path}' '${onion.path}/authorized_clients'
+              '' ++
+              imap0 (i: pubKey: ''
+                echo ${pubKey} |
+                install -o tor -g tor -m 0400 /dev/stdin '${onion.path}/authorized_clients/${toString i}.auth'
+              '') onion.authorizedClients ++
+              optional (onion.secretKey != null) ''
+                install -d -o tor -g tor -m 0700 '${onion.path}'
+                key="$(cut -f1 -d: '${onion.secretKey}')"
+                case "$key" in
+                 ("== ed25519v"*"-secret")
+                  install -o tor -g tor -m 0400 '${onion.secretKey}' '${onion.path}/hs_ed25519_secret_key';;
+                 (*) echo >&2 "NixOS does not (yet) support secret key type for onion: ${name}"; exit 1;;
+                esac
+              ''
+            ) cfg.relay.onionServices ++
+            mapAttrsToList (name: onion: imap0 (i: prvKeyPath:
+              let hostname = removeSuffix ".onion" name; in ''
+              printf "%s:" "${hostname}" | cat - '${prvKeyPath}' |
+              install -o tor -g tor -m 0700 /dev/stdin \
+               ${runDir}/ClientOnionAuthDir/${hostname}.${toString i}.auth_private
+            '') onion.clientAuthorizations)
+            cfg.client.onionServices
+          ))))
+        ];
         ExecStart = "${cfg.package}/bin/tor -f ${torrc}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         KillSignal = "SIGINT";
         TimeoutSec = 30;
         Restart = "on-failure";
         LimitNOFILE = 32768;
-        RuntimeDirectory = [ "tor" "tor/mnt-root" "tor/ClientOnionAuthDir" ];
-        RuntimeDirectoryMode = "0750";
+        RuntimeDirectory = [
+          # g+x allows access to the control socket
+          "tor"
+          "tor/mnt-root"
+          # g+x can't be removed in ExecStart=, but will be removed by Tor
+          "tor/ClientOnionAuthDir"
+        ];
+        RuntimeDirectoryMode = "0710";
         StateDirectoryMode = "0700";
-        StateDirectory =
-          [ "tor" "tor/onion" ] ++
-          mapAttrsToList (name: v: "tor/onion/${name}") cfg.relay.hiddenServices;
-
-        # The following are only to optimize:
+        StateDirectory = [
+            "tor"
+            "tor/onion"
+          ] ++
+          flatten (mapAttrsToList (name: onion:
+            optional (onion.secretKey == null) "tor/onion/${name}"
+          ) cfg.relay.onionServices);
+        # The following options are only to optimize:
         # systemd-analyze security tor
-        RootDirectory = rootDir;
+        RootDirectory = runDir + "/mnt-root";
         RootDirectoryStartOnly = true;
-        #InaccessiblePaths = [ "-+${rootDir}" ];
+        #InaccessiblePaths = [ "-+${runDir}/mnt-root" ];
         UMask = "0066";
-        BindPaths = [
-          stateDir
-        ];
-        BindReadOnlyPaths = [
-          storeDir
-          "/etc"
-        ];
+        BindPaths = [ stateDir ];
+        BindReadOnlyPaths = [ storeDir "/etc" ];
         AmbientCapabilities   = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
         CapabilityBoundingSet = [""] ++ lib.optional bindsPrivilegedPort "CAP_NET_BIND_SERVICE";
         # ProtectClock= adds DeviceAllow=char-rtc r