1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7let
8 inherit (lib)
9 getExe
10 mkDefault
11 mkEnableOption
12 mkIf
13 mkOption
14 mkPackageOption
15 types
16 ;
17
18 cfg = config.services.actual;
19 configFile = formatType.generate "config.json" cfg.settings;
20 dataDir = "/var/lib/actual";
21
22 formatType = pkgs.formats.json { };
23in
24{
25 options.services.actual = {
26 enable = mkEnableOption "actual, a privacy focused app for managing your finances";
27 package = mkPackageOption pkgs "actual-server" { };
28
29 openFirewall = mkOption {
30 default = false;
31 type = types.bool;
32 description = "Whether to open the firewall for the specified port.";
33 };
34
35 settings = mkOption {
36 default = { };
37 description = "Server settings, refer to [the documentation](https://actualbudget.org/docs/config/) for available options.";
38 type = types.submodule {
39 freeformType = formatType.type;
40
41 options = {
42 hostname = mkOption {
43 type = types.str;
44 description = "The address to listen on";
45 default = "::";
46 };
47
48 port = mkOption {
49 type = types.port;
50 description = "The port to listen on";
51 default = 3000;
52 };
53 };
54
55 config = {
56 serverFiles = mkDefault "${dataDir}/server-files";
57 userFiles = mkDefault "${dataDir}/user-files";
58 dataDir = mkDefault dataDir;
59 };
60 };
61 };
62 };
63
64 config = mkIf cfg.enable {
65 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.port ];
66
67 systemd.services.actual = {
68 description = "Actual server, a local-first personal finance app";
69 after = [ "network.target" ];
70 wantedBy = [ "multi-user.target" ];
71 environment.ACTUAL_CONFIG_PATH = configFile;
72 serviceConfig = {
73 ExecStart = getExe cfg.package;
74 DynamicUser = true;
75 User = "actual";
76 Group = "actual";
77 StateDirectory = "actual";
78 WorkingDirectory = dataDir;
79 LimitNOFILE = "1048576";
80 PrivateTmp = true;
81 PrivateDevices = true;
82 StateDirectoryMode = "0700";
83 Restart = "always";
84
85 # Hardening
86 CapabilityBoundingSet = "";
87 LockPersonality = true;
88 #MemoryDenyWriteExecute = true; # Leads to coredump because V8 does JIT
89 PrivateUsers = true;
90 ProtectClock = true;
91 ProtectControlGroups = true;
92 ProtectHome = true;
93 ProtectHostname = true;
94 ProtectKernelLogs = true;
95 ProtectKernelModules = true;
96 ProtectKernelTunables = true;
97 ProtectProc = "invisible";
98 ProcSubset = "pid";
99 ProtectSystem = "strict";
100 RestrictAddressFamilies = [
101 "AF_INET"
102 "AF_INET6"
103 "AF_NETLINK"
104 ];
105 RestrictNamespaces = true;
106 RestrictRealtime = true;
107 SystemCallArchitectures = "native";
108 SystemCallFilter = [
109 "@system-service"
110 "@pkey"
111 ];
112 UMask = "0077";
113 };
114 };
115 };
116
117 meta.maintainers = [
118 lib.maintainers.oddlama
119 lib.maintainers.patrickdag
120 ];
121}