at 23.05-pre 11 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 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}