at master 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 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 600 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 601 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 602 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 603 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 604 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 605 "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 606 "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 607 "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 608 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 609 "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 610 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" 611 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 612 "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 613 "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 614 "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 615 "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 616 617 # If we have a folder or symlink with Forgejo locales, remove it 618 # And symlink the current Forgejo locales in place 619 "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" 620 621 ] 622 ++ optionals cfg.lfs.enable [ 623 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 624 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 625 ]; 626 627 systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) { 628 description = "Forgejo secret bootstrap helper"; 629 script = '' 630 if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then 631 ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}' 632 fi 633 634 if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then 635 ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}' 636 fi 637 638 ${optionalString cfg.lfs.enable '' 639 if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then 640 ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}' 641 fi 642 ''} 643 644 if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then 645 ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}' 646 fi 647 ''; 648 serviceConfig = { 649 Type = "oneshot"; 650 RemainAfterExit = true; 651 User = cfg.user; 652 Group = cfg.group; 653 ReadWritePaths = [ cfg.customDir ]; 654 UMask = "0077"; 655 }; 656 }; 657 658 systemd.services.forgejo = { 659 description = "Forgejo (Beyond coding. We forge.)"; 660 after = [ 661 "network.target" 662 ] 663 ++ optionals usePostgresql [ 664 "postgresql.target" 665 ] 666 ++ optionals useMysql [ 667 "mysql.service" 668 ] 669 ++ optionals (!cfg.useWizard) [ 670 "forgejo-secrets.service" 671 ]; 672 requires = 673 optionals (cfg.database.createDatabase && usePostgresql) [ 674 "postgresql.target" 675 ] 676 ++ optionals (cfg.database.createDatabase && useMysql) [ 677 "mysql.service" 678 ] 679 ++ optionals (!cfg.useWizard) [ 680 "forgejo-secrets.service" 681 ]; 682 wantedBy = [ "multi-user.target" ]; 683 path = [ 684 cfg.package 685 pkgs.git 686 pkgs.gnupg 687 ]; 688 689 # In older versions the secret naming for JWT was kind of confusing. 690 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET 691 # wasn't persistent at all. 692 # To fix that, there is now the file oauth2_jwt_secret containing the 693 # values for JWT_SECRET and the file jwt_secret gets renamed to 694 # lfs_jwt_secret. 695 # We have to consider this to stay compatible with older installations. 696 preStart = '' 697 ${optionalString (!cfg.useWizard) '' 698 function forgejo_setup { 699 config='${cfg.customDir}/conf/app.ini' 700 cp -f '${format.generate "app.ini" cfg.settings}' "$config" 701 702 chmod u+w "$config" 703 ${lib.getExe' cfg.package "environment-to-ini"} --config "$config" 704 chmod u-w "$config" 705 } 706 (umask 027; forgejo_setup) 707 ''} 708 709 # run migrations/init the database 710 ${exe} migrate 711 712 # update all hooks' binary paths 713 ${exe} admin regenerate hooks 714 715 # update command option in authorized_keys 716 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] 717 then 718 ${exe} admin regenerate keys 719 fi 720 ''; 721 722 serviceConfig = { 723 Type = "notify"; 724 User = cfg.user; 725 Group = cfg.group; 726 WorkingDirectory = cfg.stateDir; 727 ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid"; 728 Restart = "always"; 729 # Runtime directory and mode 730 RuntimeDirectory = "forgejo"; 731 RuntimeDirectoryMode = "0755"; 732 # Proc filesystem 733 ProcSubset = "pid"; 734 ProtectProc = "invisible"; 735 # Access write directories 736 ReadWritePaths = [ 737 cfg.customDir 738 cfg.dump.backupDir 739 cfg.repositoryRoot 740 cfg.stateDir 741 cfg.lfs.contentDir 742 ]; 743 UMask = "0027"; 744 # Capabilities 745 CapabilityBoundingSet = ""; 746 # Security 747 NoNewPrivileges = true; 748 # Sandboxing 749 ProtectSystem = "strict"; 750 ProtectHome = true; 751 PrivateTmp = true; 752 PrivateDevices = true; 753 PrivateUsers = true; 754 ProtectHostname = true; 755 ProtectClock = true; 756 ProtectKernelTunables = true; 757 ProtectKernelModules = true; 758 ProtectKernelLogs = true; 759 ProtectControlGroups = true; 760 RestrictAddressFamilies = [ 761 "AF_UNIX" 762 "AF_INET" 763 "AF_INET6" 764 ]; 765 RestrictNamespaces = true; 766 LockPersonality = true; 767 MemoryDenyWriteExecute = true; 768 RestrictRealtime = true; 769 RestrictSUIDSGID = true; 770 RemoveIPC = true; 771 PrivateMounts = true; 772 # System Call Filtering 773 SystemCallArchitectures = "native"; 774 SystemCallFilter = [ 775 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" 776 "setrlimit" 777 ]; 778 # cfg.secrets 779 LoadCredential = map (e: "${e.env}:${e.path}") secrets; 780 }; 781 782 environment = { 783 USER = cfg.user; 784 HOME = cfg.stateDir; 785 FORGEJO_WORK_DIR = cfg.stateDir; 786 FORGEJO_CUSTOM = cfg.customDir; 787 } 788 // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets); 789 }; 790 791 services.openssh.settings.AcceptEnv = mkIf ( 792 !cfg.settings.server.START_SSH_SERVER or false 793 ) "GIT_PROTOCOL"; 794 795 users.users = mkIf (cfg.user == "forgejo") { 796 forgejo = { 797 home = cfg.stateDir; 798 useDefaultShell = true; 799 group = cfg.group; 800 isSystemUser = true; 801 }; 802 }; 803 804 users.groups = mkIf (cfg.group == "forgejo") { 805 forgejo = { }; 806 }; 807 808 systemd.services.forgejo-dump = mkIf cfg.dump.enable { 809 description = "forgejo dump"; 810 after = [ "forgejo.service" ]; 811 path = [ cfg.package ]; 812 813 environment = { 814 USER = cfg.user; 815 HOME = cfg.stateDir; 816 FORGEJO_WORK_DIR = cfg.stateDir; 817 FORGEJO_CUSTOM = cfg.customDir; 818 }; 819 820 serviceConfig = { 821 Type = "oneshot"; 822 User = cfg.user; 823 ExecStart = 824 "${exe} dump --type ${cfg.dump.type}" 825 + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; 826 WorkingDirectory = cfg.dump.backupDir; 827 }; 828 }; 829 830 systemd.timers.forgejo-dump = mkIf cfg.dump.enable { 831 description = "Forgejo dump timer"; 832 partOf = [ "forgejo-dump.service" ]; 833 wantedBy = [ "timers.target" ]; 834 timerConfig.OnCalendar = cfg.dump.interval; 835 }; 836 }; 837 838 meta.doc = ./forgejo.md; 839 meta.maintainers = with lib.maintainers; [ 840 bendlas 841 emilylange 842 pyrox0 843 ]; 844}