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