diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index e802470e7a04d..d2f3e506bd9bc 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -128,6 +128,8 @@ - [wg-access-server](https://github.com/freifunkMUC/wg-access-server/), an all-in-one WireGuard VPN solution with a web ui for connecting devices. Available at [services.wg-access-server](#opt-services.wg-access-server.enable). +- [Actual Budget](https://actualbudget.org/), a local-first personal finance app. Available as [services.actual](#opt-services.actual.enable). + - [Pingvin Share](https://github.com/stonith404/pingvin-share), a self-hosted file sharing platform and an alternative for WeTransfer. Available as [services.pingvin-share](#opt-services.pingvin-share.enable). - [Envision](https://gitlab.com/gabmus/envision), a UI for building, configuring and running Monado, the open source OpenXR runtime. Available as [programs.envision](#opt-programs.envision.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 6910458baf401..df47449951b5c 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1379,6 +1379,7 @@ ./services/video/v4l2-relayd.nix ./services/wayland/cage.nix ./services/wayland/hypridle.nix + ./services/web-apps/actual.nix ./services/web-apps/akkoma.nix ./services/web-apps/alps.nix ./services/web-apps/anuko-time-tracker.nix diff --git a/nixos/modules/services/web-apps/actual.nix b/nixos/modules/services/web-apps/actual.nix new file mode 100644 index 0000000000000..3af5d0c804ad5 --- /dev/null +++ b/nixos/modules/services/web-apps/actual.nix @@ -0,0 +1,121 @@ +{ + lib, + pkgs, + config, + ... +}: +let + inherit (lib) + getExe + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; + + cfg = config.services.actual; + configFile = formatType.generate "config.json" cfg.settings; + dataDir = "/var/lib/actual"; + + formatType = pkgs.formats.json { }; +in +{ + options.services.actual = { + enable = mkEnableOption "actual, a privacy focused app for managing your finances"; + package = mkPackageOption pkgs "actual-server" { }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = "Whether to open the firewall for the specified port."; + }; + + settings = mkOption { + default = { }; + description = "Server settings, refer to (the documentation)[https://actualbudget.org/docs/config/] for available options."; + type = types.submodule { + freeformType = formatType.type; + + options = { + hostname = mkOption { + type = types.str; + description = "The address to listen on"; + default = "::"; + }; + + port = mkOption { + type = types.port; + description = "The port to listen on"; + default = 3000; + }; + }; + + config = { + serverFiles = mkDefault "${dataDir}/server-files"; + userFiles = mkDefault "${dataDir}/user-files"; + dataDir = mkDefault dataDir; + }; + }; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.port ]; + + systemd.services.actual = { + description = "Actual server, a local-first personal finance app"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment.ACTUAL_CONFIG_PATH = configFile; + serviceConfig = { + ExecStart = getExe cfg.package; + DynamicUser = true; + User = "actual"; + Group = "actual"; + StateDirectory = "actual"; + WorkingDirectory = dataDir; + LimitNOFILE = "1048576"; + PrivateTmp = true; + PrivateDevices = true; + StateDirectoryMode = "0700"; + Restart = "always"; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + #MemoryDenyWriteExecute = true; # Leads to coredump because V8 does JIT + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@pkey" + ]; + UMask = "0077"; + }; + }; + }; + + meta.maintainers = [ + lib.maintainers.oddlama + lib.maintainers.patrickdag + ]; +} diff --git a/nixos/tests/actual.nix b/nixos/tests/actual.nix new file mode 100644 index 0000000000000..b8ee303f81272 --- /dev/null +++ b/nixos/tests/actual.nix @@ -0,0 +1,18 @@ +import ./make-test-python.nix ( + { lib, ... }: + { + name = "actual"; + meta.maintainers = [ lib.maintainers.oddlama ]; + + nodes.machine = + { ... }: + { + services.actual.enable = true; + }; + + testScript = '' + machine.wait_for_open_port(3000) + machine.succeed("curl -fvvv -Ls http://localhost:3000/ | grep 'Actual'") + ''; + } +) diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0e2a21803c2e3..cb65c6401db06 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -107,6 +107,7 @@ in { aaaaxy = runTest ./aaaaxy.nix; acme = runTest ./acme.nix; acme-dns = handleTest ./acme-dns.nix {}; + actual = handleTest ./actual.nix {}; adguardhome = runTest ./adguardhome.nix; aesmd = runTestOn ["x86_64-linux"] ./aesmd.nix; agate = runTest ./web-servers/agate.nix;