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
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))
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
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 {
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.
'';
};
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.
'';
};
};
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'
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";
};
};
};
});
- # 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>
#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,
${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,
];
};
+ meta.maintainers = with lib.maintainers; [ julm ];
}