1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkIf
11 getExe
12 maintainers
13 mkEnableOption
14 mkOption
15 mkPackageOption
16 ;
17 inherit (lib.types) str path bool;
18 cfg = config.services.jellyfin;
19in
20{
21 options = {
22 services.jellyfin = {
23 enable = mkEnableOption "Jellyfin Media Server";
24
25 package = mkPackageOption pkgs "jellyfin" { };
26
27 user = mkOption {
28 type = str;
29 default = "jellyfin";
30 description = "User account under which Jellyfin runs.";
31 };
32
33 group = mkOption {
34 type = str;
35 default = "jellyfin";
36 description = "Group under which jellyfin runs.";
37 };
38
39 dataDir = mkOption {
40 type = path;
41 default = "/var/lib/jellyfin";
42 description = ''
43 Base data directory,
44 passed with `--datadir` see [#data-directory](https://jellyfin.org/docs/general/administration/configuration/#data-directory)
45 '';
46 };
47
48 configDir = mkOption {
49 type = path;
50 default = "${cfg.dataDir}/config";
51 defaultText = "\${cfg.dataDir}/config";
52 description = ''
53 Directory containing the server configuration files,
54 passed with `--configdir` see [configuration-directory](https://jellyfin.org/docs/general/administration/configuration/#configuration-directory)
55 '';
56 };
57
58 cacheDir = mkOption {
59 type = path;
60 default = "/var/cache/jellyfin";
61 description = ''
62 Directory containing the jellyfin server cache,
63 passed with `--cachedir` see [#cache-directory](https://jellyfin.org/docs/general/administration/configuration/#cache-directory)
64 '';
65 };
66
67 logDir = mkOption {
68 type = path;
69 default = "${cfg.dataDir}/log";
70 defaultText = "\${cfg.dataDir}/log";
71 description = ''
72 Directory where the Jellyfin logs will be stored,
73 passed with `--logdir` see [#log-directory](https://jellyfin.org/docs/general/administration/configuration/#log-directory)
74 '';
75 };
76
77 openFirewall = mkOption {
78 type = bool;
79 default = false;
80 description = ''
81 Open the default ports in the firewall for the media server. The
82 HTTP/HTTPS ports can be changed in the Web UI, so this option should
83 only be used if they are unchanged, see [Port Bindings](https://jellyfin.org/docs/general/networking/#port-bindings).
84 '';
85 };
86 };
87 };
88
89 config = mkIf cfg.enable {
90 systemd = {
91 tmpfiles.settings.jellyfinDirs = {
92 "${cfg.dataDir}"."d" = {
93 mode = "700";
94 inherit (cfg) user group;
95 };
96 "${cfg.configDir}"."d" = {
97 mode = "700";
98 inherit (cfg) user group;
99 };
100 "${cfg.logDir}"."d" = {
101 mode = "700";
102 inherit (cfg) user group;
103 };
104 "${cfg.cacheDir}"."d" = {
105 mode = "700";
106 inherit (cfg) user group;
107 };
108 };
109 services.jellyfin = {
110 description = "Jellyfin Media Server";
111 after = [ "network-online.target" ];
112 wants = [ "network-online.target" ];
113 wantedBy = [ "multi-user.target" ];
114
115 # This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service
116 # Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option
117 serviceConfig = {
118 Type = "simple";
119 User = cfg.user;
120 Group = cfg.group;
121 UMask = "0077";
122 WorkingDirectory = cfg.dataDir;
123 ExecStart = "${getExe cfg.package} --datadir '${cfg.dataDir}' --configdir '${cfg.configDir}' --cachedir '${cfg.cacheDir}' --logdir '${cfg.logDir}'";
124 Restart = "on-failure";
125 TimeoutSec = 15;
126 SuccessExitStatus = [
127 "0"
128 "143"
129 ];
130
131 # Security options:
132 NoNewPrivileges = true;
133 SystemCallArchitectures = "native";
134 # AF_NETLINK needed because Jellyfin monitors the network connection
135 RestrictAddressFamilies = [
136 "AF_UNIX"
137 "AF_INET"
138 "AF_INET6"
139 "AF_NETLINK"
140 ];
141 RestrictNamespaces = !config.boot.isContainer;
142 RestrictRealtime = true;
143 RestrictSUIDSGID = true;
144 ProtectControlGroups = !config.boot.isContainer;
145 ProtectHostname = true;
146 ProtectKernelLogs = !config.boot.isContainer;
147 ProtectKernelModules = !config.boot.isContainer;
148 ProtectKernelTunables = !config.boot.isContainer;
149 LockPersonality = true;
150 PrivateTmp = !config.boot.isContainer;
151 # needed for hardware acceleration
152 PrivateDevices = false;
153 PrivateUsers = true;
154 RemoveIPC = true;
155
156 SystemCallFilter = [
157 "~@clock"
158 "~@aio"
159 "~@chown"
160 "~@cpu-emulation"
161 "~@debug"
162 "~@keyring"
163 "~@memlock"
164 "~@module"
165 "~@mount"
166 "~@obsolete"
167 "~@privileged"
168 "~@raw-io"
169 "~@reboot"
170 "~@setuid"
171 "~@swap"
172 ];
173 SystemCallErrorNumber = "EPERM";
174 };
175 };
176 };
177
178 users.users = mkIf (cfg.user == "jellyfin") {
179 jellyfin = {
180 inherit (cfg) group;
181 isSystemUser = true;
182 };
183 };
184
185 users.groups = mkIf (cfg.group == "jellyfin") {
186 jellyfin = { };
187 };
188
189 networking.firewall = mkIf cfg.openFirewall {
190 # from https://jellyfin.org/docs/general/networking/index.html
191 allowedTCPPorts = [
192 8096
193 8920
194 ];
195 allowedUDPPorts = [
196 1900
197 7359
198 ];
199 };
200
201 };
202
203 meta.maintainers = with maintainers; [
204 minijackson
205 fsnkty
206 ];
207}