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}