tor: improve type-checking and hardening (ter)
authorJulien Moutinho <julm@sourcephile.fr>
Mon, 14 Sep 2020 05:33:58 +0000 (07:33 +0200)
committerJulien Moutinho <julm@sourcephile.fr>
Mon, 14 Sep 2020 20:24:40 +0000 (22:24 +0200)
flake.lock
flake.nix
machines/losurdo/networking/tor.nix
machines/losurdo/nginx/sourcephile.fr/losurdo.nix
nixos/modules/services/security/tor.nix

index 0e380392f4916ec997cb5bb533e233607fa1e962..2af21cfe0c01c58ce5adf234fb91bbd01a8dcbcf 100644 (file)
@@ -34,7 +34,7 @@
     "pass": {
       "flake": false,
       "locked": {
-        "narHash": "sha256-b0q9B0mrSgizVJZROtXWUDM83g7fdDl2oQDS8P+vBho=",
+        "narHash": "sha256-/yEPnG21jtNzZRbyJLjkZ+PlqmmPOtJnyeXoknKnLKQ=",
         "path": "./pass",
         "type": "path"
       },
index 3cf7a5c33bfff1fb797d22470b378a506fe65e19..90e01ec757d9841afccb9973f642b17b07feb618 100644 (file)
--- a/flake.nix
+++ b/flake.nix
@@ -50,11 +50,11 @@ outputs = inputs: let
     }
     { meta.description = "tor: improve type-checking and hardening";
       url = "https://github.com/NixOS/nixpkgs/pull/97740.diff";
-      sha256 = "sha256-bOu1SxfHdPEUCxK0ntRFbeqXT3YtlaNbU2RN9Ly1jd0=";
+      sha256 = "sha256-ojmDxbGtfsvrUCtjkR7ZynayEGNDLwYl2Ea6yLOQpIk=";
     }
   ];
   localNixpkgsPatches = [
-    #nixpkgs/patches/apparmor.diff
+    #nixpkgs/patches/tor.diff
     #nixpkgs/patches/fix-ld-nix.diff
     #nixpkgs/patches/fix-ld-nix-apparmor.diff
   ];
index b0511370b64934df1565b36ede26aee46f2f16f2..2a526a19ed204a9acad646a9aeb7094fd59a95f0 100644 (file)
@@ -5,6 +5,7 @@ let
   inherit (config.users) users;
   inherit (config.security) gnupg;
   ports = lib.flatten (map (map (p: p.port)) [tor.settings.ORPort tor.settings.DirPort]);
+  onion = "5spcvlzbaxwo4knhwnrekjtnakxmvekmlc5qwsigi33rn45hd5gewlyd";
 in
 {
 environment.systemPackages = [
@@ -12,39 +13,37 @@ environment.systemPackages = [
   pkgs.nyx
 ];
 networking.nftables.ruleset = ''
-  add rule inet filter net2fw tcp dport {${lib.concatMapStringsSep "," toString ports}} counter accept comment "Tor"
   add rule inet filter fw2net meta skuid ${users.tor.name} meta l4proto tcp counter accept comment "Tor"
+'' + lib.optionalString (ports != []) ''
+  add rule inet filter net2fw tcp dport {${lib.concatMapStringsSep "," toString ports}} counter accept comment "Tor"
 '';
-systemd.services.tor-init.script = ''
-  install -d -m 700 -o tor -g tor /var/lib/tor/onion/${networking.domain}
-'';
-systemd.services.tor.serviceConfig.StateDirectoryMode = "0700";
 #security.gnupg.secrets."tor/auth/julm" = {};
+security.gnupg.secrets."tor/onion/${onion}/hs_ed25519_secret_key" = {};
 services.tor = {
   enable = true;
-  #enableGeoIP = false;
+  enableGeoIP = true;
   controlSocket.enable = true;
-  #controlPort = [9051];
-  relay.enable = true;
-  relay.role = "private-bridge";
   client.enable = true;
-  client.privoxy.enable = true;
-  /*
-  hiddenServices = {
-    "${networking.domain}/${networking.hostName}" = {
-      version = 3;
-      map = [
-        { port = 22; target = { addr = "127.0.0.1"; port = 22; }; }
+  relay.enable = false;
+  relay.role = "private-bridge";
+  #client.privoxy.enable = false;
+  relay.onionServices = {
+    "ssh/${networking.domain}/${networking.hostName}" = {
+      secretKey = gnupg.secrets."tor/onion/${onion}/hs_ed25519_secret_key".path;
+      map = [ 22 ];
+      /*
+      authorizedClients = [
+        "descriptor:x25519:2EZQ3AOZXERDVSN6WO5LNSCOIIPL2AT2A7KOS4ZIYNVQDR5EFM2Q" # julm
       ];
+      */
       settings = {
         #HiddenServiceNumIntroductionPoints = 20;
       };
     };
   };
-  */
   settings.ORPort = [
     #{ addr = "[::]"; port = 2222; NoAdvertise = false; IPv6Only = true; }
-    { port = 2222; NoAdvertise = false; IPv4Only = false; }
+    { port = 2222; IPv4Only = false; }
     #{ addr = "80.67.180.251"; port = 222; IPv4Only = true; NoAdvertise = true; }
   ];
   settings.BandwidthRate = 500000;
@@ -60,43 +59,38 @@ services.tor = {
     "reject [2001:6b0:e:2a18::119]"
     "reject [2600:3c02::f03c:91ff:fe59:7d2e]"
   ];
+  /*
   settings.TransPort = { port = 9040; };
   settings.DNSPort = { port = 9053; };
   #settings.ExtORPort = lib.mkForce null;
   settings.AutomapHostsOnResolve = true;
+  */
 };
-/*
-# copy your onion folder
+
 boot.initrd.secrets = {
-  "/etc/tor/onion/bootup" = /home/tony/tor/onion; # maybe find a better spot to store this.
+  "/var/lib/tor/onion/ssh/hs_ed25519_secret_key" =
+    gnupg.secrets."tor/onion/${onion}/hs_ed25519_secret_key".path;
 };
-
-# copy tor to you initrd
 boot.initrd.extraUtilsCommands = ''
   copy_bin_and_libs ${pkgs.tor}/bin/tor
 '';
-
-# start tor during boot process
 boot.initrd.network.postCommands = let
-  torRc = (pkgs.writeText "tor.rc" ''
-    DataDirectory /etc/tor
-    SOCKSPort 127.0.0.1:9050 IsolateDestAddr
-    SOCKSPort 127.0.0.1:9063
-    HiddenServiceDir /etc/tor/onion/bootup
-    HiddenServicePort 22 127.0.0.1:22
-  '');
-in ''
+  torRc = pkgs.writeText "tor.rc" ''
+    DataDirectory /var/lib/tor
+    HiddenServicePort 22 127.0.0.1:2222
+    HiddenServiceDir /var/lib/tor/onion/ssh
+  '';
+  in ''
   echo "tor: preparing onion folder"
   # have to do this otherwise tor does not want to start
-  chmod -R 700 /etc/tor
+  install -d -m 700 /var/lib/tor
 
   echo "make sure localhost is up"
-  ip a a 127.0.0.1/8 dev lo
+  ip addr add 127.0.0.1/8 dev lo
   ip link set lo up
 
   echo "tor: starting tor"
   tor -f ${torRc} --verify-config
   tor -f ${torRc} &
 '';
-*/
 }
index a0c1dd4334cd5a2c3836e7cba552159555cfc4ca..83f9ca3389d180904dad3236d01a7f7944f7ae55 100644 (file)
@@ -8,20 +8,24 @@ let
   onion = "dfc66yn2fundui5yvq2ndx4nmcmbxpho4ji32tlc4cncrjvs2b5yu4id";
 in
 {
-services.tor.relay.hiddenServices."${domain}/${srv}" = {
-  map = [
-    80
-    #{ port = 443; target = { port = 8443; }; }
-  ];
-  authorizedClients = [
-    "descriptor:x25519:2EZQ3AOZXERDVSN6WO5LNSCOIIPL2AT2A7KOS4ZIYNVQDR5EFM2Q" # julm
-  ];
-};
-services.tor.client.hiddenServices.${onion} = {
-  clientAuthorizations = [
-    gnupg.secrets."tor/auth/julm".path
-  ];
+services.tor = {
+  relay.onionServices."nginx/${domain}/${srv}" = {
+    secretKey = gnupg.secrets."tor/onion/${onion}/hs_ed25519_secret_key".path;
+    map = [
+      80
+      #{ port = 443; target = { port = 8443; }; }
+    ];
+    authorizedClients = [
+      "descriptor:x25519:2EZQ3AOZXERDVSN6WO5LNSCOIIPL2AT2A7KOS4ZIYNVQDR5EFM2Q" # julm
+    ];
+  };
+  client.onionServices.${onion} = {
+    clientAuthorizations = [
+      gnupg.secrets."tor/auth/julm".path
+    ];
+  };
 };
+security.gnupg.secrets."tor/onion/${onion}/hs_ed25519_secret_key" = {};
 security.gnupg.secrets."tor/auth/julm" = {};
 services.nginx = {
   virtualHosts."${srv}" = {
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