apparmor: temporary merge nixpkgs#101071 and nixpkgs#96655
authorJulien Moutinho <julm@sourcephile.fr>
Mon, 7 Dec 2020 14:01:49 +0000 (15:01 +0100)
committerJulien Moutinho <julm@sourcephile.fr>
Mon, 7 Dec 2020 14:02:14 +0000 (15:02 +0100)
flake.nix
nixos/modules.nix
nixpkgs/patches.nix
nixpkgs/patches/apparmor.diff

index f2fae2a2eaa95a91c9472114a96649b695fcb3e8..3a8bc7100a3d58e1b2c938e90effd26c64aa09f4 100644 (file)
--- a/flake.nix
+++ b/flake.nix
@@ -9,7 +9,7 @@ inputs.pass = { type = "path"; path = "./pass"; flake = false; };
 outputs = inputs: let
   remoteNixpkgsPatches = import nixpkgs/patches.nix;
   localNixpkgsPatches = [
-    #nixpkgs/patches/apparmor.diff
+    nixpkgs/patches/apparmor.diff
     #nixpkgs/patches/public-inbox.diff
     #nixpkgs/patches/zerobin.diff
     #nixpkgs/patches/gitolite.diff
index 6c66e1a426d75b7a560baf0f93d7eaee0d32e43d..148b8eed4d138fbdb5962aafc7610f9f794181f4 100644 (file)
@@ -7,7 +7,7 @@ imports = [
   #modules/services/mail/public-inbox.nix
   #modules/services/security/tor.nix
   #modules/services/backup/syncoid.nix
-  modules/services/torrent/transmission.nix
+  #modules/services/torrent/transmission.nix
   #modules/security/gnupg.nix
   #modules/services/networking/biboumi.nix
   #modules/services/networking/croc.nix
@@ -30,7 +30,7 @@ disabledModules = [
   #"services/networking/croc.nix"
   "services/networking/netns.nix"
   "services/networking/openvpn.nix"
-  "services/torrent/transmission.nix"
+  #"services/torrent/transmission.nix"
   #"services/games/freeciv.nix"
 ];
 }
index 7b61630b36e8e4a622976029cac5b9f1ace7d310..7a2f37cfe551ff027dd312be954f1888f17dbee9 100644 (file)
   url = "https://github.com/NixOS/nixpkgs/pull/94938.diff";
   sha256 = "sha256-BDD5f5UjtBPnBYj5pcI+Yo01g2HHgn8btvbgfTnjcoE=";
 }
+/*
 {
   meta.description = "transmission: use freeformType on settings";
   url = "https://github.com/NixOS/nixpkgs/pull/96655.diff";
   sha256 = "sha256-lRKe8WnGPc8ojaD9W8ZS+NVMhIoGOCOj9njhqQhzaCM=";
 }
+*/
 {
   meta.description = "nixos/tor: improve type-checking and hardening";
   url = "https://github.com/NixOS/nixpkgs/pull/97740.diff";
@@ -63,7 +65,7 @@
 {
   meta.description = "apparmor: try again to fix and improve";
   url = "https://github.com/NixOS/nixpkgs/pull/101071.diff";
-  sha256 = "sha256-/H23K8Cfkyy21xN8Vl8ylc+fcHJxCNFQok3GqAYXDbU=";
+  sha256 = "sha256-HCoCyCHXoSFo2XxPPqMcXLzj3BayrkhDzOZXixYg420=";
 }
 {
   meta.description = "Update public-inbox to 1.6.0 and add systemd services";
@@ -73,6 +75,6 @@
 {
   meta.description = "Add freeciv service";
   url = "https://github.com/NixOS/nixpkgs/pull/104460.diff";
-  sha256 = "sha256-HU6+Pq6WqVKdpnoRBU9FUneunZGc2q+STPAQR5kUS5o=";
+  sha256 = "sha256-O5WYGtGcDbGb/OXccwy8IWeqigA0E4abP4/plOhUFUw=";
 }
 ]
index 099366870339a4c43bbe930f343c28df59521aab..cc0f039eec5b1b532e327e02638eec346511f168 100644 (file)
-diff --git a/pkgs/os-specific/linux/apparmor/default.nix b/pkgs/os-specific/linux/apparmor/default.nix
-index da8cfac3e07..c6c72cc4e52 100644
---- a/pkgs/os-specific/linux/apparmor/default.nix
-+++ b/pkgs/os-specific/linux/apparmor/default.nix
-@@ -20,8 +20,8 @@
+diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
+index e9b5834dab4..779924a65a8 100644
+--- a/nixos/modules/services/torrent/transmission.nix
++++ b/nixos/modules/services/torrent/transmission.nix
+@@ -7,15 +7,20 @@ let
+   inherit (config.environment) etc;
+   apparmor = config.security.apparmor;
+   rootDir = "/run/transmission";
+-  homeDir = "/var/lib/transmission";
+   settingsDir = ".config/transmission-daemon";
+   downloadsDir = "Downloads";
+   incompleteDir = ".incomplete";
+   watchDir = "watchdir";
+-  # TODO: switch to configGen.json once RFC0042 is implemented
+-  settingsFile = pkgs.writeText "settings.json" (builtins.toJSON cfg.settings);
++  settingsFormat = pkgs.formats.json {};
++  settingsFile = settingsFormat.generate "settings.json" cfg.settings;
+ in
+ {
++  imports = [
++    (mkRenamedOptionModule ["services" "transmission" "port"]
++                           ["services" "transmission" "settings" "rpc-port"])
++    (mkAliasOptionModule ["services" "transmission" "openFirewall"]
++                         ["services" "transmission" "openPeerPorts"])
++  ];
+   options = {
+     services.transmission = {
+       enable = mkEnableOption ''the headless Transmission BitTorrent daemon.
+@@ -24,48 +29,141 @@ in
+         transmission-remote, the WebUI (http://127.0.0.1:9091/ by default),
+         or other clients like stig or tremc.
+-        Torrents are downloaded to ${homeDir}/${downloadsDir} by default and are
++        Torrents are downloaded to <xref linkend="opt-services.transmission.home"/>/${downloadsDir} by default and are
+         accessible to users in the "transmission" group'';
+-      settings = mkOption rec {
+-        # TODO: switch to types.config.json as prescribed by RFC0042 once it's implemented
+-        type = types.attrs;
+-        apply = recursiveUpdate default;
+-        default =
+-          {
+-            download-dir = "${cfg.home}/${downloadsDir}";
+-            incomplete-dir = "${cfg.home}/${incompleteDir}";
+-            incomplete-dir-enabled = true;
+-            watch-dir = "${cfg.home}/${watchDir}";
+-            watch-dir-enabled = false;
+-            message-level = 1;
+-            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;
+-            script-torrent-done-enabled = false;
+-            script-torrent-done-filename = "";
+-            umask = 2; # 0o002 in decimal as expected by Transmission
+-            utp-enabled = true;
+-          };
+-        example =
+-          {
+-            download-dir = "/srv/torrents/";
+-            incomplete-dir = "/srv/torrents/.incomplete/";
+-            incomplete-dir-enabled = true;
+-            rpc-whitelist = "127.0.0.1,192.168.*.*";
+-          };
++      settings = mkOption {
+         description = ''
+-          Attribute set whose fields overwrites fields in
++          Settings whose options overwrite 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.
++          (each time the service starts).
+           See <link xlink:href="https://github.com/transmission/transmission/wiki/Editing-Configuration-Files">Transmission's Wiki</link>
+-          for documentation.
++          for documentation of settings not explicitely covered by this module.
+         '';
++        default = {};
++        type = types.submodule {
++          freeformType = settingsFormat.type;
++          options.download-dir = mkOption {
++            type = types.path;
++            default = "${cfg.home}/${downloadsDir}";
++            description = "Directory where to download torrents.";
++          };
++          options.incomplete-dir = mkOption {
++            type = types.path;
++            default = "${cfg.home}/${incompleteDir}";
++            description = ''
++              When enabled with
++              services.transmission.home
++              <xref linkend="opt-services.transmission.settings.incomplete-dir-enabled"/>,
++              new torrents will download the files to this directory.
++              When complete, the files will be moved to download-dir
++              <xref linkend="opt-services.transmission.settings.download-dir"/>.
++            '';
++          };
++          options.incomplete-dir-enabled = mkOption {
++            type = types.bool;
++            default = true;
++            description = "";
++          };
++          options.message-level = mkOption {
++            type = types.ints.between 0 2;
++            default = 2;
++            description = "Set verbosity of transmission messages.";
++          };
++          options.peer-port = mkOption {
++            type = types.port;
++            default = 51413;
++            description = "The peer port to listen for incoming connections.";
++          };
++          options.peer-port-random-high = mkOption {
++            type = types.port;
++            default = 65535;
++            description = ''
++              The maximum peer port to listen to for incoming connections
++              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
++            '';
++          };
++          options.peer-port-random-low = mkOption {
++            type = types.port;
++            default = 65535;
++            description = ''
++              The minimal peer port to listen to for incoming connections
++              when <xref linkend="opt-services.transmission.settings.peer-port-random-on-start"/> is enabled.
++            '';
++          };
++          options.peer-port-random-on-start = mkOption {
++            type = types.bool;
++            default = false;
++            description = "Randomize the peer port.";
++          };
++          options.rpc-bind-address = mkOption {
++            type = types.str;
++            default = "127.0.0.1";
++            example = "0.0.0.0";
++            description = ''
++              Where to listen for RPC connections.
++              Use \"0.0.0.0\" to listen on all interfaces.
++            '';
++          };
++          options.rpc-port = mkOption {
++            type = types.port;
++            default = 9091;
++            description = "The RPC port to listen to.";
++          };
++          options.script-torrent-done-enabled = mkOption {
++            type = types.bool;
++            default = false;
++            description = ''
++              Whether to run
++              <xref linkend="opt-services.transmission.settings.script-torrent-done-filename"/>
++              at torrent completion.
++            '';
++          };
++          options.script-torrent-done-filename = mkOption {
++            type = types.nullOr types.path;
++            default = null;
++            description = "Executable to be run at torrent completion.";
++          };
++          options.umask = mkOption {
++            type = types.int;
++            default = 2;
++            description = ''
++              Sets transmission's file mode creation mask.
++              See the umask(2) manpage for more information.
++              Users who want their saved torrents to be world-writable
++              may want to set this value to 0.
++              Bear in mind that the json markup language only accepts numbers in base 10,
++              so the standard umask(2) octal notation "022" is written in settings.json as 18.
++            '';
++          };
++          options.utp-enabled = mkOption {
++            type = types.bool;
++            default = true;
++            description = ''
++              Whether to enable <link xlink:href="http://en.wikipedia.org/wiki/Micro_Transport_Protocol">Micro Transport Protocol (µTP)</link>.
++            '';
++          };
++          options.watch-dir = mkOption {
++            type = types.path;
++            default = "${cfg.home}/${watchDir}";
++            description = "Watch a directory for torrent files and add them to transmission.";
++          };
++          options.watch-dir-enabled = mkOption {
++            type = types.bool;
++            default = false;
++            description = ''Whether to enable the
++              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
++            '';
++          };
++          options.trash-original-torrent-files = mkOption {
++            type = types.bool;
++            default = false;
++            description = ''Whether to delete torrents added from the
++              <xref linkend="opt-services.transmission.settings.watch-dir"/>.
++            '';
++          };
++        };
+       };
+       downloadDirPermissions = mkOption {
+@@ -74,31 +172,22 @@ in
+         example = "775";
+         description = ''
+           The permissions set by <literal>systemd.activationScripts.transmission-daemon</literal>
+-          on the directories <link linkend="opt-services.transmission.settings">settings.download-dir</link>
+-          and <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link>.
++          on the directories <xref linkend="opt-services.transmission.settings.download-dir"/>
++          and <xref linkend="opt-services.transmission.settings.incomplete-dir"/>.
+           Note that you may also want to change
+-          <link linkend="opt-services.transmission.settings">settings.umask</link>.
+-        '';
+-      };
+-
+-      port = mkOption {
+-        type = types.port;
+-        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>.
++          <xref linkend="opt-services.transmission.settings.umask"/>.
+         '';
+       };
+       home = mkOption {
+         type = types.path;
+-        default = homeDir;
++        default = "/var/lib/transmission";
+         description = ''
+           The directory where Transmission will create <literal>${settingsDir}</literal>.
+-          as well as <literal>${downloadsDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.download-dir</link> is changed,
+-          and <literal>${incompleteDir}/</literal> unless <link linkend="opt-services.transmission.settings">settings.incomplete-dir</link> is changed.
++          as well as <literal>${downloadsDir}/</literal> unless
++          <xref linkend="opt-services.transmission.settings.download-dir"/> is changed,
++          and <literal>${incompleteDir}/</literal> unless
++          <xref linkend="opt-services.transmission.settings.incomplete-dir"/> is changed.
+         '';
+       };
+@@ -119,19 +208,22 @@ 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 <link linkend="opt-services.transmission.settings">settings.rpc-password</link>.
++          because it contains sensible data like
++          <xref linkend="opt-services.transmission.settings.rpc-password"/>.
+         '';
+         default = "/dev/null";
+         example = "/var/lib/secrets/transmission/settings.json";
+       };
+-      openFirewall = mkEnableOption "opening of the peer port(s) in the firewall";
++      openPeerPorts = mkEnableOption "opening of the peer port(s) in the firewall";
++
++      openRPCPort = mkEnableOption "opening of the RPC port in the firewall";
+       performanceNetParameters = mkEnableOption ''tweaking of kernel parameters
+         to open many more connections at the same time.
+         Note that you may also want to increase
+-        <link linkend="opt-services.transmission.settings">settings.peer-limit-global</link>.
++        <xref linkend="opt-services.transmission.settings.peer-limit-global"/>.
+         And be aware that these settings are quite aggressive
+         and might not suite your regular desktop use.
+         For instance, SSH sessions may time out more easily'';
+@@ -152,36 +244,10 @@ in
+       install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.download-dir}'
+       '' + optionalString cfg.settings.incomplete-dir-enabled ''
+       install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.incomplete-dir}'
++      '' + optionalString cfg.settings.watch-dir-enabled ''
++      install -d -m '${cfg.downloadDirPermissions}' -o '${cfg.user}' -g '${cfg.group}' '${cfg.settings.watch-dir}'
+       '';
+-    assertions = [
+-      { assertion = builtins.match "^/.*" cfg.home != null;
+-        message = "`services.transmission.home' must be an absolute path.";
+-      }
+-      { assertion = types.path.check cfg.settings.download-dir;
+-        message = "`services.transmission.settings.download-dir' must be an absolute path.";
+-      }
+-      { assertion = types.path.check cfg.settings.incomplete-dir;
+-        message = "`services.transmission.settings.incomplete-dir' must be an absolute path.";
+-      }
+-      { assertion = types.path.check cfg.settings.watch-dir;
+-        message = "`services.transmission.settings.watch-dir' must be an absolute path.";
+-      }
+-      { assertion = cfg.settings.script-torrent-done-filename == "" || types.path.check cfg.settings.script-torrent-done-filename;
+-        message = "`services.transmission.settings.script-torrent-done-filename' must be an absolute path.";
+-      }
+-      { assertion = types.port.check cfg.settings.rpc-port;
+-        message = "${toString cfg.settings.rpc-port} is not a valid port number for `services.transmission.settings.rpc-port`.";
+-      }
+-      # In case both port and settings.rpc-port are explicitely defined: they must be the same.
+-      { assertion = !options.services.transmission.port.isDefined || cfg.port == cfg.settings.rpc-port;
+-        message = "`services.transmission.port' is not equal to `services.transmission.settings.rpc-port'";
+-      }
+-    ];
+-
+-    services.transmission.settings =
+-      optionalAttrs options.services.transmission.port.isDefined { rpc-port = cfg.port; };
+-
+     systemd.services.transmission = {
+       description = "Transmission BitTorrent Service";
+       after = [ "network.target" ] ++ optional apparmor.enable "apparmor.service";
+@@ -226,11 +292,9 @@ in
+             cfg.settings.download-dir
+           ] ++
+           optional cfg.settings.incomplete-dir-enabled
+-            cfg.settings.incomplete-dir
+-          ++
+-          optional cfg.settings.watch-dir-enabled
+-            cfg.settings.watch-dir
+-          ;
++            cfg.settings.incomplete-dir ++
++          optional (cfg.settings.watch-dir-enabled && cfg.settings.trash-original-torrent-files)
++            cfg.settings.watch-dir;
+         BindReadOnlyPaths = [
+           # No confinement done of /nix/store here like in systemd-confinement.nix,
+           # an AppArmor profile is provided to get a confinement based upon paths and rights.
+@@ -239,8 +303,10 @@ in
+           "/run"
+           ] ++
+           optional (cfg.settings.script-torrent-done-enabled &&
+-                    cfg.settings.script-torrent-done-filename != "")
+-            cfg.settings.script-torrent-done-filename;
++                    cfg.settings.script-torrent-done-filename != null)
++            cfg.settings.script-torrent-done-filename ++
++          optional (cfg.settings.watch-dir-enabled && !cfg.settings.trash-original-torrent-files)
++            cfg.settings.watch-dir;
+         # The following options are only for optimizing:
+         # systemd-analyze security transmission
+         AmbientCapabilities = "";
+@@ -307,25 +373,28 @@ in
+       };
+     });
+-    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 ];
+-        }
+-    );
++    networking.firewall = mkMerge [
++      (mkIf cfg.openPeerPorts (
++        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 ];
++          }
++      ))
++      (mkIf cfg.openRPCPort { allowedTCPPorts = [ cfg.settings.rpc-port ]; })
++    ];
+     boot.kernel.sysctl = mkMerge [
+       # Transmission uses a single UDP socket in order to implement multiple uTP sockets,
+@@ -340,74 +409,57 @@ in
+         # Increase the number of available source (local) TCP and UDP ports to 49151.
+         # Usual default is 32768 60999, ie. 28231 ports.
+         # Find out your current usage with: ss -s
+-        "net.ipv4.ip_local_port_range" = "16384 65535";
++        "net.ipv4.ip_local_port_range" = mkDefault "16384 65535";
+         # Timeout faster generic TCP states.
+         # Usual default is 600.
+         # Find out your current usage with: watch -n 1 netstat -nptuo
+-        "net.netfilter.nf_conntrack_generic_timeout" = 60;
++        "net.netfilter.nf_conntrack_generic_timeout" = mkDefault 60;
+         # Timeout faster established but inactive connections.
+         # Usual default is 432000.
+-        "net.netfilter.nf_conntrack_tcp_timeout_established" = 600;
++        "net.netfilter.nf_conntrack_tcp_timeout_established" = mkDefault 600;
+         # Clear immediately TCP states after timeout.
+         # Usual default is 120.
+-        "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = 1;
++        "net.netfilter.nf_conntrack_tcp_timeout_time_wait" = mkDefault 1;
+         # Increase the number of trackable connections.
+         # Usual default is 262144.
+         # Find out your current usage with: conntrack -C
+-        "net.netfilter.nf_conntrack_max" = 1048576;
++        "net.netfilter.nf_conntrack_max" = mkDefault 1048576;
+       })
+     ];
+     security.apparmor.policies."bin.transmission-daemon".profile = ''
+-        include <tunables/global>
+-        ${pkgs.transmission}/bin/transmission-daemon {
+-          include <abstractions/base>
+-          include <abstractions/nameservice>
+-          include <abstractions/ssl_certs>
+-          include "${pkgs.apparmorRulesFromClosure
+-            { name = "transmission-daemon"; }
+-            [ pkgs.transmission ]}"
+-          include <local/bin.transmission-daemon>
+-
+-          r @{PROC}/sys/kernel/random/uuid,
+-          r @{PROC}/sys/vm/overcommit_memory,
+-          r @{PROC}/@{pid}/environ,
+-          r @{PROC}/@{pid}/mounts,
+-          rwk /tmp/tr_session_id_*,
+-          r /run/systemd/resolve/stub-resolv.conf,
+-
+-          r ${pkgs.openssl.out}/etc/**,
+-          r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
+-
+-          owner rw ${cfg.home}/${settingsDir}/**,
+-          rw ${cfg.settings.download-dir}/**,
+-          ${optionalString cfg.settings.incomplete-dir-enabled ''
+-            rw ${cfg.settings.incomplete-dir}/**,
+-          ''}
+-          ${optionalString cfg.settings.watch-dir-enabled ''
+-            rw ${cfg.settings.watch-dir}/**,
+-          ''}
+-          profile dirs {
+-            rw ${cfg.settings.download-dir}/**,
+-            ${optionalString cfg.settings.incomplete-dir-enabled ''
+-              rw ${cfg.settings.incomplete-dir}/**,
+-            ''}
+-            ${optionalString cfg.settings.watch-dir-enabled ''
+-              rw ${cfg.settings.watch-dir}/**,
+-            ''}
+-          }
+-
+-          ${optionalString (cfg.settings.script-torrent-done-enabled &&
+-                            cfg.settings.script-torrent-done-filename != "") ''
+-            # Stack transmission_directories profile on top of
+-            # any existing profile for script-torrent-done-filename
+-            # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
+-            # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
+-            px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
+-          ''}
+-        }
++      include "${pkgs.transmission.apparmor}/bin.transmission-daemon"
++    '';
++    security.apparmor.includes."local/bin.transmission-daemon" = ''
++      r ${config.systemd.services.transmission.environment.CURL_CA_BUNDLE},
++
++      owner rw ${cfg.home}/${settingsDir}/**,
++      rw ${cfg.settings.download-dir}/**,
++      ${optionalString cfg.settings.incomplete-dir-enabled ''
++        rw ${cfg.settings.incomplete-dir}/**,
++      ''}
++      ${optionalString cfg.settings.watch-dir-enabled ''
++        r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
++      ''}
++      profile dirs {
++        rw ${cfg.settings.download-dir}/**,
++        ${optionalString cfg.settings.incomplete-dir-enabled ''
++          rw ${cfg.settings.incomplete-dir}/**,
++        ''}
++        ${optionalString cfg.settings.watch-dir-enabled ''
++          r${optionalString cfg.settings.trash-original-torrent-files "w"} ${cfg.settings.watch-dir}/**,
++        ''}
++      }
++
++      ${optionalString (cfg.settings.script-torrent-done-enabled &&
++                        cfg.settings.script-torrent-done-filename != null) ''
++        # Stack transmission_directories profile on top of
++        # any existing profile for script-torrent-done-filename
++        # FIXME: to be tested as I'm not sure it works well with NoNewPrivileges=
++        # https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorStacking#seccomp-and-no_new_privs
++        px ${cfg.settings.script-torrent-done-filename} -> &@{dirs},
++      ''}
+     '';
+-    security.apparmor.includes."local/bin.transmission-daemon" = "";
+   };
+   meta.maintainers = with lib.maintainers; [ julm ];
+diff --git a/pkgs/applications/networking/p2p/transmission/default.nix b/pkgs/applications/networking/p2p/transmission/default.nix
+index ab4fc0908ba..858b09c9aaa 100644
+--- a/pkgs/applications/networking/p2p/transmission/default.nix
++++ b/pkgs/applications/networking/p2p/transmission/default.nix
+@@ -20,6 +20,7 @@
+ , enableSystemd ? stdenv.isLinux
+ , enableDaemon ? true
+ , enableCli ? true
++, apparmorRulesFromClosure
  }:
  
  let
--  apparmor-series = "2.13";
--  apparmor-patchver = "5";
-+  apparmor-series = "3.0";
-+  apparmor-patchver = "0";
-   apparmor-version = apparmor-series + "." + apparmor-patchver;
-   apparmor-meta = component: with stdenv.lib; {
-@@ -33,8 +33,8 @@ let
+@@ -37,6 +38,8 @@ in stdenv.mkDerivation {
+     fetchSubmodules = true;
    };
  
-   apparmor-sources = fetchurl {
--    url = "https://launchpad.net/apparmor/${apparmor-series}/${apparmor-version}/+download/apparmor-${apparmor-version}.tar.gz";
--    sha256 = "05x7r99k00r97v1cq2f711lv6yqzhbl8zp1i1c7kxra4v0a2lzk3";
-+    url = "https://launchpad.net/apparmor/${apparmor-series}/${apparmor-series}/+download/apparmor-${apparmor-version}.tar.gz";
-+    sha256 = "0pkm8f619c0ra8kpjmarzl9d409dn4sy0kl6mb92gd0ywlgpbzb6";
-   };
++  outputs = [ "out" "apparmor" ];
++
+   cmakeFlags =
+     let
+       mkFlag = opt: if opt then "ON" else "OFF";
+@@ -72,6 +75,30 @@ in stdenv.mkDerivation {
+   NIX_LDFLAGS = lib.optionalString stdenv.isDarwin "-framework CoreFoundation";
  
-   aa-teardown = writeShellScript "aa-teardown" ''
-@@ -146,7 +146,7 @@ let
-     postInstall = ''
-       sed -i $out/bin/aa-unconfined -e "/my_env\['PATH'\]/d"
--      for prog in aa-audit aa-autodep aa-cleanprof aa-complain aa-disable aa-enforce aa-genprof aa-logprof aa-mergeprof aa-status aa-unconfined ; do
-+      for prog in aa-audit aa-autodep aa-cleanprof aa-complain aa-disable aa-enforce aa-genprof aa-logprof aa-mergeprof aa-unconfined ; do
-         wrapProgram $out/bin/$prog --prefix PYTHONPATH : "$out/lib/${python.libPrefix}/site-packages:$PYTHONPATH"
-       done
-@@ -156,8 +156,6 @@ let
-       makeWrapper ${perl}/bin/perl $out/bin/aa-notify --set PERL5LIB ${libapparmor}/${perl.libPrefix} --add-flags $out/bin/aa-notify-wrapped
-       substituteInPlace $out/bin/aa-remove-unknown \
--       --replace "/usr/bin/aa-status" "$out/bin/aa-status" \
--       --replace "/sbin/modprobe" "${kmod}/bin/modprobe" \
-        --replace "/lib/apparmor/rc.apparmor.functions" "${apparmor-parser}/lib/apparmor/rc.apparmor.functions"
-       wrapProgram $out/bin/aa-remove-unknown \
-        --prefix PATH : ${lib.makeBinPath [gawk]}
-@@ -190,7 +188,7 @@ let
-     prePatch = prePatchCommon;
-     postPatch = "cd ./binutils";
-     makeFlags = [ "LANGS=" "USE_SYSTEM=1" ];
--    installFlags = [ "DESTDIR=$(out)" "BINDIR=$(out)/bin" ];
-+    installFlags = [ "DESTDIR=$(out)" "BINDIR=$(out)/bin" "SBINDIR=$(out)/bin" ];
-     inherit doCheck;
-@@ -294,7 +292,8 @@ let
-         # eg. glibc-2.30/lib/gconv/gconv-modules
-         "r $path/lib/**"
-       ]
--    }: rootPaths: runCommand "apparmor-closure-rules" {} ''
-+    , name ? ""
-+    }: rootPaths: runCommand "apparmor-closure-rules${optionalString (name != "") "-${name}"}" {} ''
-     touch $out
-     while read -r path
-     do printf >>$out "%s,\n" ${lib.concatMapStringsSep " " (x: "\"${x}\"") (baseRules ++ additionalRules)}
++  postInstall = ''
++    install -D /dev/stdin $apparmor/bin.transmission-daemon <<EOF
++    include <tunables/global>
++    $out/bin/transmission-daemon {
++      include <abstractions/base>
++      include <abstractions/nameservice>
++      include <abstractions/ssl_certs>
++      include "${apparmorRulesFromClosure { name = "transmission-daemon"; } ([
++        curl libevent openssl pcre zlib
++      ] ++ lib.optionals enableSystemd [ systemd ]
++        ++ lib.optionals stdenv.isLinux [ inotify-tools ]
++      )}"
++      r @{PROC}/sys/kernel/random/uuid,
++      r @{PROC}/sys/vm/overcommit_memory,
++      r @{PROC}/@{pid}/environ,
++      r @{PROC}/@{pid}/mounts,
++      rwk /tmp/tr_session_id_*,
++      r /run/systemd/resolve/stub-resolv.conf,
++
++      include <local/bin.transmission-daemon>
++    }
++    EOF
++  '';
++
+   meta = {
+     description = "A fast, easy and free BitTorrent client";
+     longDescription = ''