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