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