{
pkgs,
config,
lib,
utils,
...
}:
let
cfg = config.mjm.paperless;
in
{
options.mjm.paperless = {
enable = lib.mkEnableOption "paperless";
};
config = lib.mkIf cfg.enable {
<<config>>
};
_class = "nixos";
}I've been running Paperless in some form for years now to avoid needing to keep huge files of paper documents.
mjm.services.paperless = {
<<service-settings>>
};postgresql.enable = true;
Paperless uses PostgreSQL as its database.
secrets.enable = true;
Paperless uses Vault to store its OIDC client secret and its password for backing up documents.
s3.enable = true; s3.buckets = [ "paperless-docs" "restic-backups" ];
I'm using rclone to allow Paperless to store its documents in S3-compatible storage. Since it also backups those documents, its identity needs access to the backups bucket as well.
http = {
port = config.services.paperless.port;
health.path = "/";
ingress = {
subdomain = "paper";
authMode = "oidc";
oidc = {
name = "Paperless";
clientId = "LijoxTI8E2n5IeFZcFxrQcC94ENIqkzXzhh93LpKGMBCQzMDnmsQ3c6zWXjUXJe3";
clientSecret = "$argon2id$v=19$m=65536,t=3,p=4$0Rg1T9HOd4EWIf6LrHDCDQ$95ihO+jnuTMLsSJrstHTJx12SUpZmExHUYpAeqtdheg";
redirectUris = [ "https://paper.midna.dev/accounts/oidc/authelia/login/callback/" ];
};
};
};Paperless is exposed externally via the ingress at https://paper.midna.dev. It doesn't have a dedicated health check endpoint, so the home page is used. Paperless supports OpenID Connect for authentication, so I use that.
services.paperless.enable = true; services.paperless.domain = "paper.midna.dev"; services.paperless.address = "::1";
The Paperless service must be enabled and configured with the domain where it will be accessed. NixOS defaults to only listening locally on IPv4, but I prefer to use IPv6 where possible.
services.paperless.settings = {
PAPERLESS_DBENGINE = "postgresql";
PAPERLESS_DBHOST = "/run/postgresql";
PAPERLESS_DBNAME = "paperless";
PAPERLESS_DBUSER = "paperless";
};These database settings would be set automatically if I was running PostgreSQL on the same machine as Paperless, but I'm not. Instead, PostgreSQL runs in a separate microVM on the same host. The connection is still made through a local Unix socket.
services.paperless.settings = {
PAPERLESS_TASK_WORKERS = 1;
PAPERLESS_THREADS_PER_WORKER = 1;
};Pretty sure I set these to calm down the resource usage of Paperless so it wouldn't spike as high in memory usage. I don't need Paperless to work that fast in terms of consuming documents.
services.paperless.settings.PAPERLESS_OCR_USER_ARGS = {
invalidate_digital_signatures = true;
};If I remember right, this setting is important because I configure Paperless to import PDFs from attachments in my inbox. The digital signatures on some of those files would prevent them from importing without this.
services.paperless.settings = {
PAPERLESS_APP_TITLE = "Papers Please!";
PAPERLESS_ALLOWED_HOSTS = "localhost";
};I override the app title just for fun.
The allowed hosts is in addition to the configured domain. localhost is allowed because some other services use the Paperless API via a client tunnel, which is accessed as localhost.
services.paperless.settings = {
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_SOCIAL_AUTO_SIGNUP = true;
PAPERLESS_DISABLE_REGULAR_LOGIN = true;
PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
};
services.paperless.environmentFile = "/run/paperless-env/env";These settings configure Paperless to use OpenID Connect for login. In fact, it's configured to be the only way to login, to automatically create accounts if they don't already exist, and to redirect to Authelia immediately from the login page.
The environment file is used to pass the actual OpenID Connect configuration. Since that includes the client secret, it can't be specified directly in Nix: the environment file needs to be generated at runtime.
systemd.services.paperless-env =
let
units = [
"paperless-consumer.service"
"paperless-scheduler.service"
"paperless-secret-key.service"
"paperless-task-queue.service"
"paperless-web.service"
];
in
{
description = "Generate Paperless Environment File";
wantedBy = units;
before = units;
path = [
pkgs.systemd
pkgs.jq
];
startLimitIntervalSec = 0;
script =
let
<<socialConfig>>
in
''
${utils.genJqSecretsReplacementSnippet socialConfig "/tmp/social-config.json"}
social_providers=$(jq -c </tmp/social-config.json)
echo "PAPERLESS_SOCIALACCOUNT_PROVIDERS=$social_providers" > /run/paperless-env/env
'';
credentials.paperless."managed/oidc_client_secret" = { };
serviceConfig = {
Type = "oneshot";
Restart = "on-failure";
RestartSec = 5;
RemainAfterExit = true;
DynamicUser = true;
PrivateNetwork = true;
PrivateTmp = true;
RuntimeDirectory = "paperless-env";
RuntimeDirectoryMode = "0700";
};
};This systemd service runs before any Paperless service and generates the environment file that those services will need to be able to start. The file defines a single variable that configures OpenID Connect.
secrets = config.systemd.services.paperless-env.credentials.paperless;
socialConfig = {
openid_connect = {
SCOPE = [
"openid"
"profile"
"email"
];
OAUTH_PKCE_ENABLED = true;
APPS = [
{
provider_id = "authelia";
name = "Authelia";
client_id = config.mjm.services.paperless.http.ingress.oidc.clientId;
secret._secret = secrets."managed/oidc_client_secret".path;
settings = {
server_url = "https://auth.midna.dev";
token_auth_method = "client_secret_basic";
};
}
];
};
};There aren't any real surprises in the actual OpenID Connect configuration; it includes all the details you would expect. The client secret will be replaced when the script is run with the contents of the credential read from Vault.
mjm.backups.paperless.paths = [ "/var/lib/paperless/media/documents" ];
Backups for Paperless are simple: the directory containing the documents is captured by the backup.
mjm.deploy.tests = {
inherit (pkgs.nixosTests) paperless;
};My CI will run the NixOS test for Paperless from Nixpkgs to ensure there aren't significant regressions.
I use a Brother scanner that can connect to my WiFi network and SFTP documents directly to the VM running Paperless, which can then automatically discover and consume them. This requires some special setup, especially because the SSH client on the scanner is quite old.
users.users.paperless.openssh.authorizedKeys.keys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1NXtzg50EbpzudswkjUkxllahH+F54h6MnDoXarftqlHc26M46M5IPQeRpn5F4BLGWs94UNFyod4d7KNhRYXxh2G+gsJcDTREdUR7eKu5CfaFnB2sge8VJM8KwxbURXHlxNF2xha0lIg8HdfSIznogAGqcUYahTJAUdKB1A4UJ9DzHp1Mrlrk3o04TvokRmS18kPM39nstneqHRVC1TPf83QV3tAYBz2iayifH714KTcItflUe5IqDUhBfNURhOnhG0szfK2qtykdg+7/wu0Ah3HOlbfLybx2eAA048kyBiFpllFIGqoO0hN8w7wmMuQ6okxs3tssz7W+dGi5HDob root@BR5CF370C29B2A" ];
The scanner logs in as the paperless user, who is configured to accept the scanner's SSH key.
services.openssh.hostKeys = [
# add a persistent rsa key, because the scanner needs to keep its public key, and i have
# absolutely no expectation that it would support a cert authority
{
bits = 4096;
path = "${config.services.paperless.dataDir}/ssh_host_rsa_key";
type = "rsa";
}
];On the other side, the VM needs a host key that the scanner can trust. Normally, my microVMs only have a temporary ed25519 key that is generated at boot. They don't usually need a persistent key because they get an SSH certificate from Vault instead. But the scanner supports neither ed25519 keys nor certificates, so a persistent RSA key is needed.
services.openssh.settings.KexAlgorithms = lib.mkOptionDefault [ "diffie-hellman-group14-sha1" ]; services.openssh.settings.Macs = lib.mkOptionDefault [ "hmac-sha1-96" ]; services.openssh.settings.HostKeyAlgorithms = "+ssh-rsa"; services.openssh.settings.PubkeyAcceptedAlgorithms = "+ssh-rsa";
These algorithms are old and not included in default SSH server configs anymore. Unfortunately, they are necessary for the scanner's old SSH version to work, so they get added to the relevant settings.
services.openssh.extraConfig = lib.mkAfter ''
Match User paperless
X11Forwarding no
AllowTcpForwarding no
ForceCommand internal-sftp -u 0077 -d /var/lib/paperless/consume
'';Finally, the paperless user is configured to force SFTP: normal shell access is not allowed. SFTP will start in the Paperless consume directory, so documents automatically end up where they need to for them to get imported.
systemd.services.paperless-data =
let
units = [
"paperless-scheduler.service"
"paperless-web.service"
"paperless-consumer.service"
"paperless-task-queue.service"
];
in
{
description = "Mount Paperless Docs from Garage";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"network-online.target"
];
requiredBy = units;
before = units;
path = [ pkgs.glibc.getent ];
environment = {
RCLONE_VERBOSE = "2";
RCLONE_S3_PROVIDER = "Other";
RCLONE_S3_ENV_AUTH = "true";
RCLONE_S3_ENDPOINT = "http://localhost:3902";
RCLONE_S3_REGION = "home";
RCLONE_S3_USE_UNSIGNED_PAYLOAD = "true";
RCLONE_S3_USE_MULTIPART_UPLOADS = "false";
RCLONE_STREAMING_UPLOAD_CUTOFF = "0";
};
serviceConfig = {
Type = "notify";
ExecStart = "${pkgs.rclone}/bin/rclone mount :s3:paperless-docs ${config.services.paperless.mediaDir}/documents --allow-other --dir-perms 770 --file-perms 660 --uid ${toString config.users.users.paperless.uid} --gid ${toString config.users.groups.paperless.gid}";
Restart = "on-failure";
RestartSec = "5s";
};
};This systemd service uses rclone to mount the "paperless-docs" bucket from Garage to Paperless's documents directory. This allows storing documents in Garage without Paperless itself needing support for S3 storage.
Environment variables are used to configure rclone. Some are simply letting it know how to reach Garage. The last few I remember being necessary for things to work well with Garage, but I don't really remember the details.
text/gemini;lang=en-USThis content has been proxied by September (UNKNO).