vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / web-apps / mastodon.nix
bloba3a2bbd1eca6d9b4a713fc8e023dde5f91903cf3
1 { lib, pkgs, config, options, ... }:
3 let
4   cfg = config.services.mastodon;
5   opt = options.services.mastodon;
7   # We only want to create a Redis and PostgreSQL databases if we're actually going to connect to it local.
8   redisActuallyCreateLocally = cfg.redis.createLocally && (cfg.redis.host == "127.0.0.1" || cfg.redis.enableUnixSocket);
9   databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
11   env = {
12     RAILS_ENV = "production";
13     NODE_ENV = "production";
15     BOOTSNAP_CACHE_DIR="/var/cache/mastodon/precompile";
16     LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
18     MASTODON_USE_LIBVIPS = "true";
20     # Concurrency mastodon-web
21     WEB_CONCURRENCY = toString cfg.webProcesses;
22     MAX_THREADS = toString cfg.webThreads;
24     DB_USER = cfg.database.user;
26     DB_HOST = cfg.database.host;
27     DB_NAME = cfg.database.name;
28     LOCAL_DOMAIN = cfg.localDomain;
29     SMTP_SERVER = cfg.smtp.host;
30     SMTP_PORT = toString cfg.smtp.port;
31     SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
32     PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
33     PAPERCLIP_ROOT_URL = "/system";
34     ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
36     TRUSTED_PROXY_IP = cfg.trustedProxy;
37   }
38   // lib.optionalAttrs (cfg.redis.host != null) { REDIS_HOST = cfg.redis.host; }
39   // lib.optionalAttrs (cfg.redis.port != null) { REDIS_PORT = toString cfg.redis.port; }
40   // lib.optionalAttrs (cfg.redis.createLocally && cfg.redis.enableUnixSocket) { REDIS_URL = "unix://${config.services.redis.servers.mastodon.unixSocket}"; }
41   // lib.optionalAttrs (cfg.database.host != "/run/postgresql" && cfg.database.port != null) { DB_PORT = toString cfg.database.port; }
42   // lib.optionalAttrs cfg.smtp.authenticate { SMTP_LOGIN  = cfg.smtp.user; }
43   // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_HOST = cfg.elasticsearch.host; }
44   // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PORT = toString cfg.elasticsearch.port; }
45   // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PRESET = cfg.elasticsearch.preset; }
46   // lib.optionalAttrs (cfg.elasticsearch.user != null) { ES_USER = cfg.elasticsearch.user; }
47   // cfg.extraConfig;
49   systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
51   cfgService = {
52     # User and group
53     User = cfg.user;
54     Group = cfg.group;
55     # Working directory
56     WorkingDirectory = cfg.package;
57     # Cache directory and mode
58     CacheDirectory = "mastodon";
59     CacheDirectoryMode = "0750";
60     # State directory and mode
61     StateDirectory = "mastodon";
62     StateDirectoryMode = "0750";
63     # Logs directory and mode
64     LogsDirectory = "mastodon";
65     LogsDirectoryMode = "0750";
66     # Proc filesystem
67     ProcSubset = "pid";
68     ProtectProc = "invisible";
69     # Access write directories
70     UMask = "0027";
71     # Capabilities
72     CapabilityBoundingSet = "";
73     # Security
74     NoNewPrivileges = true;
75     # Sandboxing
76     ProtectSystem = "strict";
77     ProtectHome = true;
78     PrivateTmp = true;
79     PrivateDevices = true;
80     PrivateUsers = true;
81     ProtectClock = true;
82     ProtectHostname = true;
83     ProtectKernelLogs = true;
84     ProtectKernelModules = true;
85     ProtectKernelTunables = true;
86     ProtectControlGroups = true;
87     RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
88     RestrictNamespaces = true;
89     LockPersonality = true;
90     MemoryDenyWriteExecute = false;
91     RestrictRealtime = true;
92     RestrictSUIDSGID = true;
93     RemoveIPC = true;
94     PrivateMounts = true;
95     # System Call Filtering
96     SystemCallArchitectures = "native";
97   };
99   # Services that all Mastodon units After= and Requires= on
100   commonServices = lib.optional redisActuallyCreateLocally "redis-mastodon.service"
101     ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
102     ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
104   envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
105     (lib.concatLists (lib.mapAttrsToList (name: value:
106       lib.optional (value != null) ''${name}="${toString value}"''
107     ) env))));
109   mastodonTootctl = let
110     sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
111   in pkgs.writeShellScriptBin "mastodon-tootctl" ''
112     set -a
113     export RAILS_ROOT="${cfg.package}"
114     source "${envFile}"
115     source /var/lib/mastodon/.secrets_env
116     ${sourceExtraEnv}
118     sudo=exec
119     if [[ "$USER" != ${cfg.user} ]]; then
120       sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env'
121     fi
122     $sudo ${cfg.package}/bin/tootctl "$@"
123   '';
125   sidekiqUnits = lib.attrsets.mapAttrs' (name: processCfg:
126     lib.nameValuePair "mastodon-sidekiq-${name}" (let
127       jobClassArgs = toString (builtins.map (c: "-q ${c}") processCfg.jobClasses);
128       jobClassLabel = toString ([""] ++ processCfg.jobClasses);
129       threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads);
130     in {
131       after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
132       requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
133       description = "Mastodon sidekiq${jobClassLabel}";
134       wantedBy = [ "mastodon.target" ];
135       environment = env // {
136         PORT = toString cfg.sidekiqPort;
137         DB_POOL = threads;
138       };
139       serviceConfig = {
140         ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
141         Restart = "always";
142         RestartSec = 20;
143         EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
144         WorkingDirectory = cfg.package;
145         LimitNOFILE = "1024000";
146         # System Call Filtering
147         SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
148       } // cfgService;
149       path = with pkgs; [ ffmpeg-headless file imagemagick ];
150     })
151   ) cfg.sidekiqProcesses;
153   streamingUnits = builtins.listToAttrs
154       (map (i: {
155         name = "mastodon-streaming-${toString i}";
156         value = {
157           after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
158           requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
159           wantedBy = [ "mastodon.target" "mastodon-streaming.target" ];
160           description = "Mastodon streaming ${toString i}";
161           environment = env // { SOCKET = "/run/mastodon-streaming/streaming-${toString i}.socket"; };
162           serviceConfig = {
163             ExecStart = "${cfg.package}/run-streaming.sh";
164             Restart = "always";
165             RestartSec = 20;
166             EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
167             WorkingDirectory = cfg.package;
168             # Runtime directory and mode
169             RuntimeDirectory = "mastodon-streaming";
170             RuntimeDirectoryMode = "0750";
171             # System Call Filtering
172             SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@memlock" "@resources" ])) "pipe" "pipe2" ];
173           } // cfgService;
174         };
175       })
176       (lib.range 1 cfg.streamingProcesses));
178 in {
180   imports = [
181     (lib.mkRemovedOptionModule
182       [ "services" "mastodon" "streamingPort" ]
183       "Mastodon currently doesn't support streaming via TCP ports. Please open a PR if you need this."
184     )
185   ];
187   options = {
188     services.mastodon = {
189       enable = lib.mkEnableOption "Mastodon, a federated social network server";
191       configureNginx = lib.mkOption {
192         description = ''
193           Configure nginx as a reverse proxy for mastodon.
194           Note that this makes some assumptions on your setup, and sets settings that will
195           affect other virtualHosts running on your nginx instance, if any.
196           Alternatively you can configure a reverse-proxy of your choice to serve these paths:
198           `/ -> $(nix-instantiate --eval '<nixpkgs>' -A mastodon.outPath)/public`
200           `/ -> 127.0.0.1:{{ webPort }} `(If there was no file in the directory above.)
202           `/system/ -> /var/lib/mastodon/public-system/`
204           `/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}`
206           Make sure that websockets are forwarded properly. You might want to set up caching
207           of some requests. Take a look at mastodon's provided nginx configuration at
208           `https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf`.
209         '';
210         type = lib.types.bool;
211         default = false;
212       };
214       user = lib.mkOption {
215         description = ''
216           User under which mastodon runs. If it is set to "mastodon",
217           that user will be created, otherwise it should be set to the
218           name of a user created elsewhere.
219           In both cases, the `mastodon` package will be added to the user's package set
220           and a tootctl wrapper to system packages that switches to the configured account
221           and load the right environment.
222         '';
223         type = lib.types.str;
224         default = "mastodon";
225       };
227       group = lib.mkOption {
228         description = ''
229           Group under which mastodon runs.
230         '';
231         type = lib.types.str;
232         default = "mastodon";
233       };
235       streamingProcesses = lib.mkOption {
236         description = ''
237           Number of processes used by the mastodon-streaming service.
238           Please define this explicitly, recommended is the amount of your CPU cores minus one.
239         '';
240         type = lib.types.ints.positive;
241         example = 3;
242       };
244       webPort = lib.mkOption {
245         description = "TCP port used by the mastodon-web service.";
246         type = lib.types.port;
247         default = 55001;
248       };
249       webProcesses = lib.mkOption {
250         description = "Processes used by the mastodon-web service.";
251         type = lib.types.int;
252         default = 2;
253       };
254       webThreads = lib.mkOption {
255         description = "Threads per process used by the mastodon-web service.";
256         type = lib.types.int;
257         default = 5;
258       };
260       sidekiqPort = lib.mkOption {
261         description = "TCP port used by the mastodon-sidekiq service.";
262         type = lib.types.port;
263         default = 55002;
264       };
266       sidekiqThreads = lib.mkOption {
267         description = "Worker threads used by the mastodon-sidekiq-all service. If `sidekiqProcesses` is configured and any processes specify null `threads`, this value is used.";
268         type = lib.types.int;
269         default = 25;
270       };
272       sidekiqProcesses = lib.mkOption {
273         description = "How many Sidekiq processes should be used to handle background jobs, and which job classes they handle. *Read the [upstream documentation](https://docs.joinmastodon.org/admin/scaling/#sidekiq) before configuring this!*";
274         type = with lib.types; attrsOf (submodule {
275           options = {
276             jobClasses = lib.mkOption {
277               type = listOf (enum [ "default" "push" "pull" "mailers" "scheduler" "ingress" ]);
278               description = "If not empty, which job classes should be executed by this process. *Only one process should handle the 'scheduler' class. If left empty, this process will handle the 'scheduler' class.*";
279             };
280             threads = lib.mkOption {
281               type = nullOr int;
282               description = "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
283             };
284           };
285         });
286         default = {
287           all = {
288             jobClasses = [ ];
289             threads = null;
290           };
291         };
292         example = {
293           all = {
294             jobClasses = [ ];
295             threads = null;
296           };
297           ingress = {
298             jobClasses = [ "ingress" ];
299             threads = 5;
300           };
301           default = {
302             jobClasses = [ "default" ];
303             threads = 10;
304           };
305           push-pull = {
306             jobClasses = [ "push" "pull" ];
307             threads = 5;
308           };
309         };
310       };
312       vapidPublicKeyFile = lib.mkOption {
313         description = ''
314           Path to file containing the public key used for Web Push
315           Voluntary Application Server Identification.  A new keypair can
316           be generated by running:
318           `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production bin/rake webpush:generate_keys`
320           If {option}`mastodon.vapidPrivateKeyFile`does not
321           exist, it and this file will be created with a new keypair.
322         '';
323         default = "/var/lib/mastodon/secrets/vapid-public-key";
324         type = lib.types.str;
325       };
327       localDomain = lib.mkOption {
328         description = "The domain serving your Mastodon instance.";
329         example = "social.example.org";
330         type = lib.types.str;
331       };
333       activeRecordEncryptionDeterministicKeyFile = lib.mkOption {
334         description = ''
335           This key must be set to enable the Active Record Encryption feature within
336           Rails that Mastodon uses to encrypt and decrypt some database attributes.
337           A new Active Record keys can be generated by running:
339           `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
341           If this file does not exist, it will be created with a new Active Record
342           keys.
343         '';
344         default = "/var/lib/mastodon/secrets/active-record-encryption-deterministic-key";
345         type = lib.types.str;
346       };
348       activeRecordEncryptionKeyDerivationSaltFile = lib.mkOption {
349         description = ''
350           This key must be set to enable the Active Record Encryption feature within
351           Rails that Mastodon uses to encrypt and decrypt some database attributes.
352           A new Active Record keys can be generated by running:
354           `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
356           If this file does not exist, it will be created with a new Active Record
357           keys.
358         '';
359         default = "/var/lib/mastodon/secrets/active-record-encryption-key-derivation-salt";
360         type = lib.types.str;
361       };
363       activeRecordEncryptionPrimaryKeyFile = lib.mkOption {
364         description = ''
365           This key must be set to enable the Active Record Encryption feature within
366           Rails that Mastodon uses to encrypt and decrypt some database attributes.
367           A new Active Record keys can be generated by running:
369           `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
371           If this file does not exist, it will be created with a new Active Record
372           keys.
373         '';
374         default = "/var/lib/mastodon/secrets/active-record-encryption-primary-key";
375         type = lib.types.str;
376       };
378       secretKeyBaseFile = lib.mkOption {
379         description = ''
380           Path to file containing the secret key base.
381           A new secret key base can be generated by running:
383           `nix build -f '<nixpkgs>' mastodon; cd result; bin/bundle exec rails secret`
385           If this file does not exist, it will be created with a new secret key base.
386         '';
387         default = "/var/lib/mastodon/secrets/secret-key-base";
388         type = lib.types.str;
389       };
391       otpSecretFile = lib.mkOption {
392         description = ''
393           Path to file containing the OTP secret.
394           A new OTP secret can be generated by running:
396           `nix build -f '<nixpkgs>' mastodon; cd result; bin/bundle exec rails secret`
398           If this file does not exist, it will be created with a new OTP secret.
399         '';
400         default = "/var/lib/mastodon/secrets/otp-secret";
401         type = lib.types.str;
402       };
404       vapidPrivateKeyFile = lib.mkOption {
405         description = ''
406           Path to file containing the private key used for Web Push
407           Voluntary Application Server Identification.  A new keypair can
408           be generated by running:
410           `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
412           If this file does not exist, it will be created with a new
413           private key.
414         '';
415         default = "/var/lib/mastodon/secrets/vapid-private-key";
416         type = lib.types.str;
417       };
419       trustedProxy = lib.mkOption {
420         description = ''
421           You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
422           otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
423           bad because IP addresses are used for important rate limits and security functions.
424         '';
425         type = lib.types.str;
426         default = "127.0.0.1";
427       };
429       enableUnixSocket = lib.mkOption {
430         description = ''
431           Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
432           is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
433           processes and streaming API (Node.js) processes.
434         '';
435         type = lib.types.bool;
436         default = true;
437       };
439       redis = {
440         createLocally = lib.mkOption {
441           description = "Configure local Redis server for Mastodon.";
442           type = lib.types.bool;
443           default = true;
444         };
446         host = lib.mkOption {
447           description = "Redis host.";
448           type = lib.types.nullOr lib.types.str;
449           default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
450           defaultText = lib.literalExpression ''
451             if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then "127.0.0.1" else null
452           '';
453         };
455         port = lib.mkOption {
456           description = "Redis port.";
457           type = lib.types.nullOr lib.types.port;
458           default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then 31637 else null;
459           defaultText = lib.literalExpression ''
460             if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then 31637 else null
461           '';
462         };
464         passwordFile = lib.mkOption {
465           description = "A file containing the password for Redis database.";
466           type = lib.types.nullOr lib.types.path;
467           default = null;
468           example = "/run/keys/mastodon-redis-password";
469         };
471         enableUnixSocket = lib.mkOption {
472           description = "Use Unix socket";
473           type = lib.types.bool;
474           default = true;
475         };
476       };
478       database = {
479         createLocally = lib.mkOption {
480           description = "Configure local PostgreSQL database server for Mastodon.";
481           type = lib.types.bool;
482           default = true;
483         };
485         host = lib.mkOption {
486           type = lib.types.str;
487           default = "/run/postgresql";
488           example = "192.168.23.42";
489           description = "Database host address or unix socket.";
490         };
492         port = lib.mkOption {
493           type = lib.types.nullOr lib.types.port;
494           default = if cfg.database.createLocally then null else 5432;
495           defaultText = lib.literalExpression ''
496             if config.${opt.database.createLocally}
497             then null
498             else 5432
499           '';
500           description = "Database host port.";
501         };
503         name = lib.mkOption {
504           type = lib.types.str;
505           default = "mastodon";
506           description = "Database name.";
507         };
509         user = lib.mkOption {
510           type = lib.types.str;
511           default = "mastodon";
512           description = "Database user.";
513         };
515         passwordFile = lib.mkOption {
516           type = lib.types.nullOr lib.types.path;
517           default = null;
518           example = "/var/lib/mastodon/secrets/db-password";
519           description = ''
520             A file containing the password corresponding to
521             {option}`database.user`.
522           '';
523         };
524       };
526       smtp = {
527         createLocally = lib.mkOption {
528           description = "Configure local Postfix SMTP server for Mastodon.";
529           type = lib.types.bool;
530           default = true;
531         };
533         authenticate = lib.mkOption {
534           description = "Authenticate with the SMTP server using username and password.";
535           type = lib.types.bool;
536           default = false;
537         };
539         host = lib.mkOption {
540           description = "SMTP host used when sending emails to users.";
541           type = lib.types.str;
542           default = "127.0.0.1";
543         };
545         port = lib.mkOption {
546           description = "SMTP port used when sending emails to users.";
547           type = lib.types.port;
548           default = 25;
549         };
551         fromAddress = lib.mkOption {
552           description = ''"From" address used when sending Emails to users.'';
553           type = lib.types.str;
554         };
556         user = lib.mkOption {
557           type = lib.types.nullOr lib.types.str;
558           default = null;
559           example = "mastodon@example.com";
560           description = "SMTP login name.";
561         };
563         passwordFile = lib.mkOption {
564           type = lib.types.nullOr lib.types.path;
565           default = null;
566           example = "/var/lib/mastodon/secrets/smtp-password";
567           description = ''
568             Path to file containing the SMTP password.
569           '';
570         };
571       };
573       elasticsearch = {
574         host = lib.mkOption {
575           description = ''
576             Elasticsearch host.
577             If it is not null, Elasticsearch full text search will be enabled.
578           '';
579           type = lib.types.nullOr lib.types.str;
580           default = null;
581         };
583         port = lib.mkOption {
584           description = "Elasticsearch port.";
585           type = lib.types.port;
586           default = 9200;
587         };
589         preset = lib.mkOption {
590           description = ''
591             It controls the ElasticSearch indices configuration (number of shards and replica).
592           '';
593           type = lib.types.enum [ "single_node_cluster" "small_cluster" "large_cluster" ];
594           default = "single_node_cluster";
595           example = "large_cluster";
596         };
598         user = lib.mkOption {
599           description = "Used for optionally authenticating with Elasticsearch.";
600           type = lib.types.nullOr lib.types.str;
601           default = null;
602           example = "elasticsearch-mastodon";
603         };
605         passwordFile = lib.mkOption {
606           description = ''
607             Path to file containing password for optionally authenticating with Elasticsearch.
608           '';
609           type = lib.types.nullOr lib.types.path;
610           default = null;
611           example = "/var/lib/mastodon/secrets/elasticsearch-password";
612         };
613       };
615       package = lib.mkOption {
616         type = lib.types.package;
617         default = pkgs.mastodon;
618         defaultText = lib.literalExpression "pkgs.mastodon";
619         description = "Mastodon package to use.";
620       };
622       extraConfig = lib.mkOption {
623         type = lib.types.attrs;
624         default = {};
625         description = ''
626           Extra environment variables to pass to all mastodon services.
627         '';
628       };
630       extraEnvFiles = lib.mkOption {
631         type = with lib.types; listOf path;
632         default = [];
633         description = ''
634           Extra environment files to pass to all mastodon services. Useful for passing down environmental secrets.
635         '';
636         example = [ "/etc/mastodon/s3config.env" ];
637       };
639       automaticMigrations = lib.mkOption {
640         type = lib.types.bool;
641         default = true;
642         description = ''
643           Do automatic database migrations.
644         '';
645       };
647       mediaAutoRemove = {
648         enable = lib.mkOption {
649           type = lib.types.bool;
650           default = true;
651           example = false;
652           description = ''
653             Automatically remove remote media attachments and preview cards older than the configured amount of days.
655             Recommended in https://docs.joinmastodon.org/admin/setup/.
656           '';
657         };
659         startAt = lib.mkOption {
660           type = lib.types.str;
661           default = "daily";
662           example = "hourly";
663           description = ''
664             How often to remove remote media.
666             The format is described in {manpage}`systemd.time(7)`.
667           '';
668         };
670         olderThanDays = lib.mkOption {
671           type = lib.types.int;
672           default = 30;
673           example = 14;
674           description = ''
675             How old remote media needs to be in order to be removed.
676           '';
677         };
678       };
679     };
680   };
682   config = lib.mkIf cfg.enable (lib.mkMerge [{
683     assertions = [
684       {
685         assertion = !redisActuallyCreateLocally -> (cfg.redis.host != "127.0.0.1" && cfg.redis.port != null);
686         message = ''
687           `services.mastodon.redis.host` and `services.mastodon.redis.port` need to be set if
688             `services.mastodon.redis.createLocally` is not enabled.
689         '';
690       }
691       {
692         assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || (cfg.redis.host == null && cfg.redis.port == null));
693         message = ''
694           `services.mastodon.redis.enableUnixSocket` needs to be disabled if
695             `services.mastodon.redis.host` and `services.mastodon.redis.port` is used.
696         '';
697       }
698       {
699         assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null);
700         message = ''
701           <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if
702             <option>services.mastodon.redis.passwordFile</option> is used.
703         '';
704       }
705       {
706         assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name);
707         message = ''
708           For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer
709             authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user
710             and services.mastodon.database.user must be identical.
711         '';
712       }
713       {
714         assertion = !databaseActuallyCreateLocally -> (cfg.database.host != "/run/postgresql");
715         message = ''
716           <option>services.mastodon.database.host</option> needs to be set if
717             <option>services.mastodon.database.createLocally</option> is not enabled.
718         '';
719       }
720       {
721         assertion = cfg.smtp.authenticate -> (cfg.smtp.user != null);
722         message = ''
723           <option>services.mastodon.smtp.user</option> needs to be set if
724             <option>services.mastodon.smtp.authenticate</option> is enabled.
725         '';
726       }
727       {
728         assertion = cfg.smtp.authenticate -> (cfg.smtp.passwordFile != null);
729         message = ''
730           <option>services.mastodon.smtp.passwordFile</option> needs to be set if
731             <option>services.mastodon.smtp.authenticate</option> is enabled.
732         '';
733       }
734       {
735         assertion = 1 ==
736           (lib.count (x: x)
737             (lib.mapAttrsToList
738               (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ])
739               cfg.sidekiqProcesses));
740         message = "There must be exactly one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
741       }
742     ];
744     environment.systemPackages = [ mastodonTootctl ];
746     systemd.targets.mastodon = {
747       description = "Target for all Mastodon services";
748       wantedBy = [ "multi-user.target" ];
749       after = [ "network.target" ];
750     };
752     systemd.targets.mastodon-streaming = {
753       description = "Target for all Mastodon streaming services";
754       wantedBy = [ "multi-user.target" "mastodon.target" ];
755       after = [ "network.target" ];
756     };
758     systemd.services.mastodon-init-dirs = {
759       script = ''
760         umask 077
762         if ! test -d /var/cache/mastodon/precompile; then
763           ${cfg.package}/bin/bundle exec bootsnap precompile --gemfile ${cfg.package}/app ${cfg.package}/lib
764         fi
765         if ! test -f ${cfg.activeRecordEncryptionDeterministicKeyFile}; then
766           mkdir -p $(dirname ${cfg.activeRecordEncryptionDeterministicKeyFile})
767           bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=//' > ${cfg.activeRecordEncryptionDeterministicKeyFile}
768         fi
769         if ! test -f ${cfg.activeRecordEncryptionKeyDerivationSaltFile}; then
770           mkdir -p $(dirname ${cfg.activeRecordEncryptionKeyDerivationSaltFile})
771           bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=//' > ${cfg.activeRecordEncryptionKeyDerivationSaltFile}
772         fi
773         if ! test -f ${cfg.activeRecordEncryptionPrimaryKeyFile}; then
774           mkdir -p $(dirname ${cfg.activeRecordEncryptionPrimaryKeyFile})
775           bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=//' > ${cfg.activeRecordEncryptionPrimaryKeyFile}
776         fi
777         if ! test -f ${cfg.secretKeyBaseFile}; then
778           mkdir -p $(dirname ${cfg.secretKeyBaseFile})
779           bin/bundle exec rails secret > ${cfg.secretKeyBaseFile}
780         fi
781         if ! test -f ${cfg.otpSecretFile}; then
782           mkdir -p $(dirname ${cfg.otpSecretFile})
783           bin/bundle exec rails secret > ${cfg.otpSecretFile}
784         fi
785         if ! test -f ${cfg.vapidPrivateKeyFile}; then
786           mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
787           keypair=$(bin/rake webpush:generate_keys)
788           echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
789           echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
790         fi
792         cat > /var/lib/mastodon/.secrets_env <<EOF
793         ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="$(cat ${cfg.activeRecordEncryptionDeterministicKeyFile})"
794         ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="$(cat ${cfg.activeRecordEncryptionKeyDerivationSaltFile})"
795         ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="$(cat ${cfg.activeRecordEncryptionPrimaryKeyFile})"
796         SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
797         OTP_SECRET="$(cat ${cfg.otpSecretFile})"
798         VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
799         VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
800       '' + lib.optionalString (cfg.redis.passwordFile != null)''
801         REDIS_PASSWORD="$(cat ${cfg.redis.passwordFile})"
802       '' + lib.optionalString (cfg.database.passwordFile != null) ''
803         DB_PASS="$(cat ${cfg.database.passwordFile})"
804       '' + lib.optionalString cfg.smtp.authenticate ''
805         SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
806       '' + lib.optionalString (cfg.elasticsearch.passwordFile != null) ''
807         ES_PASS="$(cat ${cfg.elasticsearch.passwordFile})"
808       '' + ''
809         EOF
810       '';
811       environment = env;
812       serviceConfig = {
813         Type = "oneshot";
814         SyslogIdentifier = "mastodon-init-dirs";
815         # System Call Filtering
816         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
817       } // cfgService;
819       after = [ "network.target" ];
820     };
822     systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
823       script = lib.optionalString (!databaseActuallyCreateLocally) ''
824         umask 077
825         export PGPASSWORD="$(cat '${cfg.database.passwordFile}')"
826       '' + ''
827         result="$(psql -t --csv -c \
828             "select count(*) from pg_class c \
829             join pg_namespace s on s.oid = c.relnamespace \
830             where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
831             and s.nspname not like 'pg_temp%';")" || error_code=$?
832         if [ "''${error_code:-0}" -ne 0 ]; then
833           echo "Failure checking if database is seeded. psql gave exit code $error_code"
834           exit "$error_code"
835         fi
836         if [ "$result" -eq 0 ]; then
837           echo "Seeding database"
838           SAFETY_ASSURED=1 rails db:schema:load
839           rails db:seed
840         else
841           echo "Migrating database (this might be a noop)"
842           rails db:migrate
843         fi
844       '' +  lib.optionalString (!databaseActuallyCreateLocally) ''
845         unset PGPASSWORD
846       '';
847       path = [ cfg.package config.services.postgresql.package ];
848       environment = env // lib.optionalAttrs (!databaseActuallyCreateLocally) {
849         PGHOST = cfg.database.host;
850         PGPORT = toString cfg.database.port;
851         PGDATABASE = cfg.database.name;
852         PGUSER = cfg.database.user;
853       };
854       serviceConfig = {
855         Type = "oneshot";
856         EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
857         WorkingDirectory = cfg.package;
858         # System Call Filtering
859         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
860       } // cfgService;
861       after = [ "network.target" "mastodon-init-dirs.service" ]
862         ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
863       requires = [ "mastodon-init-dirs.service" ]
864         ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
865     };
867     systemd.services.mastodon-web = {
868       after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
869       requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
870       wantedBy = [ "mastodon.target" ];
871       description = "Mastodon web";
872       environment = env // (if cfg.enableUnixSocket
873         then { SOCKET = "/run/mastodon-web/web.socket"; }
874         else { PORT = toString cfg.webPort; }
875       );
876       serviceConfig = {
877         ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
878         Restart = "always";
879         RestartSec = 20;
880         EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
881         WorkingDirectory = cfg.package;
882         # Runtime directory and mode
883         RuntimeDirectory = "mastodon-web";
884         RuntimeDirectoryMode = "0750";
885         # System Call Filtering
886         SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
887       } // cfgService;
888       path = with pkgs; [ ffmpeg-headless file ];
889     };
891     systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
892       description = "Mastodon media auto remove";
893       environment = env;
894       serviceConfig = {
895         Type = "oneshot";
896         EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
897       } // cfgService;
898       script = let
899         olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
900       in ''
901         ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
902         ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
903       '';
904       startAt = cfg.mediaAutoRemove.startAt;
905     };
907     services.nginx = lib.mkIf cfg.configureNginx {
908       enable = true;
909       recommendedProxySettings = true; # required for redirections to work
910       virtualHosts."${cfg.localDomain}" = {
911         root = "${cfg.package}/public/";
912         # mastodon only supports https, but you can override this if you offload tls elsewhere.
913         forceSSL = lib.mkDefault true;
914         enableACME = lib.mkDefault true;
916         locations."/system/".alias = "/var/lib/mastodon/public-system/";
918         locations."/" = {
919           tryFiles = "$uri @proxy";
920         };
922         locations."@proxy" = {
923           proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString cfg.webPort}");
924           proxyWebsockets = true;
925         };
927         locations."/api/v1/streaming/" = {
928           proxyPass = "http://mastodon-streaming";
929           proxyWebsockets = true;
930         };
931       };
932       upstreams.mastodon-streaming = {
933         extraConfig = ''
934           least_conn;
935         '';
936         servers = builtins.listToAttrs
937           (map (i: {
938             name = "unix:/run/mastodon-streaming/streaming-${toString i}.socket";
939             value = { };
940           }) (lib.range 1 cfg.streamingProcesses));
941       };
942     };
944     services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
945       enable = true;
946       hostname = lib.mkDefault "${cfg.localDomain}";
947     };
948     services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (lib.mkMerge [
949       {
950         enable = true;
951       }
952       (lib.mkIf (!cfg.redis.enableUnixSocket) {
953         port = cfg.redis.port;
954       })
955     ]);
956     services.postgresql = lib.mkIf databaseActuallyCreateLocally {
957       enable = true;
958       ensureUsers = [
959         {
960           name = cfg.database.name;
961           ensureDBOwnership = true;
962         }
963       ];
964       ensureDatabases = [ cfg.database.name ];
965     };
967     users.users = lib.mkMerge [
968       (lib.mkIf (cfg.user == "mastodon") {
969         mastodon = {
970           isSystemUser = true;
971           home = cfg.package;
972           inherit (cfg) group;
973         };
974       })
975       (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package ])
976       (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {${config.services.mastodon.user}.extraGroups = [ "redis-mastodon" ];})
977     ];
979     users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
980   }
981   { systemd.services = lib.mkMerge [ sidekiqUnits streamingUnits ]; }
982   ]);
984   meta.maintainers = with lib.maintainers; [ happy-river erictapen ];