at 21.11-pre 21 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 cfg = config.services.mastodon; 5 # We only want to create a database if we're actually going to connect to it. 6 databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql"; 7 8 env = { 9 RAILS_ENV = "production"; 10 NODE_ENV = "production"; 11 12 DB_USER = cfg.database.user; 13 14 REDIS_HOST = cfg.redis.host; 15 REDIS_PORT = toString(cfg.redis.port); 16 DB_HOST = cfg.database.host; 17 DB_PORT = toString(cfg.database.port); 18 DB_NAME = cfg.database.name; 19 LOCAL_DOMAIN = cfg.localDomain; 20 SMTP_SERVER = cfg.smtp.host; 21 SMTP_PORT = toString(cfg.smtp.port); 22 SMTP_FROM_ADDRESS = cfg.smtp.fromAddress; 23 PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system"; 24 PAPERCLIP_ROOT_URL = "/system"; 25 ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false"; 26 ES_HOST = cfg.elasticsearch.host; 27 ES_PORT = toString(cfg.elasticsearch.port); 28 29 TRUSTED_PROXY_IP = cfg.trustedProxy; 30 } 31 // (if cfg.smtp.authenticate then { SMTP_LOGIN = cfg.smtp.user; } else {}) 32 // cfg.extraConfig; 33 34 systemCallsList = [ "@clock" "@cpu-emulation" "@debug" "@keyring" "@module" "@mount" "@obsolete" "@raw-io" "@reboot" "@setuid" "@swap" ]; 35 36 cfgService = { 37 # User and group 38 User = cfg.user; 39 Group = cfg.group; 40 # State directory and mode 41 StateDirectory = "mastodon"; 42 StateDirectoryMode = "0750"; 43 # Logs directory and mode 44 LogsDirectory = "mastodon"; 45 LogsDirectoryMode = "0750"; 46 # Access write directories 47 UMask = "0027"; 48 # Capabilities 49 CapabilityBoundingSet = ""; 50 # Security 51 NoNewPrivileges = true; 52 # Sandboxing 53 ProtectSystem = "strict"; 54 ProtectHome = true; 55 PrivateTmp = true; 56 PrivateDevices = true; 57 PrivateUsers = true; 58 ProtectClock = true; 59 ProtectHostname = true; 60 ProtectKernelLogs = true; 61 ProtectKernelModules = true; 62 ProtectKernelTunables = true; 63 ProtectControlGroups = true; 64 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ]; 65 RestrictNamespaces = true; 66 LockPersonality = true; 67 MemoryDenyWriteExecute = false; 68 RestrictRealtime = true; 69 RestrictSUIDSGID = true; 70 PrivateMounts = true; 71 # System Call Filtering 72 SystemCallArchitectures = "native"; 73 }; 74 75 envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") ( 76 (lib.concatLists (lib.mapAttrsToList (name: value: 77 if value != null then [ 78 "${name}=\"${toString value}\"" 79 ] else [] 80 ) env)))); 81 82 mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" '' 83 set -a 84 source "${envFile}" 85 source /var/lib/mastodon/.secrets_env 86 eval -- "\$@" 87 ''; 88 89in { 90 91 options = { 92 services.mastodon = { 93 enable = lib.mkEnableOption "Mastodon, a federated social network server"; 94 95 configureNginx = lib.mkOption { 96 description = '' 97 Configure nginx as a reverse proxy for mastodon. 98 Note that this makes some assumptions on your setup, and sets settings that will 99 affect other virtualHosts running on your nginx instance, if any. 100 Alternatively you can configure a reverse-proxy of your choice to serve these paths: 101 102 <code>/ -> $(nix-instantiate --eval '&lt;nixpkgs&gt;' -A mastodon.outPath)/public</code> 103 104 <code>/ -> 127.0.0.1:{{ webPort }} </code>(If there was no file in the directory above.) 105 106 <code>/system/ -> /var/lib/mastodon/public-system/</code> 107 108 <code>/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}</code> 109 110 Make sure that websockets are forwarded properly. You might want to set up caching 111 of some requests. Take a look at mastodon's provided nginx configuration at 112 <code>https://github.com/tootsuite/mastodon/blob/master/dist/nginx.conf</code>. 113 ''; 114 type = lib.types.bool; 115 default = false; 116 }; 117 118 user = lib.mkOption { 119 description = '' 120 User under which mastodon runs. If it is set to "mastodon", 121 that user will be created, otherwise it should be set to the 122 name of a user created elsewhere. In both cases, 123 <package>mastodon</package> and a package containing only 124 the shell script <code>mastodon-env</code> will be added to 125 the user's package set. To run a command from 126 <package>mastodon</package> such as <code>tootctl</code> 127 with the environment configured by this module use 128 <code>mastodon-env</code>, as in: 129 130 <code>mastodon-env tootctl accounts create newuser --email newuser@example.com</code> 131 ''; 132 type = lib.types.str; 133 default = "mastodon"; 134 }; 135 136 group = lib.mkOption { 137 description = '' 138 Group under which mastodon runs. 139 ''; 140 type = lib.types.str; 141 default = "mastodon"; 142 }; 143 144 streamingPort = lib.mkOption { 145 description = "TCP port used by the mastodon-streaming service."; 146 type = lib.types.port; 147 default = 55000; 148 }; 149 150 webPort = lib.mkOption { 151 description = "TCP port used by the mastodon-web service."; 152 type = lib.types.port; 153 default = 55001; 154 }; 155 156 sidekiqPort = lib.mkOption { 157 description = "TCP port used by the mastodon-sidekiq service"; 158 type = lib.types.port; 159 default = 55002; 160 }; 161 162 vapidPublicKeyFile = lib.mkOption { 163 description = '' 164 Path to file containing the public key used for Web Push 165 Voluntary Application Server Identification. A new keypair can 166 be generated by running: 167 168 <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code> 169 170 If <option>mastodon.vapidPrivateKeyFile</option>does not 171 exist, it and this file will be created with a new keypair. 172 ''; 173 default = "/var/lib/mastodon/secrets/vapid-public-key"; 174 type = lib.types.str; 175 }; 176 177 localDomain = lib.mkOption { 178 description = "The domain serving your Mastodon instance."; 179 example = "social.example.org"; 180 type = lib.types.str; 181 }; 182 183 secretKeyBaseFile = lib.mkOption { 184 description = '' 185 Path to file containing the secret key base. 186 A new secret key base can be generated by running: 187 188 <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code> 189 190 If this file does not exist, it will be created with a new secret key base. 191 ''; 192 default = "/var/lib/mastodon/secrets/secret-key-base"; 193 type = lib.types.str; 194 }; 195 196 otpSecretFile = lib.mkOption { 197 description = '' 198 Path to file containing the OTP secret. 199 A new OTP secret can be generated by running: 200 201 <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake secret</code> 202 203 If this file does not exist, it will be created with a new OTP secret. 204 ''; 205 default = "/var/lib/mastodon/secrets/otp-secret"; 206 type = lib.types.str; 207 }; 208 209 vapidPrivateKeyFile = lib.mkOption { 210 description = '' 211 Path to file containing the private key used for Web Push 212 Voluntary Application Server Identification. A new keypair can 213 be generated by running: 214 215 <code>nix build -f '&lt;nixpkgs&gt;' mastodon; cd result; bin/rake webpush:generate_keys</code> 216 217 If this file does not exist, it will be created with a new 218 private key. 219 ''; 220 default = "/var/lib/mastodon/secrets/vapid-private-key"; 221 type = lib.types.str; 222 }; 223 224 trustedProxy = lib.mkOption { 225 description = '' 226 You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process, 227 otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be 228 bad because IP addresses are used for important rate limits and security functions. 229 ''; 230 type = lib.types.str; 231 default = "127.0.0.1"; 232 }; 233 234 enableUnixSocket = lib.mkOption { 235 description = '' 236 Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable 237 is process-specific, e.g. you need different values for every process, and it works for both web (Puma) 238 processes and streaming API (Node.js) processes. 239 ''; 240 type = lib.types.bool; 241 default = true; 242 }; 243 244 redis = { 245 createLocally = lib.mkOption { 246 description = "Configure local Redis server for Mastodon."; 247 type = lib.types.bool; 248 default = true; 249 }; 250 251 host = lib.mkOption { 252 description = "Redis host."; 253 type = lib.types.str; 254 default = "127.0.0.1"; 255 }; 256 257 port = lib.mkOption { 258 description = "Redis port."; 259 type = lib.types.port; 260 default = 6379; 261 }; 262 }; 263 264 database = { 265 createLocally = lib.mkOption { 266 description = "Configure local PostgreSQL database server for Mastodon."; 267 type = lib.types.bool; 268 default = true; 269 }; 270 271 host = lib.mkOption { 272 type = lib.types.str; 273 default = "/run/postgresql"; 274 example = "192.168.23.42"; 275 description = "Database host address or unix socket."; 276 }; 277 278 port = lib.mkOption { 279 type = lib.types.int; 280 default = 5432; 281 description = "Database host port."; 282 }; 283 284 name = lib.mkOption { 285 type = lib.types.str; 286 default = "mastodon"; 287 description = "Database name."; 288 }; 289 290 user = lib.mkOption { 291 type = lib.types.str; 292 default = "mastodon"; 293 description = "Database user."; 294 }; 295 296 passwordFile = lib.mkOption { 297 type = lib.types.nullOr lib.types.path; 298 default = "/var/lib/mastodon/secrets/db-password"; 299 example = "/run/keys/mastodon-db-password"; 300 description = '' 301 A file containing the password corresponding to 302 <option>database.user</option>. 303 ''; 304 }; 305 }; 306 307 smtp = { 308 createLocally = lib.mkOption { 309 description = "Configure local Postfix SMTP server for Mastodon."; 310 type = lib.types.bool; 311 default = true; 312 }; 313 314 authenticate = lib.mkOption { 315 description = "Authenticate with the SMTP server using username and password."; 316 type = lib.types.bool; 317 default = true; 318 }; 319 320 host = lib.mkOption { 321 description = "SMTP host used when sending emails to users."; 322 type = lib.types.str; 323 default = "127.0.0.1"; 324 }; 325 326 port = lib.mkOption { 327 description = "SMTP port used when sending emails to users."; 328 type = lib.types.port; 329 default = 25; 330 }; 331 332 fromAddress = lib.mkOption { 333 description = ''"From" address used when sending Emails to users.''; 334 type = lib.types.str; 335 }; 336 337 user = lib.mkOption { 338 description = "SMTP login name."; 339 type = lib.types.str; 340 }; 341 342 passwordFile = lib.mkOption { 343 description = '' 344 Path to file containing the SMTP password. 345 ''; 346 default = "/var/lib/mastodon/secrets/smtp-password"; 347 example = "/run/keys/mastodon-smtp-password"; 348 type = lib.types.str; 349 }; 350 }; 351 352 elasticsearch = { 353 host = lib.mkOption { 354 description = '' 355 Elasticsearch host. 356 If it is not null, Elasticsearch full text search will be enabled. 357 ''; 358 type = lib.types.nullOr lib.types.str; 359 default = null; 360 }; 361 362 port = lib.mkOption { 363 description = "Elasticsearch port."; 364 type = lib.types.port; 365 default = 9200; 366 }; 367 }; 368 369 package = lib.mkOption { 370 type = lib.types.package; 371 default = pkgs.mastodon; 372 defaultText = "pkgs.mastodon"; 373 description = "Mastodon package to use."; 374 }; 375 376 extraConfig = lib.mkOption { 377 type = lib.types.attrs; 378 default = {}; 379 description = '' 380 Extra environment variables to pass to all mastodon services. 381 ''; 382 }; 383 384 automaticMigrations = lib.mkOption { 385 type = lib.types.bool; 386 default = true; 387 description = '' 388 Do automatic database migrations. 389 ''; 390 }; 391 }; 392 }; 393 394 config = lib.mkIf cfg.enable { 395 assertions = [ 396 { 397 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user); 398 message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.''; 399 } 400 ]; 401 402 systemd.services.mastodon-init-dirs = { 403 script = '' 404 umask 077 405 406 if ! test -f ${cfg.secretKeyBaseFile}; then 407 mkdir -p $(dirname ${cfg.secretKeyBaseFile}) 408 bin/rake secret > ${cfg.secretKeyBaseFile} 409 fi 410 if ! test -f ${cfg.otpSecretFile}; then 411 mkdir -p $(dirname ${cfg.otpSecretFile}) 412 bin/rake secret > ${cfg.otpSecretFile} 413 fi 414 if ! test -f ${cfg.vapidPrivateKeyFile}; then 415 mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile}) 416 keypair=$(bin/rake webpush:generate_keys) 417 echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile} 418 echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile} 419 fi 420 421 cat > /var/lib/mastodon/.secrets_env <<EOF 422 SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})" 423 OTP_SECRET="$(cat ${cfg.otpSecretFile})" 424 VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})" 425 VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})" 426 DB_PASS="$(cat ${cfg.database.passwordFile})" 427 '' + (if cfg.smtp.authenticate then '' 428 SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})" 429 '' else "") + '' 430 EOF 431 ''; 432 environment = env; 433 serviceConfig = { 434 Type = "oneshot"; 435 WorkingDirectory = cfg.package; 436 # System Call Filtering 437 SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]); 438 } // cfgService; 439 440 after = [ "network.target" ]; 441 wantedBy = [ "multi-user.target" ]; 442 }; 443 444 systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations { 445 script = '' 446 if [ `psql ${cfg.database.name} -c \ 447 "select count(*) from pg_class c \ 448 join pg_namespace s on s.oid = c.relnamespace \ 449 where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \ 450 and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then 451 SAFETY_ASSURED=1 rails db:schema:load 452 rails db:seed 453 else 454 rails db:migrate 455 fi 456 ''; 457 path = [ cfg.package pkgs.postgresql ]; 458 environment = env; 459 serviceConfig = { 460 Type = "oneshot"; 461 EnvironmentFile = "/var/lib/mastodon/.secrets_env"; 462 WorkingDirectory = cfg.package; 463 # System Call Filtering 464 SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]); 465 } // cfgService; 466 after = [ "mastodon-init-dirs.service" "network.target" ] ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []); 467 wantedBy = [ "multi-user.target" ]; 468 }; 469 470 systemd.services.mastodon-streaming = { 471 after = [ "network.target" ] 472 ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []) 473 ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]); 474 description = "Mastodon streaming"; 475 wantedBy = [ "multi-user.target" ]; 476 environment = env // (if cfg.enableUnixSocket 477 then { SOCKET = "/run/mastodon-streaming/streaming.socket"; } 478 else { PORT = toString(cfg.streamingPort); } 479 ); 480 serviceConfig = { 481 ExecStart = "${cfg.package}/run-streaming.sh"; 482 Restart = "always"; 483 RestartSec = 20; 484 EnvironmentFile = "/var/lib/mastodon/.secrets_env"; 485 WorkingDirectory = cfg.package; 486 # Runtime directory and mode 487 RuntimeDirectory = "mastodon-streaming"; 488 RuntimeDirectoryMode = "0750"; 489 # System Call Filtering 490 SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@privileged" "@resources" ]); 491 } // cfgService; 492 }; 493 494 systemd.services.mastodon-web = { 495 after = [ "network.target" ] 496 ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []) 497 ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]); 498 description = "Mastodon web"; 499 wantedBy = [ "multi-user.target" ]; 500 environment = env // (if cfg.enableUnixSocket 501 then { SOCKET = "/run/mastodon-web/web.socket"; } 502 else { PORT = toString(cfg.webPort); } 503 ); 504 serviceConfig = { 505 ExecStart = "${cfg.package}/bin/puma -C config/puma.rb"; 506 Restart = "always"; 507 RestartSec = 20; 508 EnvironmentFile = "/var/lib/mastodon/.secrets_env"; 509 WorkingDirectory = cfg.package; 510 # Runtime directory and mode 511 RuntimeDirectory = "mastodon-web"; 512 RuntimeDirectoryMode = "0750"; 513 # System Call Filtering 514 SystemCallFilter = "~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]); 515 } // cfgService; 516 path = with pkgs; [ file imagemagick ffmpeg ]; 517 }; 518 519 systemd.services.mastodon-sidekiq = { 520 after = [ "network.target" ] 521 ++ (if databaseActuallyCreateLocally then [ "postgresql.service" ] else []) 522 ++ (if cfg.automaticMigrations then [ "mastodon-init-db.service" ] else [ "mastodon-init-dirs.service" ]); 523 description = "Mastodon sidekiq"; 524 wantedBy = [ "multi-user.target" ]; 525 environment = env // { 526 PORT = toString(cfg.sidekiqPort); 527 }; 528 serviceConfig = { 529 ExecStart = "${cfg.package}/bin/sidekiq -c 25 -r ${cfg.package}"; 530 Restart = "always"; 531 RestartSec = 20; 532 EnvironmentFile = "/var/lib/mastodon/.secrets_env"; 533 WorkingDirectory = cfg.package; 534 # System Call Filtering 535 SystemCallFilter = "~" + lib.concatStringsSep " " systemCallsList; 536 } // cfgService; 537 path = with pkgs; [ file imagemagick ffmpeg ]; 538 }; 539 540 services.nginx = lib.mkIf cfg.configureNginx { 541 enable = true; 542 recommendedProxySettings = true; # required for redirections to work 543 virtualHosts."${cfg.localDomain}" = { 544 root = "${cfg.package}/public/"; 545 forceSSL = true; # mastodon only supports https 546 enableACME = true; 547 548 locations."/system/".alias = "/var/lib/mastodon/public-system/"; 549 550 locations."/" = { 551 tryFiles = "$uri @proxy"; 552 }; 553 554 locations."@proxy" = { 555 proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}"); 556 proxyWebsockets = true; 557 }; 558 559 locations."/api/v1/streaming/" = { 560 proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/"); 561 proxyWebsockets = true; 562 }; 563 }; 564 }; 565 566 services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") { 567 enable = true; 568 }; 569 services.redis = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") { 570 enable = true; 571 }; 572 services.postgresql = lib.mkIf databaseActuallyCreateLocally { 573 enable = true; 574 ensureUsers = [ 575 { 576 name = cfg.database.user; 577 ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; 578 } 579 ]; 580 ensureDatabases = [ cfg.database.name ]; 581 }; 582 583 users.users = lib.mkMerge [ 584 (lib.mkIf (cfg.user == "mastodon") { 585 mastodon = { 586 isSystemUser = true; 587 home = cfg.package; 588 inherit (cfg) group; 589 }; 590 }) 591 (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv ]) 592 ]; 593 594 users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user; 595 }; 596 597 meta.maintainers = with lib.maintainers; [ happy-river erictapen ]; 598 599}