at 25.11-pre 32 kB view raw
1{ 2 config, 3 lib, 4 options, 5 pkgs, 6 ... 7}: 8 9with lib; 10 11let 12 cfg = config.services.gitea; 13 opt = options.services.gitea; 14 exe = lib.getExe cfg.package; 15 pg = config.services.postgresql; 16 useMysql = cfg.database.type == "mysql"; 17 usePostgresql = cfg.database.type == "postgres"; 18 useSqlite = cfg.database.type == "sqlite3"; 19 format = pkgs.formats.ini { }; 20 configFile = pkgs.writeText "app.ini" '' 21 APP_NAME = ${cfg.appName} 22 RUN_USER = ${cfg.user} 23 RUN_MODE = prod 24 WORK_PATH = ${cfg.stateDir} 25 26 ${generators.toINI { } cfg.settings} 27 28 ${optionalString (cfg.extraConfig != null) cfg.extraConfig} 29 ''; 30in 31 32{ 33 imports = [ 34 (mkRenamedOptionModule 35 [ "services" "gitea" "cookieSecure" ] 36 [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ] 37 ) 38 (mkRenamedOptionModule 39 [ "services" "gitea" "disableRegistration" ] 40 [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ] 41 ) 42 (mkRenamedOptionModule 43 [ "services" "gitea" "domain" ] 44 [ "services" "gitea" "settings" "server" "DOMAIN" ] 45 ) 46 (mkRenamedOptionModule 47 [ "services" "gitea" "httpAddress" ] 48 [ "services" "gitea" "settings" "server" "HTTP_ADDR" ] 49 ) 50 (mkRenamedOptionModule 51 [ "services" "gitea" "httpPort" ] 52 [ "services" "gitea" "settings" "server" "HTTP_PORT" ] 53 ) 54 (mkRenamedOptionModule 55 [ "services" "gitea" "log" "level" ] 56 [ "services" "gitea" "settings" "log" "LEVEL" ] 57 ) 58 (mkRenamedOptionModule 59 [ "services" "gitea" "log" "rootPath" ] 60 [ "services" "gitea" "settings" "log" "ROOT_PATH" ] 61 ) 62 (mkRenamedOptionModule 63 [ "services" "gitea" "rootUrl" ] 64 [ "services" "gitea" "settings" "server" "ROOT_URL" ] 65 ) 66 (mkRenamedOptionModule 67 [ "services" "gitea" "ssh" "clonePort" ] 68 [ "services" "gitea" "settings" "server" "SSH_PORT" ] 69 ) 70 (mkRenamedOptionModule 71 [ "services" "gitea" "staticRootPath" ] 72 [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ] 73 ) 74 75 (mkChangedOptionModule 76 [ "services" "gitea" "enableUnixSocket" ] 77 [ "services" "gitea" "settings" "server" "PROTOCOL" ] 78 (config: if config.services.gitea.enableUnixSocket then "http+unix" else "http") 79 ) 80 81 (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] 82 "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted" 83 ) 84 ]; 85 86 options = { 87 services.gitea = { 88 enable = mkOption { 89 default = false; 90 type = types.bool; 91 description = "Enable Gitea Service."; 92 }; 93 94 package = mkPackageOption pkgs "gitea" { }; 95 96 useWizard = mkOption { 97 default = false; 98 type = types.bool; 99 description = "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator."; 100 }; 101 102 stateDir = mkOption { 103 default = "/var/lib/gitea"; 104 type = types.str; 105 description = "Gitea data directory."; 106 }; 107 108 customDir = mkOption { 109 default = "${cfg.stateDir}/custom"; 110 defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; 111 type = types.str; 112 description = "Gitea custom directory. Used for config, custom templates and other options."; 113 }; 114 115 user = mkOption { 116 type = types.str; 117 default = "gitea"; 118 description = "User account under which gitea runs."; 119 }; 120 121 group = mkOption { 122 type = types.str; 123 default = "gitea"; 124 description = "Group under which gitea runs."; 125 }; 126 127 database = { 128 type = mkOption { 129 type = types.enum [ 130 "sqlite3" 131 "mysql" 132 "postgres" 133 ]; 134 example = "mysql"; 135 default = "sqlite3"; 136 description = "Database engine to use."; 137 }; 138 139 host = mkOption { 140 type = types.str; 141 default = "127.0.0.1"; 142 description = "Database host address."; 143 }; 144 145 port = mkOption { 146 type = types.port; 147 default = if usePostgresql then pg.settings.port else 3306; 148 defaultText = literalExpression '' 149 if config.${opt.database.type} != "postgresql" 150 then 3306 151 else 5432 152 ''; 153 description = "Database host port."; 154 }; 155 156 name = mkOption { 157 type = types.str; 158 default = "gitea"; 159 description = "Database name."; 160 }; 161 162 user = mkOption { 163 type = types.str; 164 default = "gitea"; 165 description = "Database user."; 166 }; 167 168 password = mkOption { 169 type = types.str; 170 default = ""; 171 description = '' 172 The password corresponding to {option}`database.user`. 173 Warning: this is stored in cleartext in the Nix store! 174 Use {option}`database.passwordFile` instead. 175 ''; 176 }; 177 178 passwordFile = mkOption { 179 type = types.nullOr types.path; 180 default = null; 181 example = "/run/keys/gitea-dbpassword"; 182 description = '' 183 A file containing the password corresponding to 184 {option}`database.user`. 185 ''; 186 }; 187 188 socket = mkOption { 189 type = types.nullOr types.path; 190 default = 191 if (cfg.database.createDatabase && usePostgresql) then 192 "/run/postgresql" 193 else if (cfg.database.createDatabase && useMysql) then 194 "/run/mysqld/mysqld.sock" 195 else 196 null; 197 defaultText = literalExpression "null"; 198 example = "/run/mysqld/mysqld.sock"; 199 description = "Path to the unix socket file to use for authentication."; 200 }; 201 202 path = mkOption { 203 type = types.str; 204 default = "${cfg.stateDir}/data/gitea.db"; 205 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"''; 206 description = "Path to the sqlite3 database file."; 207 }; 208 209 createDatabase = mkOption { 210 type = types.bool; 211 default = true; 212 description = "Whether to create a local database automatically."; 213 }; 214 }; 215 216 captcha = { 217 enable = mkOption { 218 type = types.bool; 219 default = false; 220 description = '' 221 Enables Gitea to display a CAPTCHA challenge on registration. 222 ''; 223 }; 224 225 secretFile = mkOption { 226 type = types.nullOr types.str; 227 default = null; 228 example = "/var/lib/secrets/gitea/captcha_secret"; 229 description = "Path to a file containing the CAPTCHA secret key."; 230 }; 231 232 siteKey = mkOption { 233 type = types.nullOr types.str; 234 default = null; 235 example = "my_site_key"; 236 description = "CAPTCHA site key to use for Gitea."; 237 }; 238 239 url = mkOption { 240 type = types.nullOr types.str; 241 default = null; 242 example = "https://google.com/recaptcha"; 243 description = "CAPTCHA url to use for Gitea. Only relevant for `recaptcha` and `mcaptcha`."; 244 }; 245 246 type = mkOption { 247 type = types.enum [ 248 "image" 249 "recaptcha" 250 "hcaptcha" 251 "mcaptcha" 252 "cfturnstile" 253 ]; 254 default = "image"; 255 example = "recaptcha"; 256 description = "The type of CAPTCHA to use for Gitea."; 257 }; 258 259 requireForLogin = mkOption { 260 type = types.bool; 261 default = false; 262 example = true; 263 description = "Displays a CAPTCHA challenge whenever a user logs in."; 264 }; 265 266 requireForExternalRegistration = mkOption { 267 type = types.bool; 268 default = false; 269 example = true; 270 description = "Displays a CAPTCHA challenge for users that register externally."; 271 }; 272 }; 273 274 dump = { 275 enable = mkOption { 276 type = types.bool; 277 default = false; 278 description = '' 279 Enable a timer that runs gitea dump to generate backup-files of the 280 current gitea database and repositories. 281 ''; 282 }; 283 284 interval = mkOption { 285 type = types.str; 286 default = "04:31"; 287 example = "hourly"; 288 description = '' 289 Run a gitea dump at this interval. Runs by default at 04:31 every day. 290 291 The format is described in 292 {manpage}`systemd.time(7)`. 293 ''; 294 }; 295 296 backupDir = mkOption { 297 type = types.str; 298 default = "${cfg.stateDir}/dump"; 299 defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"''; 300 description = "Path to the dump files."; 301 }; 302 303 type = mkOption { 304 type = types.enum [ 305 "zip" 306 "rar" 307 "tar" 308 "sz" 309 "tar.gz" 310 "tar.xz" 311 "tar.bz2" 312 "tar.br" 313 "tar.lz4" 314 "tar.zst" 315 ]; 316 default = "zip"; 317 description = "Archive format used to store the dump file."; 318 }; 319 320 file = mkOption { 321 type = types.nullOr types.str; 322 default = null; 323 description = "Filename to be used for the dump. If `null` a default name is chosen by gitea."; 324 example = "gitea-dump"; 325 }; 326 }; 327 328 lfs = { 329 enable = mkOption { 330 type = types.bool; 331 default = false; 332 description = "Enables git-lfs support."; 333 }; 334 335 contentDir = mkOption { 336 type = types.str; 337 default = "${cfg.stateDir}/data/lfs"; 338 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"''; 339 description = "Where to store LFS files."; 340 }; 341 }; 342 343 appName = mkOption { 344 type = types.str; 345 default = "gitea: Gitea Service"; 346 description = "Application name."; 347 }; 348 349 repositoryRoot = mkOption { 350 type = types.str; 351 default = "${cfg.stateDir}/repositories"; 352 defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"''; 353 description = "Path to the git repositories."; 354 }; 355 356 camoHmacKeyFile = mkOption { 357 type = types.nullOr types.str; 358 default = null; 359 example = "/var/lib/secrets/gitea/camoHmacKey"; 360 description = "Path to a file containing the camo HMAC key."; 361 }; 362 363 mailerPasswordFile = mkOption { 364 type = types.nullOr types.str; 365 default = null; 366 example = "/var/lib/secrets/gitea/mailpw"; 367 description = "Path to a file containing the SMTP password."; 368 }; 369 370 metricsTokenFile = mkOption { 371 type = types.nullOr types.str; 372 default = null; 373 example = "/var/lib/secrets/gitea/metrics_token"; 374 description = "Path to a file containing the metrics authentication token."; 375 }; 376 377 settings = mkOption { 378 default = { }; 379 description = '' 380 Gitea configuration. Refer to <https://docs.gitea.io/en-us/config-cheat-sheet/> 381 for details on supported values. 382 ''; 383 example = literalExpression '' 384 { 385 "cron.sync_external_users" = { 386 RUN_AT_START = true; 387 SCHEDULE = "@every 24h"; 388 UPDATE_EXISTING = true; 389 }; 390 mailer = { 391 ENABLED = true; 392 MAILER_TYPE = "sendmail"; 393 FROM = "do-not-reply@example.org"; 394 SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail"; 395 }; 396 other = { 397 SHOW_FOOTER_VERSION = false; 398 }; 399 } 400 ''; 401 type = types.submodule { 402 freeformType = format.type; 403 options = { 404 log = { 405 ROOT_PATH = mkOption { 406 default = "${cfg.stateDir}/log"; 407 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"''; 408 type = types.str; 409 description = "Root path for log files."; 410 }; 411 LEVEL = mkOption { 412 default = "Info"; 413 type = types.enum [ 414 "Trace" 415 "Debug" 416 "Info" 417 "Warn" 418 "Error" 419 "Critical" 420 ]; 421 description = "General log level."; 422 }; 423 }; 424 425 server = { 426 PROTOCOL = mkOption { 427 type = types.enum [ 428 "http" 429 "https" 430 "fcgi" 431 "http+unix" 432 "fcgi+unix" 433 ]; 434 default = "http"; 435 description = ''Listen protocol. `+unix` means "over unix", not "in addition to."''; 436 }; 437 438 HTTP_ADDR = mkOption { 439 type = types.either types.str types.path; 440 default = 441 if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"; 442 defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"''; 443 description = "Listen address. Must be a path when using a unix socket."; 444 }; 445 446 HTTP_PORT = mkOption { 447 type = types.port; 448 default = 3000; 449 description = "Listen port. Ignored when using a unix socket."; 450 }; 451 452 DOMAIN = mkOption { 453 type = types.str; 454 default = "localhost"; 455 description = "Domain name of your server."; 456 }; 457 458 ROOT_URL = mkOption { 459 type = types.str; 460 default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; 461 defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"''; 462 description = "Full public URL of gitea server."; 463 }; 464 465 STATIC_ROOT_PATH = mkOption { 466 type = types.either types.str types.path; 467 default = cfg.package.data; 468 defaultText = literalExpression "config.${opt.package}.data"; 469 example = "/var/lib/gitea/data"; 470 description = "Upper level of template and static files path."; 471 }; 472 473 DISABLE_SSH = mkOption { 474 type = types.bool; 475 default = false; 476 description = "Disable external SSH feature."; 477 }; 478 479 SSH_PORT = mkOption { 480 type = types.port; 481 default = 22; 482 example = 2222; 483 description = '' 484 SSH port displayed in clone URL. 485 The option is required to configure a service when the external visible port 486 differs from the local listening port i.e. if port forwarding is used. 487 ''; 488 }; 489 }; 490 491 service = { 492 DISABLE_REGISTRATION = mkEnableOption "the registration lock" // { 493 description = '' 494 By default any user can create an account on this `gitea` instance. 495 This can be disabled by using this option. 496 497 *Note:* please keep in mind that this should be added after the initial 498 deploy unless [](#opt-services.gitea.useWizard) 499 is `true` as the first registered user will be the administrator if 500 no install wizard is used. 501 ''; 502 }; 503 }; 504 505 session = { 506 COOKIE_SECURE = mkOption { 507 type = types.bool; 508 default = false; 509 description = '' 510 Marks session cookies as "secure" as a hint for browsers to only send 511 them via HTTPS. This option is recommend, if gitea is being served over HTTPS. 512 ''; 513 }; 514 }; 515 }; 516 }; 517 }; 518 519 extraConfig = mkOption { 520 type = with types; nullOr str; 521 default = null; 522 description = "Configuration lines appended to the generated gitea configuration file."; 523 }; 524 }; 525 }; 526 527 config = mkIf cfg.enable { 528 assertions = [ 529 { 530 assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; 531 message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned"; 532 } 533 { 534 assertion = cfg.database.createDatabase && usePostgresql -> cfg.database.user == cfg.database.name; 535 message = '' 536 When creating a database via NixOS, the db user and db name must be equal! 537 If you already have an existing DB+user and this assertion is new, you can safely set 538 `services.gitea.createDatabase` to `false` because removal of `ensureUsers` 539 and `ensureDatabases` doesn't have any effect. 540 ''; 541 } 542 { 543 assertion = 544 cfg.captcha.enable 545 -> cfg.captcha.type != "image" 546 -> (cfg.captcha.secretFile != null && cfg.captcha.siteKey != null); 547 message = '' 548 Using a CAPTCHA service that is not `image` requires providing a CAPTCHA secret through 549 the `captcha.secretFile` option and a CAPTCHA site key through the `captcha.siteKey` option. 550 ''; 551 } 552 { 553 assertion = 554 cfg.captcha.url != null 555 -> (builtins.elem cfg.captcha.type [ 556 "mcaptcha" 557 "recaptcha" 558 ]); 559 message = '' 560 `captcha.url` is only relevant when `captcha.type` is `mcaptcha` or `recaptcha`. 561 ''; 562 } 563 ]; 564 565 services.gitea.settings = 566 let 567 captchaPrefix = optionalString cfg.captcha.enable ( 568 { 569 image = "IMAGE"; 570 recaptcha = "RECAPTCHA"; 571 hcaptcha = "HCAPTCHA"; 572 mcaptcha = "MCAPTCHA"; 573 cfturnstile = "CF_TURNSTILE"; 574 } 575 ."${cfg.captcha.type}" 576 ); 577 in 578 { 579 "cron.update_checker".ENABLED = lib.mkDefault false; 580 581 database = mkMerge [ 582 { 583 DB_TYPE = cfg.database.type; 584 } 585 (mkIf (useMysql || usePostgresql) { 586 HOST = 587 if cfg.database.socket != null then 588 cfg.database.socket 589 else 590 cfg.database.host + ":" + toString cfg.database.port; 591 NAME = cfg.database.name; 592 USER = cfg.database.user; 593 PASSWD = "#dbpass#"; 594 }) 595 (mkIf useSqlite { 596 PATH = cfg.database.path; 597 }) 598 (mkIf usePostgresql { 599 SSL_MODE = "disable"; 600 }) 601 ]; 602 603 repository = { 604 ROOT = cfg.repositoryRoot; 605 }; 606 607 server = mkIf cfg.lfs.enable { 608 LFS_START_SERVER = true; 609 LFS_JWT_SECRET = "#lfsjwtsecret#"; 610 }; 611 612 camo = mkIf (cfg.camoHmacKeyFile != null) { 613 HMAC_KEY = "#hmackey#"; 614 }; 615 616 session = { 617 COOKIE_NAME = lib.mkDefault "session"; 618 }; 619 620 security = { 621 SECRET_KEY = "#secretkey#"; 622 INTERNAL_TOKEN = "#internaltoken#"; 623 INSTALL_LOCK = true; 624 }; 625 626 service = mkIf cfg.captcha.enable (mkMerge [ 627 { 628 ENABLE_CAPTCHA = true; 629 CAPTCHA_TYPE = cfg.captcha.type; 630 REQUIRE_CAPTCHA_FOR_LOGIN = cfg.captcha.requireForLogin; 631 REQUIRE_EXTERNAL_REGISTRATION_CAPTCHA = cfg.captcha.requireForExternalRegistration; 632 } 633 (mkIf (cfg.captcha.secretFile != null) { 634 "${captchaPrefix}_SECRET" = "#captchasecret#"; 635 }) 636 (mkIf (cfg.captcha.siteKey != null) { 637 "${captchaPrefix}_SITEKEY" = cfg.captcha.siteKey; 638 }) 639 (mkIf (cfg.captcha.url != null) { 640 "${captchaPrefix}_URL" = cfg.captcha.url; 641 }) 642 ]); 643 644 mailer = mkIf (cfg.mailerPasswordFile != null) { 645 PASSWD = "#mailerpass#"; 646 }; 647 648 metrics = mkIf (cfg.metricsTokenFile != null) { 649 TOKEN = "#metricstoken#"; 650 }; 651 652 oauth2 = { 653 JWT_SECRET = "#oauth2jwtsecret#"; 654 }; 655 656 lfs = mkIf cfg.lfs.enable { 657 PATH = cfg.lfs.contentDir; 658 }; 659 660 packages.CHUNKED_UPLOAD_PATH = "${cfg.stateDir}/tmp/package-upload"; 661 }; 662 663 services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { 664 enable = mkDefault true; 665 666 ensureDatabases = [ cfg.database.name ]; 667 ensureUsers = [ 668 { 669 name = cfg.database.user; 670 ensureDBOwnership = true; 671 } 672 ]; 673 }; 674 675 services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { 676 enable = mkDefault true; 677 package = mkDefault pkgs.mariadb; 678 679 ensureDatabases = [ cfg.database.name ]; 680 ensureUsers = [ 681 { 682 name = cfg.database.user; 683 ensurePermissions = { 684 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 685 }; 686 } 687 ]; 688 }; 689 690 systemd.tmpfiles.rules = 691 [ 692 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 693 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" 694 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 695 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" 696 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 697 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 698 "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 699 "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 700 "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 701 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 702 "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" 703 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" 704 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 705 "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" 706 "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" 707 "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" 708 "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" 709 710 # If we have a folder or symlink with gitea locales, remove it 711 # And symlink the current gitea locales in place 712 "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" 713 714 ] 715 ++ lib.optionals cfg.lfs.enable [ 716 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 717 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" 718 ]; 719 720 systemd.services.gitea = { 721 description = "gitea"; 722 after = 723 [ "network.target" ] 724 ++ optional usePostgresql "postgresql.service" 725 ++ optional useMysql "mysql.service"; 726 requires = 727 optional (cfg.database.createDatabase && usePostgresql) "postgresql.service" 728 ++ optional (cfg.database.createDatabase && useMysql) "mysql.service"; 729 wantedBy = [ "multi-user.target" ]; 730 path = [ 731 cfg.package 732 pkgs.git 733 pkgs.gnupg 734 ]; 735 736 # In older versions the secret naming for JWT was kind of confusing. 737 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET 738 # wasn't persistent at all. 739 # To fix that, there is now the file oauth2_jwt_secret containing the 740 # values for JWT_SECRET and the file jwt_secret gets renamed to 741 # lfs_jwt_secret. 742 # We have to consider this to stay compatible with older installations. 743 preStart = 744 let 745 runConfig = "${cfg.customDir}/conf/app.ini"; 746 secretKey = "${cfg.customDir}/conf/secret_key"; 747 oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; 748 oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET 749 lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET 750 internalToken = "${cfg.customDir}/conf/internal_token"; 751 replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; 752 in 753 '' 754 # copy custom configuration and generate random secrets if needed 755 ${optionalString (!cfg.useWizard) '' 756 function gitea_setup { 757 cp -f '${configFile}' '${runConfig}' 758 759 if [ ! -s '${secretKey}' ]; then 760 ${exe} generate secret SECRET_KEY > '${secretKey}' 761 fi 762 763 # Migrate LFS_JWT_SECRET filename 764 if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then 765 mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' 766 fi 767 768 if [ ! -s '${oauth2JwtSecret}' ]; then 769 ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' 770 fi 771 772 ${lib.optionalString cfg.lfs.enable '' 773 if [ ! -s '${lfsJwtSecret}' ]; then 774 ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' 775 fi 776 ''} 777 778 if [ ! -s '${internalToken}' ]; then 779 ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' 780 fi 781 782 chmod u+w '${runConfig}' 783 ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' 784 ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' 785 ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' 786 ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' 787 788 ${lib.optionalString cfg.lfs.enable '' 789 ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' 790 ''} 791 792 ${lib.optionalString (cfg.camoHmacKeyFile != null) '' 793 ${replaceSecretBin} '#hmackey#' '${cfg.camoHmacKeyFile}' '${runConfig}' 794 ''} 795 796 ${lib.optionalString (cfg.mailerPasswordFile != null) '' 797 ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' 798 ''} 799 800 ${lib.optionalString (cfg.metricsTokenFile != null) '' 801 ${replaceSecretBin} '#metricstoken#' '${cfg.metricsTokenFile}' '${runConfig}' 802 ''} 803 804 ${lib.optionalString (cfg.captcha.secretFile != null) '' 805 ${replaceSecretBin} '#captchasecret#' '${cfg.captcha.secretFile}' '${runConfig}' 806 ''} 807 chmod u-w '${runConfig}' 808 } 809 (umask 027; gitea_setup) 810 ''} 811 812 # run migrations/init the database 813 ${exe} migrate 814 815 # update all hooks' binary paths 816 ${exe} admin regenerate hooks 817 818 # update command option in authorized_keys 819 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] 820 then 821 ${exe} admin regenerate keys 822 fi 823 ''; 824 825 serviceConfig = { 826 Type = "simple"; 827 User = cfg.user; 828 Group = cfg.group; 829 WorkingDirectory = cfg.stateDir; 830 ExecStart = "${exe} web --pid /run/gitea/gitea.pid"; 831 Restart = "always"; 832 # Runtime directory and mode 833 RuntimeDirectory = "gitea"; 834 RuntimeDirectoryMode = "0755"; 835 # Proc filesystem 836 ProcSubset = "pid"; 837 ProtectProc = "invisible"; 838 # Access write directories 839 ReadWritePaths = [ 840 cfg.customDir 841 cfg.dump.backupDir 842 cfg.repositoryRoot 843 cfg.stateDir 844 cfg.lfs.contentDir 845 ]; 846 UMask = "0027"; 847 # Capabilities 848 CapabilityBoundingSet = ""; 849 # Security 850 NoNewPrivileges = true; 851 # Sandboxing 852 ProtectSystem = "strict"; 853 ProtectHome = true; 854 PrivateTmp = true; 855 PrivateDevices = true; 856 PrivateUsers = true; 857 ProtectHostname = true; 858 ProtectClock = true; 859 ProtectKernelTunables = true; 860 ProtectKernelModules = true; 861 ProtectKernelLogs = true; 862 ProtectControlGroups = true; 863 RestrictAddressFamilies = [ 864 "AF_UNIX" 865 "AF_INET" 866 "AF_INET6" 867 ]; 868 RestrictNamespaces = true; 869 LockPersonality = true; 870 MemoryDenyWriteExecute = true; 871 RestrictRealtime = true; 872 RestrictSUIDSGID = true; 873 RemoveIPC = true; 874 PrivateMounts = true; 875 # System Call Filtering 876 SystemCallArchitectures = "native"; 877 SystemCallFilter = [ 878 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid" 879 "setrlimit" 880 ]; 881 }; 882 883 environment = { 884 USER = cfg.user; 885 HOME = cfg.stateDir; 886 GITEA_WORK_DIR = cfg.stateDir; 887 GITEA_CUSTOM = cfg.customDir; 888 }; 889 }; 890 891 users.users = mkIf (cfg.user == "gitea") { 892 gitea = { 893 description = "Gitea Service"; 894 home = cfg.stateDir; 895 useDefaultShell = true; 896 group = cfg.group; 897 isSystemUser = true; 898 }; 899 }; 900 901 users.groups = mkIf (cfg.group == "gitea") { 902 gitea = { }; 903 }; 904 905 warnings = 906 optional (cfg.database.password != "") 907 "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." 908 ++ optional (cfg.extraConfig != null) '' 909 services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`. 910 '' 911 ++ optional (lib.getName cfg.package == "forgejo") '' 912 Running forgejo via services.gitea.package is no longer supported. 913 Please use services.forgejo instead. 914 See https://nixos.org/manual/nixos/unstable/#module-forgejo for migration instructions. 915 ''; 916 917 # Create database passwordFile default when password is configured. 918 services.gitea.database.passwordFile = mkDefault ( 919 toString ( 920 pkgs.writeTextFile { 921 name = "gitea-database-password"; 922 text = cfg.database.password; 923 } 924 ) 925 ); 926 927 systemd.services.gitea-dump = mkIf cfg.dump.enable { 928 description = "gitea dump"; 929 after = [ "gitea.service" ]; 930 path = [ cfg.package ]; 931 932 environment = { 933 USER = cfg.user; 934 HOME = cfg.stateDir; 935 GITEA_WORK_DIR = cfg.stateDir; 936 GITEA_CUSTOM = cfg.customDir; 937 }; 938 939 serviceConfig = { 940 Type = "oneshot"; 941 User = cfg.user; 942 ExecStart = 943 "${exe} dump --type ${cfg.dump.type}" 944 + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; 945 WorkingDirectory = cfg.dump.backupDir; 946 }; 947 }; 948 949 systemd.timers.gitea-dump = mkIf cfg.dump.enable { 950 description = "Update timer for gitea-dump"; 951 partOf = [ "gitea-dump.service" ]; 952 wantedBy = [ "timers.target" ]; 953 timerConfig.OnCalendar = cfg.dump.interval; 954 }; 955 }; 956 meta.maintainers = with lib.maintainers; [ 957 ma27 958 techknowlogick 959 SuperSandro2000 960 ]; 961}