1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8
9let
10 cfg = config.services.forgejo;
11 opt = options.services.forgejo;
12 format = pkgs.formats.ini { };
13
14 exe = lib.getExe cfg.package;
15
16 pg = config.services.postgresql;
17 useMysql = cfg.database.type == "mysql";
18 usePostgresql = cfg.database.type == "postgres";
19 useSqlite = cfg.database.type == "sqlite3";
20
21 secrets =
22 let
23 mkSecret =
24 section: values:
25 lib.mapAttrsToList (key: value: {
26 env = envEscape "FORGEJO__${section}__${key}__FILE";
27 path = value;
28 }) values;
29 # https://codeberg.org/forgejo/forgejo/src/tag/v7.0.2/contrib/environment-to-ini/environment-to-ini.go
30 envEscape =
31 string: lib.replaceStrings [ "." "-" ] [ "_0X2E_" "_0X2D_" ] (lib.strings.toUpper string);
32 in
33 lib.flatten (lib.mapAttrsToList mkSecret cfg.secrets);
34
35 inherit (lib)
36 literalExpression
37 mkChangedOptionModule
38 mkDefault
39 mkEnableOption
40 mkIf
41 mkMerge
42 mkOption
43 mkPackageOption
44 mkRemovedOptionModule
45 mkRenamedOptionModule
46 optionalAttrs
47 optionals
48 optionalString
49 types
50 ;
51in
52{
53 imports = [
54 (mkRenamedOptionModule
55 [ "services" "forgejo" "appName" ]
56 [ "services" "forgejo" "settings" "DEFAULT" "APP_NAME" ]
57 )
58 (mkRemovedOptionModule [ "services" "forgejo" "extraConfig" ]
59 "services.forgejo.extraConfig has been removed. Please use the freeform services.forgejo.settings option instead"
60 )
61 (mkRemovedOptionModule [ "services" "forgejo" "database" "password" ]
62 "services.forgejo.database.password has been removed. Please use services.forgejo.database.passwordFile instead"
63 )
64 (mkRenamedOptionModule
65 [ "services" "forgejo" "mailerPasswordFile" ]
66 [ "services" "forgejo" "secrets" "mailer" "PASSWD" ]
67 )
68
69 # copied from services.gitea; remove at some point
70 (mkRenamedOptionModule
71 [ "services" "forgejo" "cookieSecure" ]
72 [ "services" "forgejo" "settings" "session" "COOKIE_SECURE" ]
73 )
74 (mkRenamedOptionModule
75 [ "services" "forgejo" "disableRegistration" ]
76 [ "services" "forgejo" "settings" "service" "DISABLE_REGISTRATION" ]
77 )
78 (mkRenamedOptionModule
79 [ "services" "forgejo" "domain" ]
80 [ "services" "forgejo" "settings" "server" "DOMAIN" ]
81 )
82 (mkRenamedOptionModule
83 [ "services" "forgejo" "httpAddress" ]
84 [ "services" "forgejo" "settings" "server" "HTTP_ADDR" ]
85 )
86 (mkRenamedOptionModule
87 [ "services" "forgejo" "httpPort" ]
88 [ "services" "forgejo" "settings" "server" "HTTP_PORT" ]
89 )
90 (mkRenamedOptionModule
91 [ "services" "forgejo" "log" "level" ]
92 [ "services" "forgejo" "settings" "log" "LEVEL" ]
93 )
94 (mkRenamedOptionModule
95 [ "services" "forgejo" "log" "rootPath" ]
96 [ "services" "forgejo" "settings" "log" "ROOT_PATH" ]
97 )
98 (mkRenamedOptionModule
99 [ "services" "forgejo" "rootUrl" ]
100 [ "services" "forgejo" "settings" "server" "ROOT_URL" ]
101 )
102 (mkRenamedOptionModule
103 [ "services" "forgejo" "ssh" "clonePort" ]
104 [ "services" "forgejo" "settings" "server" "SSH_PORT" ]
105 )
106 (mkRenamedOptionModule
107 [ "services" "forgejo" "staticRootPath" ]
108 [ "services" "forgejo" "settings" "server" "STATIC_ROOT_PATH" ]
109 )
110 (mkChangedOptionModule
111 [ "services" "forgejo" "enableUnixSocket" ]
112 [ "services" "forgejo" "settings" "server" "PROTOCOL" ]
113 (config: if config.services.forgejo.enableUnixSocket then "http+unix" else "http")
114 )
115 (mkRemovedOptionModule [ "services" "forgejo" "ssh" "enable" ]
116 "services.forgejo.ssh.enable has been migrated into freeform setting services.forgejo.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted"
117 )
118 ];
119
120 options = {
121 services.forgejo = {
122 enable = mkEnableOption "Forgejo, a software forge";
123
124 package = mkPackageOption pkgs "forgejo-lts" { };
125
126 useWizard = mkOption {
127 default = false;
128 type = types.bool;
129 description = ''
130 Whether to use the built-in installation wizard instead of
131 declaratively managing the {file}`app.ini` config file in nix.
132 '';
133 };
134
135 stateDir = mkOption {
136 default = "/var/lib/forgejo";
137 type = types.str;
138 description = "Forgejo data directory.";
139 };
140
141 customDir = mkOption {
142 default = "${cfg.stateDir}/custom";
143 defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
144 type = types.str;
145 description = ''
146 Base directory for custom templates and other options.
147
148 If {option}`${opt.useWizard}` is disabled (default), this directory will also
149 hold secrets and the resulting {file}`app.ini` config at runtime.
150 '';
151 };
152
153 user = mkOption {
154 type = types.str;
155 default = "forgejo";
156 description = "User account under which Forgejo runs.";
157 };
158
159 group = mkOption {
160 type = types.str;
161 default = "forgejo";
162 description = "Group under which Forgejo runs.";
163 };
164
165 database = {
166 type = mkOption {
167 type = types.enum [
168 "sqlite3"
169 "mysql"
170 "postgres"
171 ];
172 example = "mysql";
173 default = "sqlite3";
174 description = "Database engine to use.";
175 };
176
177 host = mkOption {
178 type = types.str;
179 default = "127.0.0.1";
180 description = "Database host address.";
181 };
182
183 port = mkOption {
184 type = types.port;
185 default = if usePostgresql then pg.settings.port else 3306;
186 defaultText = literalExpression ''
187 if config.${opt.database.type} != "postgresql"
188 then 3306
189 else 5432
190 '';
191 description = "Database host port.";
192 };
193
194 name = mkOption {
195 type = types.str;
196 default = "forgejo";
197 description = "Database name.";
198 };
199
200 user = mkOption {
201 type = types.str;
202 default = "forgejo";
203 description = "Database user.";
204 };
205
206 passwordFile = mkOption {
207 type = types.nullOr types.path;
208 default = null;
209 example = "/run/keys/forgejo-dbpassword";
210 description = ''
211 A file containing the password corresponding to
212 {option}`${opt.database.user}`.
213 '';
214 };
215
216 socket = mkOption {
217 type = types.nullOr types.path;
218 default =
219 if (cfg.database.createDatabase && usePostgresql) then
220 "/run/postgresql"
221 else if (cfg.database.createDatabase && useMysql) then
222 "/run/mysqld/mysqld.sock"
223 else
224 null;
225 defaultText = literalExpression "null";
226 example = "/run/mysqld/mysqld.sock";
227 description = "Path to the unix socket file to use for authentication.";
228 };
229
230 path = mkOption {
231 type = types.str;
232 default = "${cfg.stateDir}/data/forgejo.db";
233 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/forgejo.db"'';
234 description = "Path to the sqlite3 database file.";
235 };
236
237 createDatabase = mkOption {
238 type = types.bool;
239 default = true;
240 description = "Whether to create a local database automatically.";
241 };
242 };
243
244 dump = {
245 enable = mkEnableOption "periodic dumps via the [built-in {command}`dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump)";
246
247 interval = mkOption {
248 type = types.str;
249 default = "04:31";
250 example = "hourly";
251 description = ''
252 Run a Forgejo dump at this interval. Runs by default at 04:31 every day.
253
254 The format is described in
255 {manpage}`systemd.time(7)`.
256 '';
257 };
258
259 backupDir = mkOption {
260 type = types.str;
261 default = "${cfg.stateDir}/dump";
262 defaultText = literalExpression ''"''${config.${opt.stateDir}}/dump"'';
263 description = "Path to the directory where the dump archives will be stored.";
264 };
265
266 type = mkOption {
267 type = types.enum [
268 "zip"
269 "tar"
270 "tar.sz"
271 "tar.gz"
272 "tar.xz"
273 "tar.bz2"
274 "tar.br"
275 "tar.lz4"
276 "tar.zst"
277 ];
278 default = "zip";
279 description = "Archive format used to store the dump file.";
280 };
281
282 file = mkOption {
283 type = types.nullOr types.str;
284 default = null;
285 description = "Filename to be used for the dump. If `null` a default name is chosen by forgejo.";
286 example = "forgejo-dump";
287 };
288 };
289
290 lfs = {
291 enable = mkOption {
292 type = types.bool;
293 default = false;
294 description = "Enables git-lfs support.";
295 };
296
297 contentDir = mkOption {
298 type = types.str;
299 default = "${cfg.stateDir}/data/lfs";
300 defaultText = literalExpression ''"''${config.${opt.stateDir}}/data/lfs"'';
301 description = "Where to store LFS files.";
302 };
303 };
304
305 repositoryRoot = mkOption {
306 type = types.str;
307 default = "${cfg.stateDir}/repositories";
308 defaultText = literalExpression ''"''${config.${opt.stateDir}}/repositories"'';
309 description = "Path to the git repositories.";
310 };
311
312 settings = mkOption {
313 default = { };
314 description = ''
315 Free-form settings written directly to the `app.ini` configfile file.
316 Refer to <https://forgejo.org/docs/latest/admin/config-cheat-sheet/> for supported values.
317 '';
318 example = literalExpression ''
319 {
320 DEFAULT = {
321 RUN_MODE = "dev";
322 };
323 "cron.sync_external_users" = {
324 RUN_AT_START = true;
325 SCHEDULE = "@every 24h";
326 UPDATE_EXISTING = true;
327 };
328 mailer = {
329 ENABLED = true;
330 MAILER_TYPE = "sendmail";
331 FROM = "do-not-reply@example.org";
332 SENDMAIL_PATH = "''${pkgs.system-sendmail}/bin/sendmail";
333 };
334 other = {
335 SHOW_FOOTER_VERSION = false;
336 };
337 }
338 '';
339 type = types.submodule {
340 freeformType = format.type;
341 options = {
342 log = {
343 ROOT_PATH = mkOption {
344 default = "${cfg.stateDir}/log";
345 defaultText = literalExpression ''"''${config.${opt.stateDir}}/log"'';
346 type = types.str;
347 description = "Root path for log files.";
348 };
349 LEVEL = mkOption {
350 default = "Info";
351 type = types.enum [
352 "Trace"
353 "Debug"
354 "Info"
355 "Warn"
356 "Error"
357 "Critical"
358 ];
359 description = "General log level.";
360 };
361 };
362
363 server = {
364 PROTOCOL = mkOption {
365 type = types.enum [
366 "http"
367 "https"
368 "fcgi"
369 "http+unix"
370 "fcgi+unix"
371 ];
372 default = "http";
373 description = ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
374 };
375
376 HTTP_ADDR = mkOption {
377 type = types.either types.str types.path;
378 default =
379 if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then
380 "/run/forgejo/forgejo.sock"
381 else
382 "0.0.0.0";
383 defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/forgejo/forgejo.sock" else "0.0.0.0"'';
384 description = "Listen address. Must be a path when using a unix socket.";
385 };
386
387 HTTP_PORT = mkOption {
388 type = types.port;
389 default = 3000;
390 description = "Listen port. Ignored when using a unix socket.";
391 };
392
393 DOMAIN = mkOption {
394 type = types.str;
395 default = "localhost";
396 description = "Domain name of your server.";
397 };
398
399 ROOT_URL = mkOption {
400 type = types.str;
401 default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
402 defaultText = literalExpression ''"http://''${config.services.forgejo.settings.server.DOMAIN}:''${toString config.services.forgejo.settings.server.HTTP_PORT}/"'';
403 description = "Full public URL of Forgejo server.";
404 };
405
406 STATIC_ROOT_PATH = mkOption {
407 type = types.either types.str types.path;
408 default = cfg.package.data;
409 defaultText = literalExpression "config.${opt.package}.data";
410 example = "/var/lib/forgejo/data";
411 description = "Upper level of template and static files path.";
412 };
413
414 DISABLE_SSH = mkOption {
415 type = types.bool;
416 default = false;
417 description = "Disable external SSH feature.";
418 };
419
420 SSH_PORT = mkOption {
421 type = types.port;
422 default = 22;
423 example = 2222;
424 description = ''
425 SSH port displayed in clone URL.
426 The option is required to configure a service when the external visible port
427 differs from the local listening port i.e. if port forwarding is used.
428 '';
429 };
430 };
431
432 session = {
433 COOKIE_SECURE = mkOption {
434 type = types.bool;
435 default = false;
436 description = ''
437 Marks session cookies as "secure" as a hint for browsers to only send
438 them via HTTPS. This option is recommend, if Forgejo is being served over HTTPS.
439 '';
440 };
441 };
442 };
443 };
444 };
445
446 secrets = mkOption {
447 default = { };
448 description = ''
449 This is a small wrapper over systemd's `LoadCredential`.
450
451 It takes the same sections and keys as {option}`services.forgejo.settings`,
452 but the value of each key is a path instead of a string or bool.
453
454 The path is then loaded as credential, exported as environment variable
455 and then feed through
456 <https://codeberg.org/forgejo/forgejo/src/branch/forgejo/contrib/environment-to-ini/environment-to-ini.go>.
457
458 It does the required environment variable escaping for you.
459
460 ::: {.note}
461 Keys specified here take priority over the ones in {option}`services.forgejo.settings`!
462 :::
463 '';
464 example = literalExpression ''
465 {
466 metrics = {
467 TOKEN = "/run/keys/forgejo-metrics-token";
468 };
469 camo = {
470 HMAC_KEY = "/run/keys/forgejo-camo-hmac";
471 };
472 service = {
473 HCAPTCHA_SECRET = "/run/keys/forgejo-hcaptcha-secret";
474 HCAPTCHA_SITEKEY = "/run/keys/forgejo-hcaptcha-sitekey";
475 };
476 }
477 '';
478 type = types.submodule {
479 freeformType = with types; attrsOf (attrsOf path);
480 options = { };
481 };
482 };
483 };
484 };
485
486 config = mkIf cfg.enable {
487 assertions = [
488 {
489 assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
490 message = "services.forgejo.database.user must match services.forgejo.user if the database is to be automatically provisioned";
491 }
492 {
493 assertion = cfg.database.createDatabase && usePostgresql -> cfg.database.user == cfg.database.name;
494 message = ''
495 When creating a database via NixOS, the db user and db name must be equal!
496 If you already have an existing DB+user and this assertion is new, you can safely set
497 `services.forgejo.createDatabase` to `false` because removal of `ensureUsers`
498 and `ensureDatabases` doesn't have any effect.
499 '';
500 }
501 ];
502
503 services.forgejo.settings = {
504 DEFAULT = {
505 RUN_MODE = mkDefault "prod";
506 RUN_USER = mkDefault cfg.user;
507 WORK_PATH = mkDefault cfg.stateDir;
508 };
509
510 database = mkMerge [
511 {
512 DB_TYPE = cfg.database.type;
513 }
514 (mkIf (useMysql || usePostgresql) {
515 HOST =
516 if cfg.database.socket != null then
517 cfg.database.socket
518 else
519 cfg.database.host + ":" + toString cfg.database.port;
520 NAME = cfg.database.name;
521 USER = cfg.database.user;
522 })
523 (mkIf useSqlite {
524 PATH = cfg.database.path;
525 })
526 (mkIf usePostgresql {
527 SSL_MODE = "disable";
528 })
529 ];
530
531 repository = {
532 ROOT = cfg.repositoryRoot;
533 };
534
535 server = mkIf cfg.lfs.enable {
536 LFS_START_SERVER = true;
537 };
538
539 session = {
540 COOKIE_NAME = mkDefault "session";
541 };
542
543 security = {
544 INSTALL_LOCK = true;
545 };
546
547 lfs = mkIf cfg.lfs.enable {
548 PATH = cfg.lfs.contentDir;
549 };
550 };
551
552 services.forgejo.secrets = {
553 security = {
554 SECRET_KEY = "${cfg.customDir}/conf/secret_key";
555 INTERNAL_TOKEN = "${cfg.customDir}/conf/internal_token";
556 };
557
558 oauth2 = {
559 JWT_SECRET = "${cfg.customDir}/conf/oauth2_jwt_secret";
560 };
561
562 database = mkIf (cfg.database.passwordFile != null) {
563 PASSWD = cfg.database.passwordFile;
564 };
565
566 server = mkIf cfg.lfs.enable {
567 LFS_JWT_SECRET = "${cfg.customDir}/conf/lfs_jwt_secret";
568 };
569 };
570
571 services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
572 enable = mkDefault true;
573
574 ensureDatabases = [ cfg.database.name ];
575 ensureUsers = [
576 {
577 name = cfg.database.user;
578 ensureDBOwnership = true;
579 }
580 ];
581 };
582
583 services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) {
584 enable = mkDefault true;
585 package = mkDefault pkgs.mariadb;
586
587 ensureDatabases = [ cfg.database.name ];
588 ensureUsers = [
589 {
590 name = cfg.database.user;
591 ensurePermissions = {
592 "${cfg.database.name}.*" = "ALL PRIVILEGES";
593 };
594 }
595 ];
596 };
597
598 systemd.tmpfiles.rules =
599 [
600 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
601 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
602 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
603 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
604 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
605 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
606 "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
607 "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
608 "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
609 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
610 "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
611 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
612 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
613 "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
614 "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
615 "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
616 "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
617
618 # If we have a folder or symlink with Forgejo locales, remove it
619 # And symlink the current Forgejo locales in place
620 "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
621
622 ]
623 ++ optionals cfg.lfs.enable [
624 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
625 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
626 ];
627
628 systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) {
629 description = "Forgejo secret bootstrap helper";
630 script = ''
631 if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then
632 ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}'
633 fi
634
635 if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then
636 ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}'
637 fi
638
639 ${optionalString cfg.lfs.enable ''
640 if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then
641 ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}'
642 fi
643 ''}
644
645 if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then
646 ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}'
647 fi
648 '';
649 serviceConfig = {
650 Type = "oneshot";
651 RemainAfterExit = true;
652 User = cfg.user;
653 Group = cfg.group;
654 ReadWritePaths = [ cfg.customDir ];
655 UMask = "0077";
656 };
657 };
658
659 systemd.services.forgejo = {
660 description = "Forgejo (Beyond coding. We forge.)";
661 after =
662 [
663 "network.target"
664 ]
665 ++ optionals usePostgresql [
666 "postgresql.service"
667 ]
668 ++ optionals useMysql [
669 "mysql.service"
670 ]
671 ++ optionals (!cfg.useWizard) [
672 "forgejo-secrets.service"
673 ];
674 requires =
675 optionals (cfg.database.createDatabase && usePostgresql) [
676 "postgresql.service"
677 ]
678 ++ optionals (cfg.database.createDatabase && useMysql) [
679 "mysql.service"
680 ]
681 ++ optionals (!cfg.useWizard) [
682 "forgejo-secrets.service"
683 ];
684 wantedBy = [ "multi-user.target" ];
685 path = [
686 cfg.package
687 pkgs.git
688 pkgs.gnupg
689 ];
690
691 # In older versions the secret naming for JWT was kind of confusing.
692 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
693 # wasn't persistent at all.
694 # To fix that, there is now the file oauth2_jwt_secret containing the
695 # values for JWT_SECRET and the file jwt_secret gets renamed to
696 # lfs_jwt_secret.
697 # We have to consider this to stay compatible with older installations.
698 preStart = ''
699 ${optionalString (!cfg.useWizard) ''
700 function forgejo_setup {
701 config='${cfg.customDir}/conf/app.ini'
702 cp -f '${format.generate "app.ini" cfg.settings}' "$config"
703
704 chmod u+w "$config"
705 ${lib.getExe' cfg.package "environment-to-ini"} --config "$config"
706 chmod u-w "$config"
707 }
708 (umask 027; forgejo_setup)
709 ''}
710
711 # run migrations/init the database
712 ${exe} migrate
713
714 # update all hooks' binary paths
715 ${exe} admin regenerate hooks
716
717 # update command option in authorized_keys
718 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
719 then
720 ${exe} admin regenerate keys
721 fi
722 '';
723
724 serviceConfig = {
725 Type = "notify";
726 User = cfg.user;
727 Group = cfg.group;
728 WorkingDirectory = cfg.stateDir;
729 ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid";
730 Restart = "always";
731 # Runtime directory and mode
732 RuntimeDirectory = "forgejo";
733 RuntimeDirectoryMode = "0755";
734 # Proc filesystem
735 ProcSubset = "pid";
736 ProtectProc = "invisible";
737 # Access write directories
738 ReadWritePaths = [
739 cfg.customDir
740 cfg.dump.backupDir
741 cfg.repositoryRoot
742 cfg.stateDir
743 cfg.lfs.contentDir
744 ];
745 UMask = "0027";
746 # Capabilities
747 CapabilityBoundingSet = "";
748 # Security
749 NoNewPrivileges = true;
750 # Sandboxing
751 ProtectSystem = "strict";
752 ProtectHome = true;
753 PrivateTmp = true;
754 PrivateDevices = true;
755 PrivateUsers = true;
756 ProtectHostname = true;
757 ProtectClock = true;
758 ProtectKernelTunables = true;
759 ProtectKernelModules = true;
760 ProtectKernelLogs = true;
761 ProtectControlGroups = true;
762 RestrictAddressFamilies = [
763 "AF_UNIX"
764 "AF_INET"
765 "AF_INET6"
766 ];
767 RestrictNamespaces = true;
768 LockPersonality = true;
769 MemoryDenyWriteExecute = true;
770 RestrictRealtime = true;
771 RestrictSUIDSGID = true;
772 RemoveIPC = true;
773 PrivateMounts = true;
774 # System Call Filtering
775 SystemCallArchitectures = "native";
776 SystemCallFilter = [
777 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"
778 "setrlimit"
779 ];
780 # cfg.secrets
781 LoadCredential = map (e: "${e.env}:${e.path}") secrets;
782 };
783
784 environment = {
785 USER = cfg.user;
786 HOME = cfg.stateDir;
787 FORGEJO_WORK_DIR = cfg.stateDir;
788 FORGEJO_CUSTOM = cfg.customDir;
789 } // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets);
790 };
791
792 services.openssh.settings.AcceptEnv = mkIf (
793 !cfg.settings.server.START_SSH_SERVER or false
794 ) "GIT_PROTOCOL";
795
796 users.users = mkIf (cfg.user == "forgejo") {
797 forgejo = {
798 home = cfg.stateDir;
799 useDefaultShell = true;
800 group = cfg.group;
801 isSystemUser = true;
802 };
803 };
804
805 users.groups = mkIf (cfg.group == "forgejo") {
806 forgejo = { };
807 };
808
809 systemd.services.forgejo-dump = mkIf cfg.dump.enable {
810 description = "forgejo dump";
811 after = [ "forgejo.service" ];
812 path = [ cfg.package ];
813
814 environment = {
815 USER = cfg.user;
816 HOME = cfg.stateDir;
817 FORGEJO_WORK_DIR = cfg.stateDir;
818 FORGEJO_CUSTOM = cfg.customDir;
819 };
820
821 serviceConfig = {
822 Type = "oneshot";
823 User = cfg.user;
824 ExecStart =
825 "${exe} dump --type ${cfg.dump.type}"
826 + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
827 WorkingDirectory = cfg.dump.backupDir;
828 };
829 };
830
831 systemd.timers.forgejo-dump = mkIf cfg.dump.enable {
832 description = "Forgejo dump timer";
833 partOf = [ "forgejo-dump.service" ];
834 wantedBy = [ "timers.target" ];
835 timerConfig.OnCalendar = cfg.dump.interval;
836 };
837 };
838
839 meta.doc = ./forgejo.md;
840 meta.maintainers = with lib.maintainers; [
841 bendlas
842 emilylange
843 pyrox0
844 ];
845}