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