at 25.11-pre 27 kB view raw
1{ 2 config, 3 lib, 4 options, 5 pkgs, 6 ... 7}: 8 9let 10 cfg = config.services.forgejo; 11 opt = options.services.forgejo; 12 format = pkgs.formats.ini { }; 13 14 exe = lib.getExe cfg.package; 15 16 pg = config.services.postgresql; 17 useMysql = cfg.database.type == "mysql"; 18 usePostgresql = cfg.database.type == "postgres"; 19 useSqlite = cfg.database.type == "sqlite3"; 20 21 secrets = 22 let 23 mkSecret = 24 section: values: 25 lib.mapAttrsToList (key: value: { 26 env = envEscape "FORGEJO__${section}__${key}__FILE"; 27 path = value; 28 }) values; 29 # https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go 30 envEscape = 31 string: lib.replaceStrings [ "." "-" ] [ "_0X2E_" "_0X2D_" ] (lib.strings.toUpper string); 32 in 33 lib.flatten (lib.mapAttrsToList mkSecret cfg.secrets); 34 35 inherit (lib) 36 literalExpression 37 mkChangedOptionModule 38 mkDefault 39 mkEnableOption 40 mkIf 41 mkMerge 42 mkOption 43 mkPackageOption 44 mkRemovedOptionModule 45 mkRenamedOptionModule 46 optionalAttrs 47 optionals 48 optionalString 49 types 50 ; 51in 52{ 53 imports = [ 54 (mkRenamedOptionModule 55 [ "services" "forgejo" "appName" ] 56 [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ] 57 ) 58 (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ] 59 "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead" 60 ) 61 (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ] 62 "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead" 63 ) 64 (mkRenamedOptionModule 65 [ "services" "forgejo" "mailerPasswordFile" ] 66 [ "services" "forgejo" "secrets" "mailer" "PASSWD" ] 67 ) 68 69 # copied from services.gitea; remove at some point 70 (mkRenamedOptionModule 71 [ "services" "forgejo" "cookieSecure" ] 72 [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ] 73 ) 74 (mkRenamedOptionModule 75 [ "services" "forgejo" "disableRegistration" ] 76 [ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ] 77 ) 78 (mkRenamedOptionModule 79 [ "services" "forgejo" "domain" ] 80 [ "services" "forgejo" "settings" "server" "DOMAIN" ] 81 ) 82 (mkRenamedOptionModule 83 [ "services" "forgejo" "httpAddress" ] 84 [ "services" "forgejo" "settings" "server" "HTTP_ADDR" ] 85 ) 86 (mkRenamedOptionModule 87 [ "services" "forgejo" "httpPort" ] 88 [ "services" "forgejo" "settings" "server" "HTTP_PORT" ] 89 ) 90 (mkRenamedOptionModule 91 [ "services" "forgejo" "log" "level" ] 92 [ "services" "forgejo" "settings" "log" "LEVEL" ] 93 ) 94 (mkRenamedOptionModule 95 [ "services" "forgejo" "log" "rootPath" ] 96 [ "services" "forgejo" "settings" "log" "ROOT_PATH" ] 97 ) 98 (mkRenamedOptionModule 99 [ "services" "forgejo" "rootUrl" ] 100 [ "services" "forgejo" "settings" "server" "ROOT_URL" ] 101 ) 102 (mkRenamedOptionModule 103 [ "services" "forgejo" "ssh" "clonePort" ] 104 [ "services" "forgejo" "settings" "server" "SSH_PORT" ] 105 ) 106 (mkRenamedOptionModule 107 [ "services" "forgejo" "staticRootPath" ] 108 [ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ] 109 ) 110 (mkChangedOptionModule 111 [ "services" "forgejo" "enableUnixSocket" ] 112 [ "services" "forgejo" "settings" "server" "PROTOCOL" ] 113 (config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http") 114 ) 115 (mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ] 116 "services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted" 117 ) 118 ]; 119 120 options = { 121 services.forgejo = { 122 enable = mkEnableOption "Forgejo, a software forge"; 123 124 package = mkPackageOption pkgs "forgejo-lts" { }; 125 126 useWizard = mkOption { 127 default = false; 128 type = types.bool; 129 description = '' 130 Whether to use the built-in installation wizard instead of 131 declaratively managing the {file}`app.ini` config file in nix. 132 ''; 133 }; 134 135 stateDir = mkOption { 136 default = "/var/lib/forgejo"; 137 type = types.str; 138 description = "Forgejo data directory."; 139 }; 140 141 customDir = mkOption { 142 default = "${cfg.stateDir}/custom"; 143 defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; 144 type = types.str; 145 description = '' 146 Base directory for custom templates and other options. 147 148 If {option}`${opt.useWizard}` is disabled (default), this directory will also 149 hold secrets and the resulting {file}`app.ini` config at runtime. 150 ''; 151 }; 152 153 user = mkOption { 154 type = types.str; 155 default = "forgejo"; 156 description = "User account under which Forgejo runs."; 157 }; 158 159 group = mkOption { 160 type = types.str; 161 default = "forgejo"; 162 description = "Group under which Forgejo runs."; 163 }; 164 165 database = { 166 type = mkOption { 167 type = types.enum [ 168 "sqlite3" 169 "mysql" 170 "postgres" 171 ]; 172 example = "mysql"; 173 default = "sqlite3"; 174 description = "Database engine to use."; 175 }; 176 177 host = mkOption { 178 type = types.str; 179 default = "127.0.0.1"; 180 description = "Database host address."; 181 }; 182 183 port = mkOption { 184 type = types.port; 185 default = if usePostgresql then pg.settings.port else 3306; 186 defaultText = literalExpression '' 187 if config.${opt.database.type} != "postgresql" 188 then 3306 189 else 5432 190 ''; 191 description = "Database host port."; 192 }; 193 194 name = mkOption { 195 type = types.str; 196 default = "forgejo"; 197 description = "Database name."; 198 }; 199 200 user = mkOption { 201 type = types.str; 202 default = "forgejo"; 203 description = "Database user."; 204 }; 205 206 passwordFile = mkOption { 207 type = types.nullOr types.path; 208 default = null; 209 example = "/run/keys/forgejo-dbpassword"; 210 description = '' 211 A file containing the password corresponding to 212 {option}`${opt.database.user}`. 213 ''; 214 }; 215 216 socket = mkOption { 217 type = types.nullOr types.path; 218 default = 219 if (cfg.database.createDatabase && usePostgresql) then 220 "/run/postgresql" 221 else if (cfg.database.createDatabase && useMysql) then 222 "/run/mysqld/mysqld.sock" 223 else 224 null; 225 defaultText = literalExpression "null"; 226 example = "/run/mysqld/mysqld.sock"; 227 description = "Path to the unix socket file to use for authentication."; 228 }; 229 230 path = mkOption { 231 type = types.str; 232 default = "${cfg.stateDir}/data/forgejo.db"; 233 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"''; 234 description = "Path to the sqlite3 database file."; 235 }; 236 237 createDatabase = mkOption { 238 type = types.bool; 239 default = true; 240 description = "Whether to create a local database automatically."; 241 }; 242 }; 243 244 dump = { 245 enable = mkEnableOption "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)"; 246 247 interval = mkOption { 248 type = types.str; 249 default = "04:31"; 250 example = "hourly"; 251 description = '' 252 Run a Forgejo dump at this interval. Runs by default at 04:31 every day. 253 254 The format is described in 255 {manpage}`systemd.time(7)`. 256 ''; 257 }; 258 259 backupDir = mkOption { 260 type = types.str; 261 default = "${cfg.stateDir}/dump"; 262 defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"''; 263 description = "Path to the directory where the dump archives will be stored."; 264 }; 265 266 type = mkOption { 267 type = types.enum [ 268 "zip" 269 "tar" 270 "tar.sz" 271 "tar.gz" 272 "tar.xz" 273 "tar.bz2" 274 "tar.br" 275 "tar.lz4" 276 "tar.zst" 277 ]; 278 default = "zip"; 279 description = "Archive format used to store the dump file."; 280 }; 281 282 file = mkOption { 283 type = types.nullOr types.str; 284 default = null; 285 description = "Filename to be used for the dump. If `null` a default name is chosen by forgejo."; 286 example = "forgejo-dump"; 287 }; 288 }; 289 290 lfs = { 291 enable = mkOption { 292 type = types.bool; 293 default = false; 294 description = "Enables git-lfs support."; 295 }; 296 297 contentDir = mkOption { 298 type = types.str; 299 default = "${cfg.stateDir}/data/lfs"; 300 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"''; 301 description = "Where to store LFS files."; 302 }; 303 }; 304 305 repositoryRoot = mkOption { 306 type = types.str; 307 default = "${cfg.stateDir}/repositories"; 308 defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"''; 309 description = "Path to the git repositories."; 310 }; 311 312 settings = mkOption { 313 default = { }; 314 description = '' 315 Free-form settings written directly to the `app.ini` configfile file. 316 Refer to <https://forgejo.org/docs/latest/admin/config-cheat-sheet/> for supported values. 317 ''; 318 example = literalExpression '' 319 { 320 DEFAULT = { 321 RUN_MODE = "dev"; 322 }; 323 "cron.sync_external_users" = { 324 RUN_AT_START = true; 325 SCHEDULE = "@every 24h"; 326 UPDATE_EXISTING = true; 327 }; 328 mailer = { 329 ENABLED = true; 330 MAILER_TYPE = "sendmail"; 331 FROM = "do-not-reply@example.org"; 332 SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail"; 333 }; 334 other = { 335 SHOW_FOOTER_VERSION = false; 336 }; 337 } 338 ''; 339 type = types.submodule { 340 freeformType = format.type; 341 options = { 342 log = { 343 ROOT_PATH = mkOption { 344 default = "${cfg.stateDir}/log"; 345 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"''; 346 type = types.str; 347 description = "Root path for log files."; 348 }; 349 LEVEL = mkOption { 350 default = "Info"; 351 type = types.enum [ 352 "Trace" 353 "Debug" 354 "Info" 355 "Warn" 356 "Error" 357 "Critical" 358 ]; 359 description = "General log level."; 360 }; 361 }; 362 363 server = { 364 PROTOCOL = mkOption { 365 type = types.enum [ 366 "http" 367 "https" 368 "fcgi" 369 "http+unix" 370 "fcgi+unix" 371 ]; 372 default = "http"; 373 description = ''Listen protocol. `+unix` means "over unix", not "in addition to."''; 374 }; 375 376 HTTP_ADDR = mkOption { 377 type = types.either types.str types.path; 378 default = 379 if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then 380 "/run/forgejo/forgejo.sock" 381 else 382 "0.0.0.0"; 383 defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"''; 384 description = "Listen address. Must be a path when using a unix socket."; 385 }; 386 387 HTTP_PORT = mkOption { 388 type = types.port; 389 default = 3000; 390 description = "Listen port. Ignored when using a unix socket."; 391 }; 392 393 DOMAIN = mkOption { 394 type = types.str; 395 default = "localhost"; 396 description = "Domain name of your server."; 397 }; 398 399 ROOT_URL = mkOption { 400 type = types.str; 401 default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; 402 defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"''; 403 description = "Full public URL of Forgejo server."; 404 }; 405 406 STATIC_ROOT_PATH = mkOption { 407 type = types.either types.str types.path; 408 default = cfg.package.data; 409 defaultText = literalExpression "config.${opt.package}.data"; 410 example = "/var/lib/forgejo/data"; 411 description = "Upper level of template and static files path."; 412 }; 413 414 DISABLE_SSH = mkOption { 415 type = types.bool; 416 default = false; 417 description = "Disable external SSH feature."; 418 }; 419 420 SSH_PORT = mkOption { 421 type = types.port; 422 default = 22; 423 example = 2222; 424 description = '' 425 SSH port displayed in clone URL. 426 The option is required to configure a service when the external visible port 427 differs from the local listening port i.e. if port forwarding is used. 428 ''; 429 }; 430 }; 431 432 session = { 433 COOKIE_SECURE = mkOption { 434 type = types.bool; 435 default = false; 436 description = '' 437 Marks session cookies as "secure" as a hint for browsers to only send 438 them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS. 439 ''; 440 }; 441 }; 442 }; 443 }; 444 }; 445 446 secrets = mkOption { 447 default = { }; 448 description = '' 449 This is a small wrapper over systemd's `LoadCredential`. 450 451 It takes the same sections and keys as {option}`services.forgejo.settings`, 452 but the value of each key is a path instead of a string or bool. 453 454 The path is then loaded as credential, exported as environment variable 455 and then feed through 456 <https://codeberg.org/forgejo/forgejo/src/branch/forgejo/contrib/environment-to-ini/environment-to-ini.go>. 457 458 It does the required environment variable escaping for you. 459 460 ::: {.note} 461 Keys specified here take priority over the ones in {option}`services.forgejo.settings`! 462 ::: 463 ''; 464 example = literalExpression '' 465 { 466 metrics = { 467 TOKEN = "/run/keys/forgejo-metrics-token"; 468 }; 469 camo = { 470 HMAC_KEY = "/run/keys/forgejo-camo-hmac"; 471 }; 472 service = { 473 HCAPTCHA_SECRET = "/run/keys/forgejo-hcaptcha-secret"; 474 HCAPTCHA_SITEKEY = "/run/keys/forgejo-hcaptcha-sitekey"; 475 }; 476 } 477 ''; 478 type = types.submodule { 479 freeformType = with types; attrsOf (attrsOf path); 480 options = { }; 481 }; 482 }; 483 }; 484 }; 485 486 config = mkIf cfg.enable { 487 assertions = [ 488 { 489 assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; 490 message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned"; 491 } 492 { 493 assertion = cfg.database.createDatabase && usePostgresql -> cfg.database.user == cfg.database.name; 494 message = '' 495 When creating a database via NixOS, the db user and db name must be equal! 496 If you already have an existing DB+user and this assertion is new, you can safely set 497 `services.forgejo.createDatabase` to `false` because removal of `ensureUsers` 498 and `ensureDatabases` doesn't have any effect. 499 ''; 500 } 501 ]; 502 503 services.forgejo.settings = { 504 DEFAULT = { 505 RUN_MODE = mkDefault "prod"; 506 RUN_USER = mkDefault cfg.user; 507 WORK_PATH = mkDefault cfg.stateDir; 508 }; 509 510 database = mkMerge [ 511 { 512 DB_TYPE = cfg.database.type; 513 } 514 (mkIf (useMysql || usePostgresql) { 515 HOST = 516 if cfg.database.socket != null then 517 cfg.database.socket 518 else 519 cfg.database.host + ":" + toString cfg.database.port; 520 NAME = cfg.database.name; 521 USER = cfg.database.user; 522 }) 523 (mkIf useSqlite { 524 PATH = cfg.database.path; 525 }) 526 (mkIf usePostgresql { 527 SSL_MODE = "disable"; 528 }) 529 ]; 530 531 repository = { 532 ROOT = cfg.repositoryRoot; 533 }; 534 535 server = mkIf cfg.lfs.enable { 536 LFS_START_SERVER = true; 537 }; 538 539 session = { 540 COOKIE_NAME = mkDefault "session"; 541 }; 542 543 security = { 544 INSTALL_LOCK = true; 545 }; 546 547 lfs = mkIf cfg.lfs.enable { 548 PATH = cfg.lfs.contentDir; 549 }; 550 }; 551 552 services.forgejo.secrets = { 553 security = { 554 SECRET_KEY = "${cfg.customDir}/conf/secret_key"; 555 INTERNAL_TOKEN = "${cfg.customDir}/conf/internal_token"; 556 }; 557 558 oauth2 = { 559 JWT_SECRET = "${cfg.customDir}/conf/oauth2_jwt_secret"; 560 }; 561 562 database = mkIf (cfg.database.passwordFile != null) { 563 PASSWD = cfg.database.passwordFile; 564 }; 565 566 server = mkIf cfg.lfs.enable { 567 LFS_JWT_SECRET = "${cfg.customDir}/conf/lfs_jwt_secret"; 568 }; 569 }; 570 571 services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { 572 enable = mkDefault true; 573 574 ensureDatabases = [ cfg.database.name ]; 575 ensureUsers = [ 576 { 577 name = cfg.database.user; 578 ensureDBOwnership = true; 579 } 580 ]; 581 }; 582 583 services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { 584 enable = mkDefault true; 585 package = mkDefault pkgs.mariadb; 586 587 ensureDatabases = [ cfg.database.name ]; 588 ensureUsers = [ 589 { 590 name = cfg.database.user; 591 ensurePermissions = { 592 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 593 }; 594 } 595 ]; 596 }; 597 598 systemd.tmpfiles.rules = 599 [ 600 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 601 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 602 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 603 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 604 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 605 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 606 "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 607 "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 608 "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 609 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 610 "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 611 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" 612 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 613 "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 614 "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 615 "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 616 "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 617 618 # If we have a folder or symlink with Forgejo locales, remove it 619 # And symlink the current Forgejo locales in place 620 "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" 621 622 ] 623 ++ optionals cfg.lfs.enable [ 624 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 625 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 626 ]; 627 628 systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) { 629 description = "Forgejo secret bootstrap helper"; 630 script = '' 631 if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then 632 ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}' 633 fi 634 635 if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then 636 ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}' 637 fi 638 639 ${optionalString cfg.lfs.enable '' 640 if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then 641 ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}' 642 fi 643 ''} 644 645 if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then 646 ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}' 647 fi 648 ''; 649 serviceConfig = { 650 Type = "oneshot"; 651 RemainAfterExit = true; 652 User = cfg.user; 653 Group = cfg.group; 654 ReadWritePaths = [ cfg.customDir ]; 655 UMask = "0077"; 656 }; 657 }; 658 659 systemd.services.forgejo = { 660 description = "Forgejo (Beyond coding. We forge.)"; 661 after = 662 [ 663 "network.target" 664 ] 665 ++ optionals usePostgresql [ 666 "postgresql.service" 667 ] 668 ++ optionals useMysql [ 669 "mysql.service" 670 ] 671 ++ optionals (!cfg.useWizard) [ 672 "forgejo-secrets.service" 673 ]; 674 requires = 675 optionals (cfg.database.createDatabase && usePostgresql) [ 676 "postgresql.service" 677 ] 678 ++ optionals (cfg.database.createDatabase && useMysql) [ 679 "mysql.service" 680 ] 681 ++ optionals (!cfg.useWizard) [ 682 "forgejo-secrets.service" 683 ]; 684 wantedBy = [ "multi-user.target" ]; 685 path = [ 686 cfg.package 687 pkgs.git 688 pkgs.gnupg 689 ]; 690 691 # In older versions the secret naming for JWT was kind of confusing. 692 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET 693 # wasn't persistent at all. 694 # To fix that, there is now the file oauth2_jwt_secret containing the 695 # values for JWT_SECRET and the file jwt_secret gets renamed to 696 # lfs_jwt_secret. 697 # We have to consider this to stay compatible with older installations. 698 preStart = '' 699 ${optionalString (!cfg.useWizard) '' 700 function forgejo_setup { 701 config='${cfg.customDir}/conf/app.ini' 702 cp -f '${format.generate "app.ini" cfg.settings}' "$config" 703 704 chmod u+w "$config" 705 ${lib.getExe' cfg.package "environment-to-ini"} --config "$config" 706 chmod u-w "$config" 707 } 708 (umask 027; forgejo_setup) 709 ''} 710 711 # run migrations/init the database 712 ${exe} migrate 713 714 # update all hooks' binary paths 715 ${exe} admin regenerate hooks 716 717 # update command option in authorized_keys 718 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] 719 then 720 ${exe} admin regenerate keys 721 fi 722 ''; 723 724 serviceConfig = { 725 Type = "notify"; 726 User = cfg.user; 727 Group = cfg.group; 728 WorkingDirectory = cfg.stateDir; 729 ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid"; 730 Restart = "always"; 731 # Runtime directory and mode 732 RuntimeDirectory = "forgejo"; 733 RuntimeDirectoryMode = "0755"; 734 # Proc filesystem 735 ProcSubset = "pid"; 736 ProtectProc = "invisible"; 737 # Access write directories 738 ReadWritePaths = [ 739 cfg.customDir 740 cfg.dump.backupDir 741 cfg.repositoryRoot 742 cfg.stateDir 743 cfg.lfs.contentDir 744 ]; 745 UMask = "0027"; 746 # Capabilities 747 CapabilityBoundingSet = ""; 748 # Security 749 NoNewPrivileges = true; 750 # Sandboxing 751 ProtectSystem = "strict"; 752 ProtectHome = true; 753 PrivateTmp = true; 754 PrivateDevices = true; 755 PrivateUsers = true; 756 ProtectHostname = true; 757 ProtectClock = true; 758 ProtectKernelTunables = true; 759 ProtectKernelModules = true; 760 ProtectKernelLogs = true; 761 ProtectControlGroups = true; 762 RestrictAddressFamilies = [ 763 "AF_UNIX" 764 "AF_INET" 765 "AF_INET6" 766 ]; 767 RestrictNamespaces = true; 768 LockPersonality = true; 769 MemoryDenyWriteExecute = true; 770 RestrictRealtime = true; 771 RestrictSUIDSGID = true; 772 RemoveIPC = true; 773 PrivateMounts = true; 774 # System Call Filtering 775 SystemCallArchitectures = "native"; 776 SystemCallFilter = [ 777 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" 778 "setrlimit" 779 ]; 780 # cfg.secrets 781 LoadCredential = map (e: "${e.env}:${e.path}") secrets; 782 }; 783 784 environment = { 785 USER = cfg.user; 786 HOME = cfg.stateDir; 787 FORGEJO_WORK_DIR = cfg.stateDir; 788 FORGEJO_CUSTOM = cfg.customDir; 789 } // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets); 790 }; 791 792 services.openssh.settings.AcceptEnv = mkIf ( 793 !cfg.settings.server.START_SSH_SERVER or false 794 ) "GIT_PROTOCOL"; 795 796 users.users = mkIf (cfg.user == "forgejo") { 797 forgejo = { 798 home = cfg.stateDir; 799 useDefaultShell = true; 800 group = cfg.group; 801 isSystemUser = true; 802 }; 803 }; 804 805 users.groups = mkIf (cfg.group == "forgejo") { 806 forgejo = { }; 807 }; 808 809 systemd.services.forgejo-dump = mkIf cfg.dump.enable { 810 description = "forgejo dump"; 811 after = [ "forgejo.service" ]; 812 path = [ cfg.package ]; 813 814 environment = { 815 USER = cfg.user; 816 HOME = cfg.stateDir; 817 FORGEJO_WORK_DIR = cfg.stateDir; 818 FORGEJO_CUSTOM = cfg.customDir; 819 }; 820 821 serviceConfig = { 822 Type = "oneshot"; 823 User = cfg.user; 824 ExecStart = 825 "${exe} dump --type ${cfg.dump.type}" 826 + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; 827 WorkingDirectory = cfg.dump.backupDir; 828 }; 829 }; 830 831 systemd.timers.forgejo-dump = mkIf cfg.dump.enable { 832 description = "Forgejo dump timer"; 833 partOf = [ "forgejo-dump.service" ]; 834 wantedBy = [ "timers.target" ]; 835 timerConfig.OnCalendar = cfg.dump.interval; 836 }; 837 }; 838 839 meta.doc = ./forgejo.md; 840 meta.maintainers = with lib.maintainers; [ 841 bendlas 842 emilylange 843 pyrox0 844 ]; 845}