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 "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
600 "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
601 "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
602 "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
603 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
604 "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
605 "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
606 "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
607 "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
608 "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
609 "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
610 "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
611 "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
612 "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
613 "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
614 "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
615 "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
616
617 # If we have a folder or symlink with Forgejo locales, remove it
618 # And symlink the current Forgejo locales in place
619 "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
620
621 ]
622 ++ optionals cfg.lfs.enable [
623 "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
624 "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
625 ];
626
627 systemd.services.forgejo-secrets = mkIf (!cfg.useWizard) {
628 description = "Forgejo secret bootstrap helper";
629 script = ''
630 if [ ! -s '${cfg.secrets.security.SECRET_KEY}' ]; then
631 ${exe} generate secret SECRET_KEY > '${cfg.secrets.security.SECRET_KEY}'
632 fi
633
634 if [ ! -s '${cfg.secrets.oauth2.JWT_SECRET}' ]; then
635 ${exe} generate secret JWT_SECRET > '${cfg.secrets.oauth2.JWT_SECRET}'
636 fi
637
638 ${optionalString cfg.lfs.enable ''
639 if [ ! -s '${cfg.secrets.server.LFS_JWT_SECRET}' ]; then
640 ${exe} generate secret LFS_JWT_SECRET > '${cfg.secrets.server.LFS_JWT_SECRET}'
641 fi
642 ''}
643
644 if [ ! -s '${cfg.secrets.security.INTERNAL_TOKEN}' ]; then
645 ${exe} generate secret INTERNAL_TOKEN > '${cfg.secrets.security.INTERNAL_TOKEN}'
646 fi
647 '';
648 serviceConfig = {
649 Type = "oneshot";
650 RemainAfterExit = true;
651 User = cfg.user;
652 Group = cfg.group;
653 ReadWritePaths = [ cfg.customDir ];
654 UMask = "0077";
655 };
656 };
657
658 systemd.services.forgejo = {
659 description = "Forgejo (Beyond coding. We forge.)";
660 after = [
661 "network.target"
662 ]
663 ++ optionals usePostgresql [
664 "postgresql.target"
665 ]
666 ++ optionals useMysql [
667 "mysql.service"
668 ]
669 ++ optionals (!cfg.useWizard) [
670 "forgejo-secrets.service"
671 ];
672 requires =
673 optionals (cfg.database.createDatabase && usePostgresql) [
674 "postgresql.target"
675 ]
676 ++ optionals (cfg.database.createDatabase && useMysql) [
677 "mysql.service"
678 ]
679 ++ optionals (!cfg.useWizard) [
680 "forgejo-secrets.service"
681 ];
682 wantedBy = [ "multi-user.target" ];
683 path = [
684 cfg.package
685 pkgs.git
686 pkgs.gnupg
687 ];
688
689 # In older versions the secret naming for JWT was kind of confusing.
690 # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
691 # wasn't persistent at all.
692 # To fix that, there is now the file oauth2_jwt_secret containing the
693 # values for JWT_SECRET and the file jwt_secret gets renamed to
694 # lfs_jwt_secret.
695 # We have to consider this to stay compatible with older installations.
696 preStart = ''
697 ${optionalString (!cfg.useWizard) ''
698 function forgejo_setup {
699 config='${cfg.customDir}/conf/app.ini'
700 cp -f '${format.generate "app.ini" cfg.settings}' "$config"
701
702 chmod u+w "$config"
703 ${lib.getExe' cfg.package "environment-to-ini"} --config "$config"
704 chmod u-w "$config"
705 }
706 (umask 027; forgejo_setup)
707 ''}
708
709 # run migrations/init the database
710 ${exe} migrate
711
712 # update all hooks' binary paths
713 ${exe} admin regenerate hooks
714
715 # update command option in authorized_keys
716 if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
717 then
718 ${exe} admin regenerate keys
719 fi
720 '';
721
722 serviceConfig = {
723 Type = "notify";
724 User = cfg.user;
725 Group = cfg.group;
726 WorkingDirectory = cfg.stateDir;
727 ExecStart = "${exe} web --pid /run/forgejo/forgejo.pid";
728 Restart = "always";
729 # Runtime directory and mode
730 RuntimeDirectory = "forgejo";
731 RuntimeDirectoryMode = "0755";
732 # Proc filesystem
733 ProcSubset = "pid";
734 ProtectProc = "invisible";
735 # Access write directories
736 ReadWritePaths = [
737 cfg.customDir
738 cfg.dump.backupDir
739 cfg.repositoryRoot
740 cfg.stateDir
741 cfg.lfs.contentDir
742 ];
743 UMask = "0027";
744 # Capabilities
745 CapabilityBoundingSet = "";
746 # Security
747 NoNewPrivileges = true;
748 # Sandboxing
749 ProtectSystem = "strict";
750 ProtectHome = true;
751 PrivateTmp = true;
752 PrivateDevices = true;
753 PrivateUsers = true;
754 ProtectHostname = true;
755 ProtectClock = true;
756 ProtectKernelTunables = true;
757 ProtectKernelModules = true;
758 ProtectKernelLogs = true;
759 ProtectControlGroups = true;
760 RestrictAddressFamilies = [
761 "AF_UNIX"
762 "AF_INET"
763 "AF_INET6"
764 ];
765 RestrictNamespaces = true;
766 LockPersonality = true;
767 MemoryDenyWriteExecute = true;
768 RestrictRealtime = true;
769 RestrictSUIDSGID = true;
770 RemoveIPC = true;
771 PrivateMounts = true;
772 # System Call Filtering
773 SystemCallArchitectures = "native";
774 SystemCallFilter = [
775 "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"
776 "setrlimit"
777 ];
778 # cfg.secrets
779 LoadCredential = map (e: "${e.env}:${e.path}") secrets;
780 };
781
782 environment = {
783 USER = cfg.user;
784 HOME = cfg.stateDir;
785 FORGEJO_WORK_DIR = cfg.stateDir;
786 FORGEJO_CUSTOM = cfg.customDir;
787 }
788 // lib.listToAttrs (map (e: lib.nameValuePair e.env "%d/${e.env}") secrets);
789 };
790
791 services.openssh.settings.AcceptEnv = mkIf (
792 !cfg.settings.server.START_SSH_SERVER or false
793 ) "GIT_PROTOCOL";
794
795 users.users = mkIf (cfg.user == "forgejo") {
796 forgejo = {
797 home = cfg.stateDir;
798 useDefaultShell = true;
799 group = cfg.group;
800 isSystemUser = true;
801 };
802 };
803
804 users.groups = mkIf (cfg.group == "forgejo") {
805 forgejo = { };
806 };
807
808 systemd.services.forgejo-dump = mkIf cfg.dump.enable {
809 description = "forgejo dump";
810 after = [ "forgejo.service" ];
811 path = [ cfg.package ];
812
813 environment = {
814 USER = cfg.user;
815 HOME = cfg.stateDir;
816 FORGEJO_WORK_DIR = cfg.stateDir;
817 FORGEJO_CUSTOM = cfg.customDir;
818 };
819
820 serviceConfig = {
821 Type = "oneshot";
822 User = cfg.user;
823 ExecStart =
824 "${exe} dump --type ${cfg.dump.type}"
825 + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
826 WorkingDirectory = cfg.dump.backupDir;
827 };
828 };
829
830 systemd.timers.forgejo-dump = mkIf cfg.dump.enable {
831 description = "Forgejo dump timer";
832 partOf = [ "forgejo-dump.service" ];
833 wantedBy = [ "timers.target" ];
834 timerConfig.OnCalendar = cfg.dump.interval;
835 };
836 };
837
838 meta.doc = ./forgejo.md;
839 meta.maintainers = with lib.maintainers; [
840 bendlas
841 emilylange
842 pyrox0
843 ];
844}