vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / invidious.nix
blobe4223cf1e8b127acf5d11182c36adb8f576ae97c
1 { lib, config, pkgs, options, ... }:
2 let
3   cfg = config.services.invidious;
4   # To allow injecting secrets with jq, json (instead of yaml) is used
5   settingsFormat = pkgs.formats.json { };
6   inherit (lib) types;
8   settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
10   generatedHmacKeyFile = "/var/lib/invidious/hmac_key";
11   generateHmac = cfg.hmacKeyFile == null;
13   commonInvidousServiceConfig = {
14     description = "Invidious (An alternative YouTube front-end)";
15     wants = [ "network-online.target" ];
16     after = [ "network-online.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service";
17     requires = lib.optional cfg.database.createLocally "postgresql.service";
18     wantedBy = [ "multi-user.target" ];
20     serviceConfig = {
21       RestartSec = "2s";
22       DynamicUser = true;
23       User = lib.mkIf (cfg.database.createLocally || cfg.serviceScale > 1) "invidious";
24       StateDirectory = "invidious";
25       StateDirectoryMode = "0750";
27       CapabilityBoundingSet = "";
28       PrivateDevices = true;
29       PrivateUsers = true;
30       ProtectHome = true;
31       ProtectKernelLogs = true;
32       ProtectProc = "invisible";
33       RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
34       RestrictNamespaces = true;
35       SystemCallArchitectures = "native";
36       SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
38       # Because of various issues Invidious must be restarted often, at least once a day, ideally
39       # every hour.
40       # This option enables the automatic restarting of the Invidious instance.
41       # To ensure multiple instances of Invidious are not restarted at the exact same time, a
42       # randomized extra offset of up to 5 minutes is added.
43       Restart = lib.mkDefault "always";
44       RuntimeMaxSec = lib.mkDefault "1h";
45       RuntimeRandomizedExtraSec = lib.mkDefault "5min";
46     };
47   };
48   mkInvidiousService = scaleIndex:
49     lib.foldl' lib.recursiveUpdate commonInvidousServiceConfig [
50       # only generate the hmac file in the first service
51       (lib.optionalAttrs (scaleIndex == 0) {
52         preStart = lib.optionalString generateHmac ''
53           if [[ ! -e "${generatedHmacKeyFile}" ]]; then
54             ${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}"
55             chmod 0600 "${generatedHmacKeyFile}"
56           fi
57         '';
58       })
59       # configure the secondary services to run after the first service
60       (lib.optionalAttrs (scaleIndex > 0) {
61         after = commonInvidousServiceConfig.after ++ [ "invidious.service" ];
62         wants = commonInvidousServiceConfig.wants ++ [ "invidious.service" ];
63       })
64       {
65         script = ''
66           configParts=()
67         ''
68         # autogenerated hmac_key
69         + lib.optionalString generateHmac ''
70           configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")")
71         ''
72         # generated settings file
73         + ''
74           configParts+=("$(< ${lib.escapeShellArg settingsFile})")
75         ''
76         # optional database password file
77         + lib.optionalString (cfg.database.host != null) ''
78           configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})")
79         ''
80         # optional extra settings file
81         + lib.optionalString (cfg.extraSettingsFile != null) ''
82           configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})")
83         ''
84         # explicitly specified hmac key file
85         + lib.optionalString (cfg.hmacKeyFile != null) ''
86           configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})")
87         ''
88         # configure threads for secondary instances
89         + lib.optionalString (scaleIndex > 0) ''
90           configParts+=('{"channel_threads":0, "feed_threads":0}')
91         ''
92         # configure different ports for the instances
93         + ''
94           configParts+=('{"port":${toString (cfg.port + scaleIndex)}}')
95         ''
96         # merge all parts into a single configuration with later elements overriding previous elements
97         + ''
98           export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")"
99           exec ${cfg.package}/bin/invidious
100         '';
101       }
102     ];
104   serviceConfig = {
105     systemd.services = builtins.listToAttrs (builtins.genList
106       (scaleIndex: {
107         name = "invidious" + lib.optionalString (scaleIndex > 0) "-${builtins.toString scaleIndex}";
108         value = mkInvidiousService scaleIndex;
109       })
110       cfg.serviceScale);
112     services.invidious.settings = {
113       # Automatically initialises and migrates the database if necessary
114       check_tables = true;
116       db = {
117         user = lib.mkDefault (
118           if (lib.versionAtLeast config.system.stateVersion "24.05")
119           then "invidious"
120           else "kemal"
121         );
122         dbname = lib.mkDefault "invidious";
123         port = cfg.database.port;
124         # Blank for unix sockets, see
125         # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
126         host = lib.optionalString (cfg.database.host != null) cfg.database.host;
127         # Not needed because peer authentication is enabled
128         password = lib.mkIf (cfg.database.host == null) "";
129       };
131       host_binding = cfg.address;
132     } // (lib.optionalAttrs (cfg.domain != null) {
133       inherit (cfg) domain;
134     });
136     assertions = [
137       {
138         assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
139         message = "If database host isn't null, database password needs to be set";
140       }
141       {
142         assertion = cfg.serviceScale >= 1;
143         message = "Service can't be scaled below one instance";
144       }
145     ];
146   };
148   # Settings necessary for running with an automatically managed local database
149   localDatabaseConfig = lib.mkIf cfg.database.createLocally {
150     assertions = [
151       {
152         assertion = cfg.settings.db.user == cfg.settings.db.dbname;
153         message = ''
154           For local automatic database provisioning (services.invidious.database.createLocally == true)
155           to  work, the username used to connect to PostgreSQL must match the database name, that is
156           services.invidious.settings.db.user must match services.invidious.settings.db.dbname.
157           This is the default since NixOS 24.05. For older systems, it is normally safe to manually set
158           the user to "invidious" as the new user will be created with permissions
159           for the existing database. `REASSIGN OWNED BY kemal TO invidious;` may also be needed, it can be
160           run as `sudo -u postgres env psql --user=postgres --dbname=invidious -c 'reassign OWNED BY kemal to invidious;'`.
161         '';
162       }
163     ];
164     # Default to using the local database if we create it
165     services.invidious.database.host = lib.mkDefault null;
167     services.postgresql = {
168       enable = true;
169       ensureUsers = lib.singleton { name = cfg.settings.db.user; ensureDBOwnership = true; };
170       ensureDatabases = lib.singleton cfg.settings.db.dbname;
171     };
172   };
174   ytproxyConfig = lib.mkIf cfg.http3-ytproxy.enable {
175     systemd.services.http3-ytproxy = {
176       description = "HTTP3 ytproxy for Invidious";
177       wants = [ "network-online.target" ];
178       after = [ "network-online.target" ];
179       wantedBy = [ "multi-user.target" ];
181       script = ''
182         mkdir -p socket
183         exec ${lib.getExe cfg.http3-ytproxy.package};
184       '';
186       serviceConfig = {
187         RestartSec = "2s";
188         DynamicUser = true;
189         User = lib.mkIf cfg.nginx.enable config.services.nginx.user;
190         RuntimeDirectory = "http3-ytproxy";
191         WorkingDirectory = "/run/http3-ytproxy";
192       };
193     };
195     services.nginx.virtualHosts.${cfg.domain} = lib.mkIf cfg.nginx.enable {
196       locations."~ (^/videoplayback|^/vi/|^/ggpht/|^/sb/)" = {
197         proxyPass = "http://unix:/run/http3-ytproxy/socket/http-proxy.sock";
198       };
199     };
200   };
202   sigHelperConfig = lib.mkIf cfg.sig-helper.enable {
203     services.invidious.settings.signature_server = "tcp://${cfg.sig-helper.listenAddress}";
204     systemd.services.invidious-sig-helper = {
205       script = ''
206         exec ${lib.getExe cfg.sig-helper.package} --tcp "${cfg.sig-helper.listenAddress}"
207       '';
208       wantedBy = [ "multi-user.target" ];
209       before = [ "invidious.service" ];
210       wants = [ "network-online.target" ];
211       after = [ "network-online.target" ];
212       serviceConfig = {
213         User = "invidious-sig-helper";
214         DynamicUser = true;
215         Restart = "always";
217         PrivateTmp = true;
218         PrivateUsers = true;
219         ProtectSystem = true;
220         ProtectProc = "invisible";
221         ProtectHome = true;
222         PrivateDevices = true;
223         NoNewPrivileges = true;
224         ProtectKernelTunables = true;
225         ProtectKernelModules = true;
226         ProtectControlGroups = true;
227         ProtectKernelLogs = true;
228         CapabilityBoundingSet = "";
229         SystemCallArchitectures = "native";
230         SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" "@network-io" ];
231         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
232         RestrictNamespaces = true;
233       };
234     };
235   };
237   nginxConfig = lib.mkIf cfg.nginx.enable {
238     services.invidious.settings = {
239       https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
240       external_port = 80;
241     };
243     services.nginx = let
244       ip = if cfg.address == "0.0.0.0" then "127.0.0.1" else cfg.address;
245     in
246     {
247       enable = true;
248       virtualHosts.${cfg.domain} = {
249         locations."/".proxyPass =
250           if cfg.serviceScale == 1 then
251             "http://${ip}:${toString cfg.port}"
252           else "http://upstream-invidious";
254         enableACME = lib.mkDefault true;
255         forceSSL = lib.mkDefault true;
256       };
257       upstreams = lib.mkIf (cfg.serviceScale > 1) {
258         "upstream-invidious".servers = builtins.listToAttrs (builtins.genList
259           (scaleIndex: {
260             name = "${ip}:${toString (cfg.port + scaleIndex)}";
261             value = { };
262           })
263           cfg.serviceScale);
264       };
265     };
267     assertions = [{
268       assertion = cfg.domain != null;
269       message = "To use services.invidious.nginx, you need to set services.invidious.domain";
270     }];
271   };
274   options.services.invidious = {
275     enable = lib.mkEnableOption "Invidious";
277     package = lib.mkPackageOption pkgs "invidious" { };
279     settings = lib.mkOption {
280       type = settingsFormat.type;
281       default = { };
282       description = ''
283         The settings Invidious should use.
285         See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options.
286       '';
287     };
289     hmacKeyFile = lib.mkOption {
290       type = types.nullOr types.path;
291       default = null;
292       description = ''
293         A path to a file containing the `hmac_key`. If `null`, a key will be generated automatically on first
294         start.
296         If non-`null`, this option overrides any `hmac_key` specified in {option}`services.invidious.settings` or
297         via {option}`services.invidious.extraSettingsFile`.
298       '';
299     };
301     extraSettingsFile = lib.mkOption {
302       type = types.nullOr types.str;
303       default = null;
304       description = ''
305         A file including Invidious settings.
307         It gets merged with the settings specified in {option}`services.invidious.settings`
308         and can be used to store secrets like `hmac_key` outside of the nix store.
309       '';
310     };
312     serviceScale = lib.mkOption {
313       type = types.int;
314       default = 1;
315       description = ''
316         How many invidious instances to run.
318         See https://docs.invidious.io/improve-public-instance/#2-multiple-invidious-processes for more details
319         on how this is intended to work. All instances beyond the first one have the options `channel_threads`
320         and `feed_threads` set to 0 to avoid conflicts with multiple instances refreshing subscriptions. Instances
321         will be configured to bind to consecutive ports starting with {option}`services.invidious.port` for the
322         first instance.
323       '';
324     };
326     # This needs to be outside of settings to avoid infinite recursion
327     # (determining if nginx should be enabled and therefore the settings
328     # modified).
329     domain = lib.mkOption {
330       type = types.nullOr types.str;
331       default = null;
332       description = ''
333         The FQDN Invidious is reachable on.
335         This is used to configure nginx and for building absolute URLs.
336       '';
337     };
339     address = lib.mkOption {
340       type = types.str;
341       # default from https://github.com/iv-org/invidious/blob/master/config/config.example.yml
342       default = if cfg.nginx.enable then "127.0.0.1" else "0.0.0.0";
343       defaultText = lib.literalExpression ''if config.services.invidious.nginx.enable then "127.0.0.1" else "0.0.0.0"'';
344       description = ''
345         The IP address Invidious should bind to.
346       '';
347     };
349     port = lib.mkOption {
350       type = types.port;
351       # Default from https://docs.invidious.io/Configuration.md
352       default = 3000;
353       description = ''
354         The port Invidious should listen on.
356         To allow access from outside,
357         you can use either {option}`services.invidious.nginx`
358         or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`.
359       '';
360     };
362     database = {
363       createLocally = lib.mkOption {
364         type = types.bool;
365         default = true;
366         description = ''
367           Whether to create a local database with PostgreSQL.
368         '';
369       };
371       host = lib.mkOption {
372         type = types.nullOr types.str;
373         default = null;
374         description = ''
375           The database host Invidious should use.
377           If `null`, the local unix socket is used. Otherwise
378           TCP is used.
379         '';
380       };
382       port = lib.mkOption {
383         type = types.port;
384         default = config.services.postgresql.settings.port;
385         defaultText = lib.literalExpression "config.services.postgresql.settings.port";
386         description = ''
387           The port of the database Invidious should use.
389           Defaults to the the default postgresql port.
390         '';
391       };
393       passwordFile = lib.mkOption {
394         type = types.nullOr types.str;
395         apply = lib.mapNullable toString;
396         default = null;
397         description = ''
398           Path to file containing the database password.
399         '';
400       };
401     };
403     nginx.enable = lib.mkOption {
404       type = types.bool;
405       default = false;
406       description = ''
407         Whether to configure nginx as a reverse proxy for Invidious.
409         It serves it under the domain specified in {option}`services.invidious.settings.domain` with enabled TLS and ACME.
410         Further configuration can be done through {option}`services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*`,
411         which can also be used to disable AMCE and TLS.
412       '';
413     };
415     http3-ytproxy = {
416       enable = lib.mkOption {
417         type = lib.types.bool;
418         default = false;
419         description = ''
420           Whether to enable http3-ytproxy for faster loading of images and video playback.
422           If {option}`services.invidious.nginx.enable` is used, nginx will be configured automatically. If not, you
423           need to configure a reverse proxy yourself according to
424           https://docs.invidious.io/improve-public-instance/#3-speed-up-video-playback-with-http3-ytproxy.
425         '';
426       };
428       package = lib.mkPackageOption pkgs "http3-ytproxy" { };
429     };
431     sig-helper = {
432       enable = lib.mkOption {
433         type = lib.types.bool;
434         default = false;
435         description = ''
436           Whether to enable and configure inv-sig-helper to emulate the youtube client's javascript. This is required
437           to make certain videos playable.
439           This will download and run completely untrusted javascript from youtube! While this service is sandboxed,
440           this may still be an issue!
441         '';
442       };
444       package = lib.mkPackageOption pkgs "inv-sig-helper" { };
446       listenAddress = lib.mkOption {
447         type = lib.types.str;
448         default = "127.0.0.1:2999";
449         description = ''
450           The IP address/port where inv-sig-helper should listen.
451         '';
452       };
453     };
454   };
456   config = lib.mkIf cfg.enable (lib.mkMerge [
457     serviceConfig
458     localDatabaseConfig
459     nginxConfig
460     ytproxyConfig
461     sigHelperConfig
462   ]);