at 24.11-pre 28 kB view raw
1{ config, lib, pkgs, ...}: 2 3let 4 defaultUser = "outline"; 5 cfg = config.services.outline; 6 inherit (lib) mkRemovedOptionModule; 7in 8{ 9 imports = [ 10 (mkRemovedOptionModule [ "services" "outline" "sequelizeArguments" ] "Database migration are run agains configurated database by outline directly") 11 ]; 12 # See here for a reference of all the options: 13 # https://github.com/outline/outline/blob/v0.67.0/.env.sample 14 # https://github.com/outline/outline/blob/v0.67.0/app.json 15 # https://github.com/outline/outline/blob/v0.67.0/server/env.ts 16 # https://github.com/outline/outline/blob/v0.67.0/shared/types.ts 17 # The order is kept the same here to make updating easier. 18 options.services.outline = { 19 enable = lib.mkEnableOption "outline"; 20 21 package = lib.mkOption { 22 default = pkgs.outline; 23 defaultText = lib.literalExpression "pkgs.outline"; 24 type = lib.types.package; 25 example = lib.literalExpression '' 26 pkgs.outline.overrideAttrs (super: { 27 # Ignore the domain part in emails that come from OIDC. This is might 28 # be helpful if you want multiple users with different email providers 29 # to still land in the same team. Note that this effectively makes 30 # Outline a single-team instance. 31 patchPhase = ${"''"} 32 sed -i 's/const domain = parts\.length && parts\[1\];/const domain = "example.com";/g' plugins/oidc/server/auth/oidc.ts 33 ${"''"}; 34 }) 35 ''; 36 description = "Outline package to use."; 37 }; 38 39 user = lib.mkOption { 40 type = lib.types.str; 41 default = defaultUser; 42 description = '' 43 User under which the service should run. If this is the default value, 44 the user will be created, with the specified group as the primary 45 group. 46 ''; 47 }; 48 49 group = lib.mkOption { 50 type = lib.types.str; 51 default = defaultUser; 52 description = '' 53 Group under which the service should run. If this is the default value, 54 the group will be created. 55 ''; 56 }; 57 58 # 59 # Required options 60 # 61 62 secretKeyFile = lib.mkOption { 63 type = lib.types.str; 64 default = "/var/lib/outline/secret_key"; 65 description = '' 66 File path that contains the application secret key. It must be 32 67 bytes long and hex-encoded. If the file does not exist, a new key will 68 be generated and saved here. 69 ''; 70 }; 71 72 utilsSecretFile = lib.mkOption { 73 type = lib.types.str; 74 default = "/var/lib/outline/utils_secret"; 75 description = '' 76 File path that contains the utility secret key. If the file does not 77 exist, a new key will be generated and saved here. 78 ''; 79 }; 80 81 databaseUrl = lib.mkOption { 82 type = lib.types.str; 83 default = "local"; 84 description = '' 85 URI to use for the main PostgreSQL database. If this needs to include 86 credentials that shouldn't be world-readable in the Nix store, set an 87 environment file on the systemd service and override the 88 `DATABASE_URL` entry. Pass the string 89 `local` to setup a database on the local server. 90 ''; 91 }; 92 93 redisUrl = lib.mkOption { 94 type = lib.types.str; 95 default = "local"; 96 description = '' 97 Connection to a redis server. If this needs to include credentials 98 that shouldn't be world-readable in the Nix store, set an environment 99 file on the systemd service and override the 100 `REDIS_URL` entry. Pass the string 101 `local` to setup a local Redis database. 102 ''; 103 }; 104 105 publicUrl = lib.mkOption { 106 type = lib.types.str; 107 default = "http://localhost:3000"; 108 description = "The fully qualified, publicly accessible URL"; 109 }; 110 111 port = lib.mkOption { 112 type = lib.types.port; 113 default = 3000; 114 description = "Listening port."; 115 }; 116 117 storage = lib.mkOption { 118 description = '' 119 To support uploading of images for avatars and document attachments an 120 s3-compatible storage can be provided. AWS S3 is recommended for 121 redundancy however if you want to keep all file storage local an 122 alternative such as [minio](https://github.com/minio/minio) 123 can be used. 124 Local filesystem storage can also be used. 125 126 A more detailed guide on setting up storage is available 127 [here](https://docs.getoutline.com/s/hosting/doc/file-storage-N4M0T6Ypu7). 128 ''; 129 example = lib.literalExpression '' 130 { 131 accessKey = "..."; 132 secretKeyFile = "/somewhere"; 133 uploadBucketUrl = "https://minio.example.com"; 134 uploadBucketName = "outline"; 135 region = "us-east-1"; 136 } 137 ''; 138 type = lib.types.submodule { 139 options = { 140 storageType = lib.mkOption { 141 type = lib.types.enum [ "local" "s3" ]; 142 description = "File storage type, it can be local or s3."; 143 default = "s3"; 144 }; 145 localRootDir = lib.mkOption { 146 type = lib.types.str; 147 description = '' 148 If `storageType` is `local`, this sets the parent directory 149 under which all attachments/images go. 150 ''; 151 default = "/var/lib/outline/data"; 152 }; 153 accessKey = lib.mkOption { 154 type = lib.types.str; 155 description = "S3 access key."; 156 }; 157 secretKeyFile = lib.mkOption { 158 type = lib.types.path; 159 description = "File path that contains the S3 secret key."; 160 }; 161 region = lib.mkOption { 162 type = lib.types.str; 163 default = "xx-xxxx-x"; 164 description = "AWS S3 region name."; 165 }; 166 uploadBucketUrl = lib.mkOption { 167 type = lib.types.str; 168 description = '' 169 URL endpoint of an S3-compatible API where uploads should be 170 stored. 171 ''; 172 }; 173 uploadBucketName = lib.mkOption { 174 type = lib.types.str; 175 description = "Name of the bucket where uploads should be stored."; 176 }; 177 uploadMaxSize = lib.mkOption { 178 type = lib.types.int; 179 default = 26214400; 180 description = "Maxmium file size for uploads."; 181 }; 182 forcePathStyle = lib.mkOption { 183 type = lib.types.bool; 184 default = true; 185 description = "Force S3 path style."; 186 }; 187 acl = lib.mkOption { 188 type = lib.types.str; 189 default = "private"; 190 description = "ACL setting."; 191 }; 192 }; 193 }; 194 }; 195 196 # 197 # Authentication 198 # 199 200 slackAuthentication = lib.mkOption { 201 description = '' 202 To configure Slack auth, you'll need to create an Application at 203 https://api.slack.com/apps 204 205 When configuring the Client ID, add a redirect URL under "OAuth & Permissions" 206 to `https://[publicUrl]/auth/slack.callback`. 207 ''; 208 default = null; 209 type = lib.types.nullOr (lib.types.submodule { 210 options = { 211 clientId = lib.mkOption { 212 type = lib.types.str; 213 description = "Authentication key."; 214 }; 215 secretFile = lib.mkOption { 216 type = lib.types.str; 217 description = "File path containing the authentication secret."; 218 }; 219 }; 220 }); 221 }; 222 223 googleAuthentication = lib.mkOption { 224 description = '' 225 To configure Google auth, you'll need to create an OAuth Client ID at 226 https://console.cloud.google.com/apis/credentials 227 228 When configuring the Client ID, add an Authorized redirect URI to 229 `https://[publicUrl]/auth/google.callback`. 230 ''; 231 default = null; 232 type = lib.types.nullOr (lib.types.submodule { 233 options = { 234 clientId = lib.mkOption { 235 type = lib.types.str; 236 description = "Authentication client identifier."; 237 }; 238 clientSecretFile = lib.mkOption { 239 type = lib.types.str; 240 description = "File path containing the authentication secret."; 241 }; 242 }; 243 }); 244 }; 245 246 azureAuthentication = lib.mkOption { 247 description = '' 248 To configure Microsoft/Azure auth, you'll need to create an OAuth 249 Client. See 250 [the guide](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4) 251 for details on setting up your Azure App. 252 ''; 253 default = null; 254 type = lib.types.nullOr (lib.types.submodule { 255 options = { 256 clientId = lib.mkOption { 257 type = lib.types.str; 258 description = "Authentication client identifier."; 259 }; 260 clientSecretFile = lib.mkOption { 261 type = lib.types.str; 262 description = "File path containing the authentication secret."; 263 }; 264 resourceAppId = lib.mkOption { 265 type = lib.types.str; 266 description = "Authentication application resource ID."; 267 }; 268 }; 269 }); 270 }; 271 272 oidcAuthentication = lib.mkOption { 273 description = '' 274 To configure generic OIDC auth, you'll need some kind of identity 275 provider. See the documentation for whichever IdP you use to fill out 276 all the fields. The redirect URL is 277 `https://[publicUrl]/auth/oidc.callback`. 278 ''; 279 default = null; 280 type = lib.types.nullOr (lib.types.submodule { 281 options = { 282 clientId = lib.mkOption { 283 type = lib.types.str; 284 description = "Authentication client identifier."; 285 }; 286 clientSecretFile = lib.mkOption { 287 type = lib.types.str; 288 description = "File path containing the authentication secret."; 289 }; 290 authUrl = lib.mkOption { 291 type = lib.types.str; 292 description = "OIDC authentication URL endpoint."; 293 }; 294 tokenUrl = lib.mkOption { 295 type = lib.types.str; 296 description = "OIDC token URL endpoint."; 297 }; 298 userinfoUrl = lib.mkOption { 299 type = lib.types.str; 300 description = "OIDC userinfo URL endpoint."; 301 }; 302 usernameClaim = lib.mkOption { 303 type = lib.types.str; 304 description = '' 305 Specify which claims to derive user information from. Supports any 306 valid JSON path with the JWT payload 307 ''; 308 default = "preferred_username"; 309 }; 310 displayName = lib.mkOption { 311 type = lib.types.str; 312 description = "Display name for OIDC authentication."; 313 default = "OpenID"; 314 }; 315 scopes = lib.mkOption { 316 type = lib.types.listOf lib.types.str; 317 description = "OpenID authentication scopes."; 318 default = [ "openid" "profile" "email" ]; 319 }; 320 }; 321 }); 322 }; 323 324 # 325 # Optional configuration 326 # 327 328 sslKeyFile = lib.mkOption { 329 type = lib.types.nullOr lib.types.str; 330 default = null; 331 description = '' 332 File path that contains the Base64-encoded private key for HTTPS 333 termination. This is only required if you do not use an external reverse 334 proxy. See 335 [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4). 336 ''; 337 }; 338 sslCertFile = lib.mkOption { 339 type = lib.types.nullOr lib.types.str; 340 default = null; 341 description = '' 342 File path that contains the Base64-encoded certificate for HTTPS 343 termination. This is only required if you do not use an external reverse 344 proxy. See 345 [the documentation](https://wiki.generaloutline.com/share/dfa77e56-d4d2-4b51-8ff8-84ea6608faa4). 346 ''; 347 }; 348 349 cdnUrl = lib.mkOption { 350 type = lib.types.str; 351 default = ""; 352 description = '' 353 If using a Cloudfront/Cloudflare distribution or similar it can be set 354 using this option. This will cause paths to JavaScript files, 355 stylesheets and images to be updated to the hostname defined here. In 356 your CDN configuration the origin server should be set to public URL. 357 ''; 358 }; 359 360 forceHttps = lib.mkOption { 361 type = lib.types.bool; 362 default = true; 363 description = '' 364 Auto-redirect to HTTPS in production. The default is 365 `true` but you may set this to `false` 366 if you can be sure that SSL is terminated at an external loadbalancer. 367 ''; 368 }; 369 370 enableUpdateCheck = lib.mkOption { 371 type = lib.types.bool; 372 default = false; 373 description = '' 374 Have the installation check for updates by sending anonymized statistics 375 to the maintainers. 376 ''; 377 }; 378 379 concurrency = lib.mkOption { 380 type = lib.types.int; 381 default = 1; 382 description = '' 383 How many processes should be spawned. For a rough estimate, divide your 384 server's available memory by 512. 385 ''; 386 }; 387 388 maximumImportSize = lib.mkOption { 389 type = lib.types.int; 390 default = 5120000; 391 description = '' 392 The maximum size of document imports. Overriding this could be required 393 if you have especially large Word documents with embedded imagery. 394 ''; 395 }; 396 397 debugOutput = lib.mkOption { 398 type = lib.types.nullOr (lib.types.enum [ "http" ]); 399 default = null; 400 description = "Set this to `http` log HTTP requests."; 401 }; 402 403 slackIntegration = lib.mkOption { 404 description = '' 405 For a complete Slack integration with search and posting to channels 406 this configuration is also needed. See here for details: 407 https://wiki.generaloutline.com/share/be25efd1-b3ef-4450-b8e5-c4a4fc11e02a 408 ''; 409 default = null; 410 type = lib.types.nullOr (lib.types.submodule { 411 options = { 412 verificationTokenFile = lib.mkOption { 413 type = lib.types.str; 414 description = "File path containing the verification token."; 415 }; 416 appId = lib.mkOption { 417 type = lib.types.str; 418 description = "Application ID."; 419 }; 420 messageActions = lib.mkOption { 421 type = lib.types.bool; 422 default = true; 423 description = "Whether to enable message actions."; 424 }; 425 }; 426 }); 427 }; 428 429 googleAnalyticsId = lib.mkOption { 430 type = lib.types.nullOr lib.types.str; 431 default = null; 432 description = '' 433 Optionally enable Google Analytics to track page views in the knowledge 434 base. 435 ''; 436 }; 437 438 sentryDsn = lib.mkOption { 439 type = lib.types.nullOr lib.types.str; 440 default = null; 441 description = '' 442 Optionally enable [Sentry](https://sentry.io/) to 443 track errors and performance. 444 ''; 445 }; 446 447 sentryTunnel = lib.mkOption { 448 type = lib.types.nullOr lib.types.str; 449 default = null; 450 description = '' 451 Optionally add a 452 [Sentry proxy tunnel](https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option) 453 for bypassing ad blockers in the UI. 454 ''; 455 }; 456 457 logo = lib.mkOption { 458 type = lib.types.nullOr lib.types.str; 459 default = null; 460 description = '' 461 Custom logo displayed on the authentication screen. This will be scaled 462 to a height of 60px. 463 ''; 464 }; 465 466 smtp = lib.mkOption { 467 description = '' 468 To support sending outgoing transactional emails such as 469 "document updated" or "you've been invited" you'll need to provide 470 authentication for an SMTP server. 471 ''; 472 default = null; 473 type = lib.types.nullOr (lib.types.submodule { 474 options = { 475 host = lib.mkOption { 476 type = lib.types.str; 477 description = "Host name or IP address of the SMTP server."; 478 }; 479 port = lib.mkOption { 480 type = lib.types.port; 481 description = "TCP port of the SMTP server."; 482 }; 483 username = lib.mkOption { 484 type = lib.types.str; 485 description = "Username to authenticate with."; 486 }; 487 passwordFile = lib.mkOption { 488 type = lib.types.str; 489 description = '' 490 File path containing the password to authenticate with. 491 ''; 492 }; 493 fromEmail = lib.mkOption { 494 type = lib.types.str; 495 description = "Sender email in outgoing mail."; 496 }; 497 replyEmail = lib.mkOption { 498 type = lib.types.str; 499 description = "Reply address in outgoing mail."; 500 }; 501 tlsCiphers = lib.mkOption { 502 type = lib.types.str; 503 default = ""; 504 description = "Override SMTP cipher configuration."; 505 }; 506 secure = lib.mkOption { 507 type = lib.types.bool; 508 default = true; 509 description = "Use a secure SMTP connection."; 510 }; 511 }; 512 }); 513 }; 514 515 defaultLanguage = lib.mkOption { 516 type = lib.types.enum [ 517 "da_DK" 518 "de_DE" 519 "en_US" 520 "es_ES" 521 "fa_IR" 522 "fr_FR" 523 "it_IT" 524 "ja_JP" 525 "ko_KR" 526 "nl_NL" 527 "pl_PL" 528 "pt_BR" 529 "pt_PT" 530 "ru_RU" 531 "sv_SE" 532 "th_TH" 533 "vi_VN" 534 "zh_CN" 535 "zh_TW" 536 ]; 537 default = "en_US"; 538 description = '' 539 The default interface language. See 540 [translate.getoutline.com](https://translate.getoutline.com/) 541 for a list of available language codes and their rough percentage 542 translated. 543 ''; 544 }; 545 546 rateLimiter.enable = lib.mkEnableOption "rate limiter for the application web server"; 547 rateLimiter.requests = lib.mkOption { 548 type = lib.types.int; 549 default = 5000; 550 description = "Maximum number of requests in a throttling window."; 551 }; 552 rateLimiter.durationWindow = lib.mkOption { 553 type = lib.types.int; 554 default = 60; 555 description = "Length of a throttling window."; 556 }; 557 }; 558 559 config = lib.mkIf cfg.enable { 560 users.users = lib.optionalAttrs (cfg.user == defaultUser) { 561 ${defaultUser} = { 562 isSystemUser = true; 563 group = cfg.group; 564 }; 565 }; 566 567 users.groups = lib.optionalAttrs (cfg.group == defaultUser) { 568 ${defaultUser} = { }; 569 }; 570 571 systemd.tmpfiles.rules = [ 572 "f ${cfg.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -" 573 "f ${cfg.utilsSecretFile} 0600 ${cfg.user} ${cfg.group} -" 574 (if (cfg.storage.storageType == "s3") then 575 "f ${cfg.storage.secretKeyFile} 0600 ${cfg.user} ${cfg.group} -" 576 else 577 "d ${cfg.storage.localRootDir} 0700 ${cfg.user} ${cfg.group} - -") 578 ]; 579 580 services.postgresql = lib.mkIf (cfg.databaseUrl == "local") { 581 enable = true; 582 ensureUsers = [{ 583 name = "outline"; 584 ensureDBOwnership = true; 585 }]; 586 ensureDatabases = [ "outline" ]; 587 }; 588 589 # Outline is unable to create the uuid-ossp extension when using postgresql 12, in later version this 590 # extension can be created without superuser permission. This services therefor this extension before 591 # outline starts and postgresql 12 is using on the host. 592 # 593 # Can be removed after postgresql 12 is dropped from nixos. 594 systemd.services.outline-postgresql = 595 let 596 pgsql = config.services.postgresql; 597 in 598 lib.mkIf (cfg.databaseUrl == "local" && pgsql.package == pkgs.postgresql_12) { 599 after = [ "postgresql.service" ]; 600 bindsTo = [ "postgresql.service" ]; 601 wantedBy = [ "outline.service" ]; 602 partOf = [ "outline.service" ]; 603 path = [ 604 pgsql.package 605 ]; 606 script = '' 607 set -o errexit -o pipefail -o nounset -o errtrace 608 shopt -s inherit_errexit 609 610 psql outline -tAc 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp"' 611 ''; 612 613 serviceConfig = { 614 User = pgsql.superUser; 615 Type = "oneshot"; 616 RemainAfterExit = true; 617 }; 618 }; 619 620 services.redis.servers.outline = lib.mkIf (cfg.redisUrl == "local") { 621 enable = true; 622 user = config.services.outline.user; 623 port = 0; # Disable the TCP listener 624 }; 625 626 systemd.services.outline = let 627 localRedisUrl = "redis+unix:///run/redis-outline/redis.sock"; 628 localPostgresqlUrl = "postgres://localhost/outline?host=/run/postgresql"; 629 in { 630 description = "Outline wiki and knowledge base"; 631 wantedBy = [ "multi-user.target" ]; 632 after = [ "networking.target" ] 633 ++ lib.optional (cfg.databaseUrl == "local") "postgresql.service" 634 ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service"; 635 requires = lib.optional (cfg.databaseUrl == "local") "postgresql.service" 636 ++ lib.optional (cfg.redisUrl == "local") "redis-outline.service"; 637 path = [ 638 pkgs.openssl # Required by the preStart script 639 ]; 640 641 642 environment = lib.mkMerge [ 643 { 644 NODE_ENV = "production"; 645 646 REDIS_URL = if cfg.redisUrl == "local" then localRedisUrl else cfg.redisUrl; 647 URL = cfg.publicUrl; 648 PORT = builtins.toString cfg.port; 649 650 CDN_URL = cfg.cdnUrl; 651 FORCE_HTTPS = builtins.toString cfg.forceHttps; 652 ENABLE_UPDATES = builtins.toString cfg.enableUpdateCheck; 653 WEB_CONCURRENCY = builtins.toString cfg.concurrency; 654 MAXIMUM_IMPORT_SIZE = builtins.toString cfg.maximumImportSize; 655 DEBUG = cfg.debugOutput; 656 GOOGLE_ANALYTICS_ID = lib.optionalString (cfg.googleAnalyticsId != null) cfg.googleAnalyticsId; 657 SENTRY_DSN = lib.optionalString (cfg.sentryDsn != null) cfg.sentryDsn; 658 SENTRY_TUNNEL = lib.optionalString (cfg.sentryTunnel != null) cfg.sentryTunnel; 659 TEAM_LOGO = lib.optionalString (cfg.logo != null) cfg.logo; 660 DEFAULT_LANGUAGE = cfg.defaultLanguage; 661 662 RATE_LIMITER_ENABLED = builtins.toString cfg.rateLimiter.enable; 663 RATE_LIMITER_REQUESTS = builtins.toString cfg.rateLimiter.requests; 664 RATE_LIMITER_DURATION_WINDOW = builtins.toString cfg.rateLimiter.durationWindow; 665 666 FILE_STORAGE = cfg.storage.storageType; 667 FILE_STORAGE_UPLOAD_MAX_SIZE = builtins.toString cfg.storage.uploadMaxSize; 668 FILE_STORAGE_LOCAL_ROOT_DIR = cfg.storage.localRootDir; 669 } 670 671 (lib.mkIf (cfg.storage.storageType == "s3") { 672 AWS_ACCESS_KEY_ID = cfg.storage.accessKey; 673 AWS_REGION = cfg.storage.region; 674 AWS_S3_UPLOAD_BUCKET_URL = cfg.storage.uploadBucketUrl; 675 AWS_S3_UPLOAD_BUCKET_NAME = cfg.storage.uploadBucketName; 676 AWS_S3_FORCE_PATH_STYLE = builtins.toString cfg.storage.forcePathStyle; 677 AWS_S3_ACL = cfg.storage.acl; 678 }) 679 680 (lib.mkIf (cfg.slackAuthentication != null) { 681 SLACK_CLIENT_ID = cfg.slackAuthentication.clientId; 682 }) 683 684 (lib.mkIf (cfg.googleAuthentication != null) { 685 GOOGLE_CLIENT_ID = cfg.googleAuthentication.clientId; 686 }) 687 688 (lib.mkIf (cfg.azureAuthentication != null) { 689 AZURE_CLIENT_ID = cfg.azureAuthentication.clientId; 690 AZURE_RESOURCE_APP_ID = cfg.azureAuthentication.resourceAppId; 691 }) 692 693 (lib.mkIf (cfg.oidcAuthentication != null) { 694 OIDC_CLIENT_ID = cfg.oidcAuthentication.clientId; 695 OIDC_AUTH_URI = cfg.oidcAuthentication.authUrl; 696 OIDC_TOKEN_URI = cfg.oidcAuthentication.tokenUrl; 697 OIDC_USERINFO_URI = cfg.oidcAuthentication.userinfoUrl; 698 OIDC_USERNAME_CLAIM = cfg.oidcAuthentication.usernameClaim; 699 OIDC_DISPLAY_NAME = cfg.oidcAuthentication.displayName; 700 OIDC_SCOPES = lib.concatStringsSep " " cfg.oidcAuthentication.scopes; 701 }) 702 703 (lib.mkIf (cfg.slackIntegration != null) { 704 SLACK_APP_ID = cfg.slackIntegration.appId; 705 SLACK_MESSAGE_ACTIONS = builtins.toString cfg.slackIntegration.messageActions; 706 }) 707 708 (lib.mkIf (cfg.smtp != null) { 709 SMTP_HOST = cfg.smtp.host; 710 SMTP_PORT = builtins.toString cfg.smtp.port; 711 SMTP_USERNAME = cfg.smtp.username; 712 SMTP_FROM_EMAIL = cfg.smtp.fromEmail; 713 SMTP_REPLY_EMAIL = cfg.smtp.replyEmail; 714 SMTP_TLS_CIPHERS = cfg.smtp.tlsCiphers; 715 SMTP_SECURE = builtins.toString cfg.smtp.secure; 716 }) 717 ]; 718 719 preStart = '' 720 if [ ! -s ${lib.escapeShellArg cfg.secretKeyFile} ]; then 721 openssl rand -hex 32 > ${lib.escapeShellArg cfg.secretKeyFile} 722 fi 723 if [ ! -s ${lib.escapeShellArg cfg.utilsSecretFile} ]; then 724 openssl rand -hex 32 > ${lib.escapeShellArg cfg.utilsSecretFile} 725 fi 726 727 ''; 728 729 script = '' 730 export SECRET_KEY="$(head -n1 ${lib.escapeShellArg cfg.secretKeyFile})" 731 export UTILS_SECRET="$(head -n1 ${lib.escapeShellArg cfg.utilsSecretFile})" 732 ${lib.optionalString (cfg.storage.storageType == "s3") '' 733 export AWS_SECRET_ACCESS_KEY="$(head -n1 ${lib.escapeShellArg cfg.storage.secretKeyFile})" 734 ''} 735 ${lib.optionalString (cfg.slackAuthentication != null) '' 736 export SLACK_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.slackAuthentication.secretFile})" 737 ''} 738 ${lib.optionalString (cfg.googleAuthentication != null) '' 739 export GOOGLE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.googleAuthentication.clientSecretFile})" 740 ''} 741 ${lib.optionalString (cfg.azureAuthentication != null) '' 742 export AZURE_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.azureAuthentication.clientSecretFile})" 743 ''} 744 ${lib.optionalString (cfg.oidcAuthentication != null) '' 745 export OIDC_CLIENT_SECRET="$(head -n1 ${lib.escapeShellArg cfg.oidcAuthentication.clientSecretFile})" 746 ''} 747 ${lib.optionalString (cfg.sslKeyFile != null) '' 748 export SSL_KEY="$(head -n1 ${lib.escapeShellArg cfg.sslKeyFile})" 749 ''} 750 ${lib.optionalString (cfg.sslCertFile != null) '' 751 export SSL_CERT="$(head -n1 ${lib.escapeShellArg cfg.sslCertFile})" 752 ''} 753 ${lib.optionalString (cfg.slackIntegration != null) '' 754 export SLACK_VERIFICATION_TOKEN="$(head -n1 ${lib.escapeShellArg cfg.slackIntegration.verificationTokenFile})" 755 ''} 756 ${lib.optionalString (cfg.smtp != null) '' 757 export SMTP_PASSWORD="$(head -n1 ${lib.escapeShellArg cfg.smtp.passwordFile})" 758 ''} 759 760 ${if (cfg.databaseUrl == "local") then '' 761 export DATABASE_URL=${lib.escapeShellArg localPostgresqlUrl} 762 export PGSSLMODE=disable 763 '' else '' 764 export DATABASE_URL=${lib.escapeShellArg cfg.databaseUrl} 765 ''} 766 767 ${cfg.package}/bin/outline-server 768 ''; 769 770 serviceConfig = { 771 User = cfg.user; 772 Group = cfg.group; 773 Restart = "always"; 774 ProtectSystem = "strict"; 775 PrivateHome = true; 776 PrivateTmp = true; 777 UMask = "0007"; 778 779 StateDirectory = "outline"; 780 StateDirectoryMode = "0750"; 781 RuntimeDirectory = "outline"; 782 RuntimeDirectoryMode = "0750"; 783 # This working directory is required to find stuff like the set of 784 # onboarding files: 785 WorkingDirectory = "${cfg.package}/share/outline"; 786 # In case this directory is not in /var/lib/outline, it needs to be made writable explicitly 787 ReadWritePaths = lib.mkIf (cfg.storage.storageType == "local") [ cfg.storage.localRootDir ]; 788 }; 789 }; 790 }; 791}