1{ config, pkgs, lib, ... }:
2
3with lib;
4let
5 cfg = config.services.paperless;
6 pkg = cfg.package;
7
8 defaultUser = "paperless";
9
10 # Don't start a redis instance if the user sets a custom redis connection
11 enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
12 redisServer = config.services.redis.servers.paperless;
13
14 env = {
15 PAPERLESS_DATA_DIR = cfg.dataDir;
16 PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
17 PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
18 GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
19 } // optionalAttrs (config.time.timeZone != null) {
20 PAPERLESS_TIME_ZONE = config.time.timeZone;
21 } // optionalAttrs enableRedis {
22 PAPERLESS_REDIS = "unix://${redisServer.unixSocket}";
23 } // (
24 lib.mapAttrs (_: toString) cfg.extraConfig
25 );
26
27 manage = let
28 setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
29 in pkgs.writeShellScript "manage" ''
30 ${setupEnv}
31 exec ${pkg}/bin/paperless-ngx "$@"
32 '';
33
34 # Secure the services
35 defaultServiceConfig = {
36 TemporaryFileSystem = "/:ro";
37 BindReadOnlyPaths = [
38 "/nix/store"
39 "-/etc/resolv.conf"
40 "-/etc/nsswitch.conf"
41 "-/etc/hosts"
42 "-/etc/localtime"
43 "-/run/postgresql"
44 ] ++ (optional enableRedis redisServer.unixSocket);
45 BindPaths = [
46 cfg.consumptionDir
47 cfg.dataDir
48 cfg.mediaDir
49 ];
50 CapabilityBoundingSet = "";
51 # ProtectClock adds DeviceAllow=char-rtc r
52 DeviceAllow = "";
53 LockPersonality = true;
54 MemoryDenyWriteExecute = true;
55 NoNewPrivileges = true;
56 PrivateDevices = true;
57 PrivateMounts = true;
58 PrivateNetwork = true;
59 PrivateTmp = true;
60 PrivateUsers = true;
61 ProtectClock = true;
62 # Breaks if the home dir of the user is in /home
63 # Also does not add much value in combination with the TemporaryFileSystem.
64 # ProtectHome = true;
65 ProtectHostname = true;
66 # Would re-mount paths ignored by temporary root
67 #ProtectSystem = "strict";
68 ProtectControlGroups = true;
69 ProtectKernelLogs = true;
70 ProtectKernelModules = true;
71 ProtectKernelTunables = true;
72 ProtectProc = "invisible";
73 # Don't restrict ProcSubset because django-q requires read access to /proc/stat
74 # to query CPU and memory information.
75 # Note that /proc only contains processes of user `paperless`, so this is safe.
76 # ProcSubset = "pid";
77 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
78 RestrictNamespaces = true;
79 RestrictRealtime = true;
80 RestrictSUIDSGID = true;
81 SupplementaryGroups = optional enableRedis redisServer.user;
82 SystemCallArchitectures = "native";
83 SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
84 # Does not work well with the temporary root
85 #UMask = "0066";
86 };
87in
88{
89 meta.maintainers = with maintainers; [ erikarvstedt Flakebi ];
90
91 imports = [
92 (mkRenamedOptionModule [ "services" "paperless-ng" ] [ "services" "paperless" ])
93 ];
94
95 options.services.paperless = {
96 enable = mkOption {
97 type = lib.types.bool;
98 default = false;
99 description = lib.mdDoc ''
100 Enable Paperless.
101
102 When started, the Paperless database is automatically created if it doesn't
103 exist and updated if the Paperless package has changed.
104 Both tasks are achieved by running a Django migration.
105
106 A script to manage the Paperless instance (by wrapping Django's manage.py) is linked to
107 `''${dataDir}/paperless-manage`.
108 '';
109 };
110
111 dataDir = mkOption {
112 type = types.str;
113 default = "/var/lib/paperless";
114 description = lib.mdDoc "Directory to store the Paperless data.";
115 };
116
117 mediaDir = mkOption {
118 type = types.str;
119 default = "${cfg.dataDir}/media";
120 defaultText = literalExpression ''"''${dataDir}/media"'';
121 description = lib.mdDoc "Directory to store the Paperless documents.";
122 };
123
124 consumptionDir = mkOption {
125 type = types.str;
126 default = "${cfg.dataDir}/consume";
127 defaultText = literalExpression ''"''${dataDir}/consume"'';
128 description = lib.mdDoc "Directory from which new documents are imported.";
129 };
130
131 consumptionDirIsPublic = mkOption {
132 type = types.bool;
133 default = false;
134 description = lib.mdDoc "Whether all users can write to the consumption dir.";
135 };
136
137 passwordFile = mkOption {
138 type = types.nullOr types.path;
139 default = null;
140 example = "/run/keys/paperless-password";
141 description = lib.mdDoc ''
142 A file containing the superuser password.
143
144 A superuser is required to access the web interface.
145 If unset, you can create a superuser manually by running
146 `''${dataDir}/paperless-manage createsuperuser`.
147
148 The default superuser name is `admin`. To change it, set
149 option {option}`extraConfig.PAPERLESS_ADMIN_USER`.
150 WARNING: When changing the superuser name after the initial setup, the old superuser
151 will continue to exist.
152
153 To disable login for the web interface, set the following:
154 `extraConfig.PAPERLESS_AUTO_LOGIN_USERNAME = "admin";`.
155 WARNING: Only use this on a trusted system without internet access to Paperless.
156 '';
157 };
158
159 address = mkOption {
160 type = types.str;
161 default = "localhost";
162 description = lib.mdDoc "Web interface address.";
163 };
164
165 port = mkOption {
166 type = types.port;
167 default = 28981;
168 description = lib.mdDoc "Web interface port.";
169 };
170
171 extraConfig = mkOption {
172 type = types.attrs;
173 default = {};
174 description = lib.mdDoc ''
175 Extra paperless config options.
176
177 See [the documentation](https://paperless-ngx.readthedocs.io/en/latest/configuration.html)
178 for available options.
179 '';
180 example = {
181 PAPERLESS_OCR_LANGUAGE = "deu+eng";
182 PAPERLESS_DBHOST = "/run/postgresql";
183 };
184 };
185
186 user = mkOption {
187 type = types.str;
188 default = defaultUser;
189 description = lib.mdDoc "User under which Paperless runs.";
190 };
191
192 package = mkOption {
193 type = types.package;
194 default = pkgs.paperless-ngx;
195 defaultText = literalExpression "pkgs.paperless-ngx";
196 description = lib.mdDoc "The Paperless package to use.";
197 };
198 };
199
200 config = mkIf cfg.enable {
201 services.redis.servers.paperless.enable = mkIf enableRedis true;
202
203 systemd.tmpfiles.rules = [
204 "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
205 "d '${cfg.mediaDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
206 (if cfg.consumptionDirIsPublic then
207 "d '${cfg.consumptionDir}' 777 - - - -"
208 else
209 "d '${cfg.consumptionDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
210 )
211 ];
212
213 systemd.services.paperless-scheduler = {
214 description = "Paperless scheduler";
215 serviceConfig = defaultServiceConfig // {
216 User = cfg.user;
217 ExecStart = "${pkg}/bin/paperless-ngx qcluster";
218 Restart = "on-failure";
219 # The `mbind` syscall is needed for running the classifier.
220 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "mbind" ];
221 # Needs to talk to mail server for automated import rules
222 PrivateNetwork = false;
223 };
224 environment = env;
225 wantedBy = [ "multi-user.target" ];
226 wants = [ "paperless-consumer.service" "paperless-web.service" ];
227
228 preStart = ''
229 ln -sf ${manage} ${cfg.dataDir}/paperless-manage
230
231 # Auto-migrate on first run or if the package has changed
232 versionFile="${cfg.dataDir}/src-version"
233 if [[ $(cat "$versionFile" 2>/dev/null) != ${pkg} ]]; then
234 ${pkg}/bin/paperless-ngx migrate
235 echo ${pkg} > "$versionFile"
236 fi
237 ''
238 + optionalString (cfg.passwordFile != null) ''
239 export PAPERLESS_ADMIN_USER="''${PAPERLESS_ADMIN_USER:-admin}"
240 export PAPERLESS_ADMIN_PASSWORD=$(cat "${cfg.dataDir}/superuser-password")
241 superuserState="$PAPERLESS_ADMIN_USER:$PAPERLESS_ADMIN_PASSWORD"
242 superuserStateFile="${cfg.dataDir}/superuser-state"
243
244 if [[ $(cat "$superuserStateFile" 2>/dev/null) != $superuserState ]]; then
245 ${pkg}/bin/paperless-ngx manage_superuser
246 echo "$superuserState" > "$superuserStateFile"
247 fi
248 '';
249 } // optionalAttrs enableRedis {
250 after = [ "redis-paperless.service" ];
251 };
252
253 # Reading the user-provided password file requires root access
254 systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
255 requiredBy = [ "paperless-scheduler.service" ];
256 before = [ "paperless-scheduler.service" ];
257 serviceConfig = {
258 ExecStart = ''
259 ${pkgs.coreutils}/bin/install --mode 600 --owner '${cfg.user}' --compare \
260 '${cfg.passwordFile}' '${cfg.dataDir}/superuser-password'
261 '';
262 Type = "oneshot";
263 };
264 };
265
266 systemd.services.paperless-consumer = {
267 description = "Paperless document consumer";
268 serviceConfig = defaultServiceConfig // {
269 User = cfg.user;
270 ExecStart = "${pkg}/bin/paperless-ngx document_consumer";
271 Restart = "on-failure";
272 };
273 environment = env;
274 # Bind to `paperless-scheduler` so that the consumer never runs
275 # during migrations
276 bindsTo = [ "paperless-scheduler.service" ];
277 after = [ "paperless-scheduler.service" ];
278 };
279
280 systemd.services.paperless-web = {
281 description = "Paperless web server";
282 serviceConfig = defaultServiceConfig // {
283 User = cfg.user;
284 ExecStart = ''
285 ${pkg.python.pkgs.gunicorn}/bin/gunicorn \
286 -c ${pkg}/lib/paperless-ngx/gunicorn.conf.py paperless.asgi:application
287 '';
288 Restart = "on-failure";
289
290 # gunicorn needs setuid, liblapack needs mbind
291 SystemCallFilter = defaultServiceConfig.SystemCallFilter ++ [ "@setuid mbind" ];
292 # Needs to serve web page
293 PrivateNetwork = false;
294 } // lib.optionalAttrs (cfg.port < 1024) {
295 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
296 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
297 };
298 environment = env // {
299 PATH = mkForce pkg.path;
300 PYTHONPATH = "${pkg.python.pkgs.makePythonPath pkg.propagatedBuildInputs}:${pkg}/lib/paperless-ngx/src";
301 };
302 # Allow the web interface to access the private /tmp directory of the server.
303 # This is required to support uploading files via the web interface.
304 unitConfig.JoinsNamespaceOf = "paperless-scheduler.service";
305 # Bind to `paperless-scheduler` so that the web server never runs
306 # during migrations
307 bindsTo = [ "paperless-scheduler.service" ];
308 after = [ "paperless-scheduler.service" ];
309 };
310
311 users = optionalAttrs (cfg.user == defaultUser) {
312 users.${defaultUser} = {
313 group = defaultUser;
314 uid = config.ids.uids.paperless;
315 home = cfg.dataDir;
316 };
317
318 groups.${defaultUser} = {
319 gid = config.ids.gids.paperless;
320 };
321 };
322 };
323}