1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkEnableOption
11 mkIf
12 mkOption
13 optionalAttrs
14 optional
15 mkPackageOption
16 ;
17 inherit (lib.types)
18 bool
19 path
20 str
21 submodule
22 ;
23
24 cfg = config.services.pocket-id;
25
26 format = pkgs.formats.keyValue { };
27 settingsFile = format.generate "pocket-id-env-vars" cfg.settings;
28in
29{
30 meta.maintainers = with lib.maintainers; [
31 gepbird
32 ymstnt
33 ];
34
35 options.services.pocket-id = {
36 enable = mkEnableOption "Pocket ID server";
37
38 package = mkPackageOption pkgs "pocket-id" { };
39
40 environmentFile = mkOption {
41 type = path;
42 description = ''
43 Path to an environment file loaded for the Pocket ID service.
44
45 This can be used to securely store tokens and secrets outside of the world-readable Nix store.
46
47 Example contents of the file:
48 MAXMIND_LICENSE_KEY=your-license-key
49 '';
50 default = "/dev/null";
51 example = "/var/lib/secrets/pocket-id";
52 };
53
54 settings = mkOption {
55 type = submodule {
56 freeformType = format.type;
57
58 options = {
59 PUBLIC_APP_URL = mkOption {
60 type = str;
61 description = ''
62 The URL where you will access the app.
63 '';
64 default = "http://localhost";
65 };
66
67 TRUST_PROXY = mkOption {
68 type = bool;
69 description = ''
70 Whether the app is behind a reverse proxy.
71 '';
72 default = false;
73 };
74 };
75 };
76
77 default = { };
78
79 description = ''
80 Environment variables that will be passed to Pocket ID, see
81 [configuration options](https://pocket-id.org/docs/configuration/environment-variables)
82 for supported values.
83 '';
84 };
85
86 dataDir = mkOption {
87 type = path;
88 default = "/var/lib/pocket-id";
89 description = ''
90 The directory where Pocket ID will store its data, such as the database.
91 '';
92 };
93
94 user = mkOption {
95 type = str;
96 default = "pocket-id";
97 description = "User account under which Pocket ID runs.";
98 };
99
100 group = mkOption {
101 type = str;
102 default = "pocket-id";
103 description = "Group account under which Pocket ID runs.";
104 };
105 };
106
107 config = mkIf cfg.enable {
108 warnings = (
109 optional (cfg.settings ? MAXMIND_LICENSE_KEY)
110 "config.services.pocket-id.settings.MAXMIND_LICENSE_KEY will be stored as plaintext in the Nix store. Use config.services.pocket-id.environmentFile instead."
111 );
112
113 systemd.tmpfiles.rules = [
114 "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group}"
115 ];
116
117 systemd.services = {
118 pocket-id-backend = {
119 description = "Pocket ID backend";
120 after = [ "network.target" ];
121 wantedBy = [ "multi-user.target" ];
122 restartTriggers = [
123 cfg.package
124 cfg.environmentFile
125 settingsFile
126 ];
127
128 serviceConfig = {
129 Type = "simple";
130 User = cfg.user;
131 Group = cfg.group;
132 WorkingDirectory = cfg.dataDir;
133 ExecStart = "${cfg.package}/bin/pocket-id-backend";
134 Restart = "always";
135 EnvironmentFile = [
136 cfg.environmentFile
137 settingsFile
138 ];
139
140 # Hardening
141 AmbientCapabilities = "";
142 CapabilityBoundingSet = "";
143 DeviceAllow = "";
144 DevicePolicy = "closed";
145 #IPAddressDeny = "any"; # communicates with the frontend
146 LockPersonality = true;
147 MemoryDenyWriteExecute = true;
148 NoNewPrivileges = true;
149 PrivateDevices = true;
150 PrivateNetwork = false; # communicates with the frontend
151 PrivateTmp = true;
152 PrivateUsers = true;
153 ProcSubset = "pid";
154 ProtectClock = true;
155 ProtectControlGroups = true;
156 ProtectHome = true;
157 ProtectHostname = true;
158 ProtectKernelLogs = true;
159 ProtectKernelModules = true;
160 ProtectKernelTunables = true;
161 ProtectProc = "invisible";
162 ProtectSystem = "full"; # needs to write in cfg.dataDir
163 RemoveIPC = true;
164 RestrictAddressFamilies = [
165 "AF_INET"
166 "AF_INET6"
167 ];
168 RestrictNamespaces = true;
169 RestrictRealtime = true;
170 RestrictSUIDSGID = true;
171 SystemCallArchitectures = "native";
172 SystemCallFilter = lib.concatStringsSep " " [
173 "~"
174 "@clock"
175 "@cpu-emulation"
176 "@debug"
177 "@module"
178 "@mount"
179 "@obsolete"
180 "@privileged"
181 "@raw-io"
182 "@reboot"
183 #"@resources" # vm test segfaults
184 "@swap"
185 ];
186 UMask = "0077";
187 };
188 };
189
190 pocket-id-frontend = {
191 description = "Pocket ID frontend";
192 after = [
193 "network.target"
194 "pocket-id-backend.service"
195 ];
196 wantedBy = [ "multi-user.target" ];
197 restartTriggers = [
198 cfg.package
199 cfg.environmentFile
200 settingsFile
201 ];
202
203 serviceConfig = {
204 Type = "simple";
205 User = cfg.user;
206 Group = cfg.group;
207 ExecStart = "${cfg.package}/bin/pocket-id-frontend";
208 Restart = "always";
209 EnvironmentFile = [
210 cfg.environmentFile
211 settingsFile
212 ];
213
214 # Hardening
215 AmbientCapabilities = "";
216 CapabilityBoundingSet = "";
217 DeviceAllow = "";
218 DevicePolicy = "closed";
219 #IPAddressDeny = "any"; # communicates with the backend and client
220 LockPersonality = true;
221 MemoryDenyWriteExecute = false; # V8_Fatal segfault
222 NoNewPrivileges = true;
223 PrivateDevices = true;
224 PrivateNetwork = false; # communicates with the backend and client
225 PrivateTmp = true;
226 PrivateUsers = true;
227 ProcSubset = "pid";
228 ProtectClock = true;
229 ProtectControlGroups = true;
230 ProtectHome = true;
231 ProtectHostname = true;
232 ProtectKernelLogs = true;
233 ProtectKernelModules = true;
234 ProtectKernelTunables = true;
235 ProtectProc = "invisible";
236 ProtectSystem = "strict";
237 RemoveIPC = true;
238 RestrictAddressFamilies = [
239 "AF_INET"
240 "AF_INET6"
241 ];
242 RestrictNamespaces = true;
243 RestrictRealtime = true;
244 RestrictSUIDSGID = true;
245 SystemCallArchitectures = "native";
246 SystemCallFilter = lib.concatStringsSep " " [
247 "~"
248 "@clock"
249 "@cpu-emulation"
250 "@debug"
251 "@module"
252 "@mount"
253 "@obsolete"
254 "@privileged"
255 "@raw-io"
256 "@reboot"
257 "@resources"
258 "@swap"
259 ];
260 UMask = "0077";
261 };
262 };
263 };
264
265 users.users = optionalAttrs (cfg.user == "pocket-id") {
266 pocket-id = {
267 isSystemUser = true;
268 group = cfg.group;
269 description = "Pocket ID backend user";
270 home = cfg.dataDir;
271 };
272 };
273
274 users.groups = optionalAttrs (cfg.group == "pocket-id") {
275 pocket-id = { };
276 };
277 };
278}