transmission: fix and improve the hardening
authorJulien Moutinho <julm@sourcephile.fr>
Wed, 15 Jul 2020 00:11:16 +0000 (02:11 +0200)
committerJulien Moutinho <julm@sourcephile.fr>
Wed, 15 Jul 2020 00:11:34 +0000 (02:11 +0200)
nixos/modules/services/torrent/transmission.nix

index 233de3aff56725de013b8d33b09eab0a6de4d7ea..4d16f67554aa2b306bf6d101eae5e65159a0345d 100644 (file)
@@ -4,6 +4,7 @@ with lib;
 
 let
   cfg = config.services.transmission;
+  inherit (config.environment) etc;
   apparmor = config.security.apparmor.enable;
   stateDir = "/var/lib/transmission";
   # TODO: switch to configGen.json once RFC0042 is implemented
@@ -44,6 +45,10 @@ in
             download-dir = "${cfg.home}/Downloads";
             incomplete-dir = "${cfg.home}/.incomplete";
             incomplete-dir-enabled = true;
+            peer-port = 51413;
+            peer-port-random-high = 65535;
+            peer-port-random-low = 49152;
+            peer-port-random-on-start = false;
             rpc-bind-address = "127.0.0.1";
             rpc-port = 9091;
             umask = 18; # 0o022 in decimal as expected by Transmission, obtained with: echo $((8#022))
@@ -56,8 +61,9 @@ in
             rpc-whitelist = "127.0.0.1,192.168.*.*";
           };
         description = ''
-          Attribute set whose fields overwrites fields in settings.json (each
-          time the service starts). String values must be quoted, integer and
+          Attribute set whose fields overwrites fields in
+          <literal>.config/transmission-daemon/settings.json</literal>
+          (each time the service starts). String values must be quoted, integer and
           boolean values must not.
 
           See https://github.com/transmission/transmission/wiki/Editing-Configuration-Files
@@ -71,13 +77,20 @@ in
         example = "775";
         description = ''
           The permissions set by the <literal>systemd-tmpfiles-setup</literal> service
-          on <literal>settings.download-dir</literal> and <literal>settings.incomplete-dir</literal>.
+          on <link linkend="opt-services.transmission.settings">settings.download-dir</link>
+          and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
         '';
       };
 
       port = mkOption {
         type = types.port;
-        description = "TCP port number to run the RPC/web interface.";
+        description = ''
+          TCP port number to run the RPC/web interface.
+
+          If instead you want to change the peer port,
+          use <link linkend="opt-services.transmission.settings">settings.peer-port</link>
+          or <link linkend="opt-services.transmission.settings">settings.peer-port-random-on-start</link>.
+        '';
       };
 
       home = mkOption {
@@ -85,8 +98,8 @@ in
         default = stateDir;
         description = ''
           The directory where Transmission will create <literal>.config/transmission-daemon/</literal>.
-          as well as <literal>Downloads/</literal> unless <literal>settings.download-dir</literal> is changed,
-          and <literal>.incomplete/</literal> unless <literal>settings.incomplete-dir</literal> is changed.
+          as well as <literal>Downloads/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
+          and <literal>.incomplete/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
         '';
       };
 
@@ -107,17 +120,17 @@ in
         description = ''
           Path to a JSON file to be merged with the settings.
           Useful to merge a file which is better kept out of the Nix store
-          because it contains sensible data like <literal>rpc-password</literal>.
+          because it contains sensible data like <link linkend="opt-services.transmission.settings">settings.rpc-password</link>.
         '';
         default = "/dev/null";
         example = "/var/lib/secrets/transmission/settings.json";
       };
 
-      enableSandbox = mkOption {
-        default = false;
+      openFirewall = mkOption {
         type = types.bool;
+        default = true;
         description = ''
-          Starting Transmission server with additional sandbox/hardening options.
+          Whether to automatically open the peer port(s) in the firewall.
         '';
       };
     };
@@ -151,6 +164,7 @@ in
       after = [ "network.target" ] ++ optional apparmor "apparmor.service";
       requires = optional apparmor "apparmor.service";
       wantedBy = [ "multi-user.target" ];
+      environment.CURL_CA_BUNDLE = etc."ssl/certs/ca-certificates.crt".source;
       preStart = ''
         set -eux
         ${pkgs.jq}/bin/jq --slurp add ${settingsFile} '${cfg.credentialsFile}' >'${stateDir}/${settingsDir}/settings.json'
@@ -162,34 +176,77 @@ in
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
         User = cfg.user;
         Group = cfg.group;
-        UMask = "0002";
         StateDirectory = removePrefix "/var/lib/" stateDir + "/" + settingsDir;
         StateDirectoryMode = "0700";
         BindPaths =
           optional (cfg.home != stateDir) "${cfg.home}/${settingsDir}:${stateDir}/${settingsDir}"
           ++ [ "${cfg.settings.download-dir}:${stateDir}/Downloads" ]
           ++ optional cfg.settings.incomplete-dir-enabled "${cfg.settings.incomplete-dir}:${stateDir}/.incomplete";
-        NoNewPrivileges = true;
-        AmbientCapabilities   = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
-        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" "CAP_SYS_RESOURCE" ];
-      } // optionalAttrs cfg.enableSandbox {
-        DevicePolicy = "closed";
+        # The following options give:
+        # systemd-analyze security transmission
+        # → Overall exposure level for transmission.service: 1.5 OK
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "";
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
         PrivateDevices = true;
         PrivateMounts = true;
+        PrivateNetwork = false;
         PrivateTmp = true;
+        PrivateUsers = false;
+        ProtectClock = true;
         ProtectControlGroups = true;
         ProtectHome = mkDefault true;
         ProtectHostname = true;
+        ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectSystem = mkDefault "strict";
         ReadWritePaths = [ stateDir ];
-        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RemoveIPC = true;
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
         RestrictRealtime = true;
         RestrictSUIDSGID = true;
+        # In case transmission crashes with status=31/SYS,
+        # having systemd.coredump.enable = true
+        # and environment.enableDebugInfo = true
+        # enables to use coredumpctl debug to find the denied syscall.
+        SystemCallFilter = [
+          "@default"
+          "@aio"
+          "@basic-io"
+          #"@chown"
+          #"@clock"
+          #"@cpu-emulation"
+          #"@debug"
+          "@file-system"
+          "@io-event"
+          #"@ipc"
+          #"@keyring"
+          #"@memlock"
+          #"@module"
+          #"@mount"
+          "@network-io"
+          #"@obsolete"
+          #"@pkey"
+          #"@privileged"
+            # Reached when querying infos through RPC (eg. with stig)
+            "quotactl"
+          "@process"
+          #"@raw-io"
+          #"@reboot"
+          #"@resources"
+          #"@setuid"
+          "@signal"
+          #"@swap"
+          "@sync"
+          "@system-service"
+          "@timer"
+        ];
         SystemCallArchitectures = "native";
+        UMask = "0077";
       };
     };
 
@@ -212,7 +269,29 @@ in
       };
     });
 
-    # AppArmor profile
+    networking.firewall = mkIf cfg.openFirewall (
+      if cfg.settings.peer-port-random-on-start
+      then
+        { allowedTCPPortRanges =
+            [ { from = cfg.settings.peer-port-random-low;
+                to   = cfg.settings.peer-port-random-high;
+              }
+            ];
+          allowedUDPPortRanges =
+            [ { from = cfg.settings.peer-port-random-low;
+                to   = cfg.settings.peer-port-random-high;
+              }
+            ];
+        }
+      else
+        { allowedTCPPorts = [ cfg.settings.peer-port ];
+          allowedUDPPorts = [ cfg.settings.peer-port ];
+        }
+    );
+
+    # You can add --Complain to apparmor_parser calls in services.apparmor's ExecStart=
+    # (because aa-complain is not working with the setup currently made by services.apparmor)
+    # then use journalctl -b --grep apparmor= to see denied accesses.
     security.apparmor.profiles = mkIf apparmor [
       (pkgs.writeText "apparmor-transmission-daemon" ''
         #include <tunables/global>
@@ -221,6 +300,15 @@ in
           #include <abstractions/base>
           #include <abstractions/nameservice>
 
+          # FIXME: these lines should be removed once <abstractions/base>
+          # has been fixed to fit NixOS.
+          ${etc."hosts".source} r,
+          /etc/ld-nix.so.preload r,
+          ${etc."ld-nix.so.preload".source} r,
+          ${concatMapStrings (p: optionalString (p != "") (p+" mr,\n"))
+            (splitString "\n" config.environment.etc."ld-nix.so.preload".text)}
+          ${etc."ssl/certs/ca-certificates.crt".source} r,
+
           ${getLib pkgs.glibc}/lib/*.so*                   mr,
           ${getLib pkgs.libevent}/lib/libevent*.so*        mr,
           ${getLib pkgs.curl}/lib/libcurl*.so*             mr,
@@ -244,9 +332,13 @@ in
           ${getLib pkgs.utillinuxMinimal.out}/lib/libuuid.so.* mr,
           ${getLib pkgs.gcc.cc.lib}/lib/libstdc++.so.* mr,
           ${getLib pkgs.gcc.cc.lib}/lib/libgcc_s.so.* mr,
+          ${pkgs.tzdata}/share/zoneinfo/** r,
 
           @{PROC}/sys/kernel/random/uuid   r,
           @{PROC}/sys/vm/overcommit_memory r,
+          @{PROC}/@{pid}/environ r,
+          @{PROC}/@{pid}/mounts r,
+          /tmp/tr_session_id_* rwk,
 
           ${pkgs.openssl.out}/etc/** r,
           ${pkgs.transmission}/share/transmission/** r,
@@ -262,4 +354,5 @@ in
     ];
   };
 
+  meta.maintainers = with lib.maintainers; [ julm ];
 }