at 23.05-pre 23 kB view raw
1{ config, lib, options, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.gitea; 7 opt = options.services.gitea; 8 gitea = cfg.package; 9 pg = config.services.postgresql; 10 useMysql = cfg.database.type == "mysql"; 11 usePostgresql = cfg.database.type == "postgres"; 12 useSqlite = cfg.database.type == "sqlite3"; 13 format = pkgs.formats.ini { }; 14 configFile = pkgs.writeText "app.ini" '' 15 APP_NAME = ${cfg.appName} 16 RUN_USER = ${cfg.user} 17 RUN_MODE = prod 18 19 ${generators.toINI {} cfg.settings} 20 21 ${optionalString (cfg.extraConfig != null) cfg.extraConfig} 22 ''; 23in 24 25{ 26 imports = [ 27 (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ]) 28 (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ]) 29 (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ]) 30 (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ]) 31 (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ]) 32 33 (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted") 34 ]; 35 36 options = { 37 services.gitea = { 38 enable = mkOption { 39 default = false; 40 type = types.bool; 41 description = lib.mdDoc "Enable Gitea Service."; 42 }; 43 44 package = mkOption { 45 default = pkgs.gitea; 46 type = types.package; 47 defaultText = literalExpression "pkgs.gitea"; 48 description = lib.mdDoc "gitea derivation to use"; 49 }; 50 51 useWizard = mkOption { 52 default = false; 53 type = types.bool; 54 description = lib.mdDoc "Do not generate a configuration and use gitea' installation wizard instead. The first registered user will be administrator."; 55 }; 56 57 stateDir = mkOption { 58 default = "/var/lib/gitea"; 59 type = types.str; 60 description = lib.mdDoc "gitea data directory."; 61 }; 62 63 user = mkOption { 64 type = types.str; 65 default = "gitea"; 66 description = lib.mdDoc "User account under which gitea runs."; 67 }; 68 69 database = { 70 type = mkOption { 71 type = types.enum [ "sqlite3" "mysql" "postgres" ]; 72 example = "mysql"; 73 default = "sqlite3"; 74 description = lib.mdDoc "Database engine to use."; 75 }; 76 77 host = mkOption { 78 type = types.str; 79 default = "127.0.0.1"; 80 description = lib.mdDoc "Database host address."; 81 }; 82 83 port = mkOption { 84 type = types.port; 85 default = if !usePostgresql then 3306 else pg.port; 86 defaultText = literalExpression '' 87 if config.${opt.database.type} != "postgresql" 88 then 3306 89 else config.${options.services.postgresql.port} 90 ''; 91 description = lib.mdDoc "Database host port."; 92 }; 93 94 name = mkOption { 95 type = types.str; 96 default = "gitea"; 97 description = lib.mdDoc "Database name."; 98 }; 99 100 user = mkOption { 101 type = types.str; 102 default = "gitea"; 103 description = lib.mdDoc "Database user."; 104 }; 105 106 password = mkOption { 107 type = types.str; 108 default = ""; 109 description = lib.mdDoc '' 110 The password corresponding to {option}`database.user`. 111 Warning: this is stored in cleartext in the Nix store! 112 Use {option}`database.passwordFile` instead. 113 ''; 114 }; 115 116 passwordFile = mkOption { 117 type = types.nullOr types.path; 118 default = null; 119 example = "/run/keys/gitea-dbpassword"; 120 description = lib.mdDoc '' 121 A file containing the password corresponding to 122 {option}`database.user`. 123 ''; 124 }; 125 126 socket = mkOption { 127 type = types.nullOr types.path; 128 default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null; 129 defaultText = literalExpression "null"; 130 example = "/run/mysqld/mysqld.sock"; 131 description = lib.mdDoc "Path to the unix socket file to use for authentication."; 132 }; 133 134 path = mkOption { 135 type = types.str; 136 default = "${cfg.stateDir}/data/gitea.db"; 137 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/gitea.db"''; 138 description = lib.mdDoc "Path to the sqlite3 database file."; 139 }; 140 141 createDatabase = mkOption { 142 type = types.bool; 143 default = true; 144 description = lib.mdDoc "Whether to create a local database automatically."; 145 }; 146 }; 147 148 dump = { 149 enable = mkOption { 150 type = types.bool; 151 default = false; 152 description = lib.mdDoc '' 153 Enable a timer that runs gitea dump to generate backup-files of the 154 current gitea database and repositories. 155 ''; 156 }; 157 158 interval = mkOption { 159 type = types.str; 160 default = "04:31"; 161 example = "hourly"; 162 description = lib.mdDoc '' 163 Run a gitea dump at this interval. Runs by default at 04:31 every day. 164 165 The format is described in 166 {manpage}`systemd.time(7)`. 167 ''; 168 }; 169 170 backupDir = mkOption { 171 type = types.str; 172 default = "${cfg.stateDir}/dump"; 173 defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"''; 174 description = lib.mdDoc "Path to the dump files."; 175 }; 176 177 type = mkOption { 178 type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ]; 179 default = "zip"; 180 description = lib.mdDoc "Archive format used to store the dump file."; 181 }; 182 183 file = mkOption { 184 type = types.nullOr types.str; 185 default = null; 186 description = lib.mdDoc "Filename to be used for the dump. If `null` a default name is choosen by gitea."; 187 example = "gitea-dump"; 188 }; 189 }; 190 191 lfs = { 192 enable = mkOption { 193 type = types.bool; 194 default = false; 195 description = lib.mdDoc "Enables git-lfs support."; 196 }; 197 198 contentDir = mkOption { 199 type = types.str; 200 default = "${cfg.stateDir}/data/lfs"; 201 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"''; 202 description = lib.mdDoc "Where to store LFS files."; 203 }; 204 }; 205 206 appName = mkOption { 207 type = types.str; 208 default = "gitea: Gitea Service"; 209 description = lib.mdDoc "Application name."; 210 }; 211 212 repositoryRoot = mkOption { 213 type = types.str; 214 default = "${cfg.stateDir}/repositories"; 215 defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"''; 216 description = lib.mdDoc "Path to the git repositories."; 217 }; 218 219 domain = mkOption { 220 type = types.str; 221 default = "localhost"; 222 description = lib.mdDoc "Domain name of your server."; 223 }; 224 225 rootUrl = mkOption { 226 type = types.str; 227 default = "http://localhost:3000/"; 228 description = lib.mdDoc "Full public URL of gitea server."; 229 }; 230 231 httpAddress = mkOption { 232 type = types.str; 233 default = "0.0.0.0"; 234 description = lib.mdDoc "HTTP listen address."; 235 }; 236 237 httpPort = mkOption { 238 type = types.int; 239 default = 3000; 240 description = lib.mdDoc "HTTP listen port."; 241 }; 242 243 enableUnixSocket = mkOption { 244 type = types.bool; 245 default = false; 246 description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port."; 247 }; 248 249 staticRootPath = mkOption { 250 type = types.either types.str types.path; 251 default = gitea.data; 252 defaultText = literalExpression "package.data"; 253 example = "/var/lib/gitea/data"; 254 description = lib.mdDoc "Upper level of template and static files path."; 255 }; 256 257 mailerPasswordFile = mkOption { 258 type = types.nullOr types.str; 259 default = null; 260 example = "/var/lib/secrets/gitea/mailpw"; 261 description = lib.mdDoc "Path to a file containing the SMTP password."; 262 }; 263 264 settings = mkOption { 265 default = {}; 266 description = lib.mdDoc '' 267 Gitea configuration. Refer to <https://docs.gitea.io/en-us/config-cheat-sheet/> 268 for details on supported values. 269 ''; 270 example = literalExpression '' 271 { 272 "cron.sync_external_users" = { 273 RUN_AT_START = true; 274 SCHEDULE = "@every 24h"; 275 UPDATE_EXISTING = true; 276 }; 277 mailer = { 278 ENABLED = true; 279 MAILER_TYPE = "sendmail"; 280 FROM = "do-not-reply@example.org"; 281 SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail"; 282 }; 283 other = { 284 SHOW_FOOTER_VERSION = false; 285 }; 286 } 287 ''; 288 type = with types; submodule { 289 freeformType = format.type; 290 options = { 291 log = { 292 ROOT_PATH = mkOption { 293 default = "${cfg.stateDir}/log"; 294 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"''; 295 type = types.str; 296 description = lib.mdDoc "Root path for log files."; 297 }; 298 LEVEL = mkOption { 299 default = "Info"; 300 type = types.enum [ "Trace" "Debug" "Info" "Warn" "Error" "Critical" ]; 301 description = lib.mdDoc "General log level."; 302 }; 303 }; 304 305 server = { 306 DISABLE_SSH = mkOption { 307 type = types.bool; 308 default = false; 309 description = lib.mdDoc "Disable external SSH feature."; 310 }; 311 312 SSH_PORT = mkOption { 313 type = types.int; 314 default = 22; 315 example = 2222; 316 description = lib.mdDoc '' 317 SSH port displayed in clone URL. 318 The option is required to configure a service when the external visible port 319 differs from the local listening port i.e. if port forwarding is used. 320 ''; 321 }; 322 }; 323 324 service = { 325 DISABLE_REGISTRATION = mkEnableOption (lib.mdDoc "the registration lock") // { 326 description = lib.mdDoc '' 327 By default any user can create an account on this `gitea` instance. 328 This can be disabled by using this option. 329 330 *Note:* please keep in mind that this should be added after the initial 331 deploy unless [](#opt-services.gitea.useWizard) 332 is `true` as the first registered user will be the administrator if 333 no install wizard is used. 334 ''; 335 }; 336 }; 337 338 session = { 339 COOKIE_SECURE = mkOption { 340 type = types.bool; 341 default = false; 342 description = lib.mdDoc '' 343 Marks session cookies as "secure" as a hint for browsers to only send 344 them via HTTPS. This option is recommend, if gitea is being served over HTTPS. 345 ''; 346 }; 347 }; 348 }; 349 }; 350 }; 351 352 extraConfig = mkOption { 353 type = with types; nullOr str; 354 default = null; 355 description = lib.mdDoc "Configuration lines appended to the generated gitea configuration file."; 356 }; 357 }; 358 }; 359 360 config = mkIf cfg.enable { 361 assertions = [ 362 { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user; 363 message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned"; 364 } 365 ]; 366 367 services.gitea.settings = { 368 database = mkMerge [ 369 { 370 DB_TYPE = cfg.database.type; 371 } 372 (mkIf (useMysql || usePostgresql) { 373 HOST = if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port; 374 NAME = cfg.database.name; 375 USER = cfg.database.user; 376 PASSWD = "#dbpass#"; 377 }) 378 (mkIf useSqlite { 379 PATH = cfg.database.path; 380 }) 381 (mkIf usePostgresql { 382 SSL_MODE = "disable"; 383 }) 384 ]; 385 386 repository = { 387 ROOT = cfg.repositoryRoot; 388 }; 389 390 server = mkMerge [ 391 { 392 DOMAIN = cfg.domain; 393 STATIC_ROOT_PATH = toString cfg.staticRootPath; 394 LFS_JWT_SECRET = "#lfsjwtsecret#"; 395 ROOT_URL = cfg.rootUrl; 396 } 397 (mkIf cfg.enableUnixSocket { 398 PROTOCOL = "unix"; 399 HTTP_ADDR = "/run/gitea/gitea.sock"; 400 }) 401 (mkIf (!cfg.enableUnixSocket) { 402 HTTP_ADDR = cfg.httpAddress; 403 HTTP_PORT = cfg.httpPort; 404 }) 405 (mkIf cfg.lfs.enable { 406 LFS_START_SERVER = true; 407 LFS_CONTENT_PATH = cfg.lfs.contentDir; 408 }) 409 410 ]; 411 412 session = { 413 COOKIE_NAME = lib.mkDefault "session"; 414 }; 415 416 security = { 417 SECRET_KEY = "#secretkey#"; 418 INTERNAL_TOKEN = "#internaltoken#"; 419 INSTALL_LOCK = true; 420 }; 421 422 mailer = mkIf (cfg.mailerPasswordFile != null) { 423 PASSWD = "#mailerpass#"; 424 }; 425 426 oauth2 = { 427 JWT_SECRET = "#oauth2jwtsecret#"; 428 }; 429 }; 430 431 services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { 432 enable = mkDefault true; 433 434 ensureDatabases = [ cfg.database.name ]; 435 ensureUsers = [ 436 { name = cfg.database.user; 437 ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; 438 } 439 ]; 440 }; 441 442 services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { 443 enable = mkDefault true; 444 package = mkDefault pkgs.mariadb; 445 446 ensureDatabases = [ cfg.database.name ]; 447 ensureUsers = [ 448 { name = cfg.database.user; 449 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; 450 } 451 ]; 452 }; 453 454 systemd.tmpfiles.rules = [ 455 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" 456 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" 457 "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -" 458 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" 459 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" 460 "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -" 461 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" 462 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" 463 "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -" 464 "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" 465 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" 466 "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" 467 "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" 468 "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" 469 "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" 470 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -" 471 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" 472 "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" 473 "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" 474 "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" 475 "Z '${cfg.stateDir}' - ${cfg.user} gitea - -" 476 477 # If we have a folder or symlink with gitea locales, remove it 478 # And symlink the current gitea locales in place 479 "L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale" 480 ]; 481 482 systemd.services.gitea = { 483 description = "gitea"; 484 after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; 485 wantedBy = [ "multi-user.target" ]; 486 path = [ gitea pkgs.git ]; 487 488 # In older versions the secret naming for JWT was kind of confusing. 489 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET 490 # wasn't persistant at all. 491 # To fix that, there is now the file oauth2_jwt_secret containing the 492 # values for JWT_SECRET and the file jwt_secret gets renamed to 493 # lfs_jwt_secret. 494 # We have to consider this to stay compatible with older installations. 495 preStart = let 496 runConfig = "${cfg.stateDir}/custom/conf/app.ini"; 497 secretKey = "${cfg.stateDir}/custom/conf/secret_key"; 498 oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret"; 499 oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET 500 lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET 501 internalToken = "${cfg.stateDir}/custom/conf/internal_token"; 502 replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; 503 in '' 504 # copy custom configuration and generate a random secret key if needed 505 ${optionalString (!cfg.useWizard) '' 506 function gitea_setup { 507 cp -f ${configFile} ${runConfig} 508 509 if [ ! -s ${secretKey} ]; then 510 ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey} 511 fi 512 513 # Migrate LFS_JWT_SECRET filename 514 if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then 515 mv ${oldLfsJwtSecret} ${lfsJwtSecret} 516 fi 517 518 if [ ! -s ${oauth2JwtSecret} ]; then 519 ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret} 520 fi 521 522 if [ ! -s ${lfsJwtSecret} ]; then 523 ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret} 524 fi 525 526 if [ ! -s ${internalToken} ]; then 527 ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken} 528 fi 529 530 chmod u+w '${runConfig}' 531 ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' 532 ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' 533 ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' 534 ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' 535 ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' 536 537 ${lib.optionalString (cfg.mailerPasswordFile != null) '' 538 ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' 539 ''} 540 chmod u-w '${runConfig}' 541 } 542 (umask 027; gitea_setup) 543 ''} 544 545 # run migrations/init the database 546 ${gitea}/bin/gitea migrate 547 548 # update all hooks' binary paths 549 ${gitea}/bin/gitea admin regenerate hooks 550 551 # update command option in authorized_keys 552 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ] 553 then 554 ${gitea}/bin/gitea admin regenerate keys 555 fi 556 ''; 557 558 serviceConfig = { 559 Type = "simple"; 560 User = cfg.user; 561 Group = "gitea"; 562 WorkingDirectory = cfg.stateDir; 563 ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid"; 564 Restart = "always"; 565 # Runtime directory and mode 566 RuntimeDirectory = "gitea"; 567 RuntimeDirectoryMode = "0755"; 568 # Access write directories 569 ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; 570 UMask = "0027"; 571 # Capabilities 572 CapabilityBoundingSet = ""; 573 # Security 574 NoNewPrivileges = true; 575 # Sandboxing 576 ProtectSystem = "strict"; 577 ProtectHome = true; 578 PrivateTmp = true; 579 PrivateDevices = true; 580 PrivateUsers = true; 581 ProtectHostname = true; 582 ProtectClock = true; 583 ProtectKernelTunables = true; 584 ProtectKernelModules = true; 585 ProtectKernelLogs = true; 586 ProtectControlGroups = true; 587 RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ]; 588 LockPersonality = true; 589 MemoryDenyWriteExecute = true; 590 RestrictRealtime = true; 591 RestrictSUIDSGID = true; 592 PrivateMounts = true; 593 # System Call Filtering 594 SystemCallArchitectures = "native"; 595 SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @setuid @swap"; 596 }; 597 598 environment = { 599 USER = cfg.user; 600 HOME = cfg.stateDir; 601 GITEA_WORK_DIR = cfg.stateDir; 602 }; 603 }; 604 605 users.users = mkIf (cfg.user == "gitea") { 606 gitea = { 607 description = "Gitea Service"; 608 home = cfg.stateDir; 609 useDefaultShell = true; 610 group = "gitea"; 611 isSystemUser = true; 612 }; 613 }; 614 615 users.groups.gitea = {}; 616 617 warnings = 618 optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++ 619 optional (cfg.extraConfig != null) '' 620 services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`. 621 ''; 622 623 # Create database passwordFile default when password is configured. 624 services.gitea.database.passwordFile = 625 mkDefault (toString (pkgs.writeTextFile { 626 name = "gitea-database-password"; 627 text = cfg.database.password; 628 })); 629 630 systemd.services.gitea-dump = mkIf cfg.dump.enable { 631 description = "gitea dump"; 632 after = [ "gitea.service" ]; 633 wantedBy = [ "default.target" ]; 634 path = [ gitea ]; 635 636 environment = { 637 USER = cfg.user; 638 HOME = cfg.stateDir; 639 GITEA_WORK_DIR = cfg.stateDir; 640 }; 641 642 serviceConfig = { 643 Type = "oneshot"; 644 User = cfg.user; 645 ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}"; 646 WorkingDirectory = cfg.dump.backupDir; 647 }; 648 }; 649 650 systemd.timers.gitea-dump = mkIf cfg.dump.enable { 651 description = "Update timer for gitea-dump"; 652 partOf = [ "gitea-dump.service" ]; 653 wantedBy = [ "timers.target" ]; 654 timerConfig.OnCalendar = cfg.dump.interval; 655 }; 656 }; 657 meta.maintainers = with lib.maintainers; [ srhb ma27 ]; 658}