Paperless-ngx & Authelia

Kurt Klinner

Ordner voll mit Dokumenten von Versicherungen, Behörden und Firmen wer kennt das nicht. Obwohl die Menge über die Jahre ein wenig zurück ging, hatte ich immer einige Mühen, alles vernünftig zu digitalisieren.

Bei meiner Suche nach aktuellen Lösungen bin ich über paperless-ngx gestolpert, einer Weiterentwicklung des nicht mehr aktivem paperless-ng Projektes.

Paperless-ngx ist eine Open Source Lösung zur Verwaltung von Dokumenten mit einem - meiner Meinung nach gelungenem - Frontend, OCR Support und Integrationen für mobile Anwendungen und E-Mail Konten.

In diesem Post gebe ich einen kurzen Überblick bzgl. der docker-basierten Installation von paperless-ngx und wie man mittels Authelia eine 2FA Lösung integriert.

paperless-ngx - Installation & Konfiguration

Für fast alle meiner Setups verwende ich einen docker-basierten Ansatz, so auch in diesem Fall.

Die Anweisungen beschreiben mehrere Optionen paperless-ngx via Docker zu betreiben, ich entschied mich dafür, die vorhandenen Images zu verwenden und mein docker-compose.yml selbst anzupassen.

Just ein Arbeitsverzeichnis namens paperless-ngx angelegt hatte, und die docker-compose.env und docker-compose.postgres-tika.yml Datei von https://github.com/paperless-ngx/paperless-ngx/tree/main/docker/compose geyogen, ging es gleich an das Anpassen der selbigen.

Unter obiger URL gibt es mehrere compose Files, je nachdem welcher Anwendungesfall abgedeckt werden soll. Da ich eine Lösung brauchte, die Support für MS Office Dokumente bietet, entschiede ich mich für die Option, die Apache Tika nutzt.

docker-compose.yml

Die paperless-ngx Dokumentation beschreibt alle vorhanden Konfigurationsmöglichkeiten.

Um zu starten reicht es aber aus, die Variablen für die Postgres Konfiguration (POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD) anzupassen.

version: "3.4"
services:
  broker:
    image: redis:6.0
    restart: unless-stopped
    volumes:
      - redisdata:/data

  db:
    image: postgres:13
    restart: unless-stopped
    volumes:
      - /home/docadmin/docker/paperless-ngx/database:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: <DB>
      POSTGRES_USER: <DBUSER>
      POSTGRES_PASSWORD: <DBPASSWORD>

  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
      - gotenberg
      - tika
    ports:
      - 127.0.0.1:8000:8000
    healthcheck:
      test: ["CMD", "curl", "-fs", "-S", "--max-time", "2", "http://localhost:8000"]
      interval: 30s
      timeout: 10s
      retries: 5
    volumes:
      - ./paperless-ngx/data:/usr/src/paperless/data
      - ./paperless-ngx/media:/usr/src/paperless/media
      - ./paperless-ngx/export:/usr/src/paperless/export
      - ./paperless-ngx/consume:/usr/src/paperless/consume
    env_file: docker-compose.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
      PAPERLESS_TIKA_ENABLED: 1
      PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
      PAPERLESS_TIKA_ENDPOINT: http://tika:9998

  gotenberg:
    image: gotenberg/gotenberg:7.4
    restart: unless-stopped
    command:
      - "gotenberg"
      - "--chromium-disable-routes=true"

  tika:
    image: ghcr.io/paperless-ngx/tika:latest
    restart: unless-stopped

volumes:
  redisdata:

docker-compose.env

Das Environment File enthält neben der URL unter der man paperless-ngx anspricht, mit dem PAPERLESS_SECRET_KEY nur einen weiteren variablen Wert. Der Key, welche zur Generierung von Session Tokens verwendet wird, sollte auf jeden Fall angepasst werden.

PAPERLESS_URL=<URL>
USERMAP_UID=1010
USERMAP_GID=100
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu
PAPERLESS_SECRET_KEY=<SECRETKEY>

Nach Abschluss der Anpassungen, starten wir den Container mittels

docker-compose up -d

und erzeugen als allererstes das Admin Userkonto

 docker-compose run --rm webserver createsuperuser 

Hat unsere Konfiguration soweit gepasst, so sollten wir unter der angegeben URL die Instanz erreichen

SSL Konfiguration

SSL verschlüsselte Kommunikation ist heute Standard und wird von paperless-ngx unterstützt.

Obwohl es möglich ist SSL direkt in der Anwendung zu terminieren, empfiehlt die offizielle Dokumentation, dies vorgelagert am Webserver/Reverse Proxy zu erledigen. In diesem Fall kommt - wie so oft - nginx und letsencrypt zur Rettung.

Wir generieren mittels certbot im ersten Schritt die Zertifikate für unser Domain

sudo certbot --nginx -d <YOURDOMAIN>
#oder alternativ
sudo certbot certonly -d <YOURDOMAIN>

Nun geht es an die Konfiguration des Servers bzw. der Location. Im Beispiel unten ist abgebildet wie man paperless-ngx via https://<YOURDOMAIN>/documents ansprechen kann

server {
  listen       *:443 ssl;
  server_name  <YOURDOMAIN>;
  
  ssl_certificate           /etc/letsencrypt/live/<YOURDOMAIN>/fullchain.pem;
  ssl_certificate_key       /etc/letsencrypt/live/<YOURDOMAIN>/privkey.pem;
  ssl_session_cache         shared:SSL:10m;
  ssl_session_timeout       5m;
  ssl_protocols             TLSv1.2;
  ssl_ciphers               CDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;
  ssl_prefer_server_ciphers on;
  ssl_stapling              on;
  ssl_stapling_verify       on;

  client_max_body_size 500M;
  index  index.html index.htm index.php;

  access_log            /var/log/nginx/<YOURDOMAIN>.access.log combined;
  error_log             /var/log/nginx/<YOURDOMAIN>.error.log;

  location /documents {
  proxy_pass http://localhost:8000/;

  # These configuration options are required for WebSockets to work.
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";

  proxy_redirect off;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Host $server_name;
  }
}

Hat alles gepasst, dann sollte man die Anwendung nach einem Reload der nginx Konfiguration unter der angegeben URL erreichen und sich als Admin User anmelden können.

In meinem Fall hatte soweit alles gepasst und meine Tests (Uploads, Tagging etc) verliefen alle positiv. Ein Punkt trieb mich aber immer noch um, welche Optionen habe ich den Zugriff auf meine Daten weiter abzusichern (Verschlüsselung der Dokumente, 2FA etc).

Beim Durcharbeiten der Dokumentation stellte sich heraus, dass eine Verschlüsselung der Dokumente via GNUPG nicht mehr unterstützt ist (siehe auch https://docs.paperless-ngx.com/administration/) von daher lag mein Hauptaugenmerk auf dem Thema 2FA.

Auch hier stellte sich die Dokumentation des Projektes als hilfreich heraus. Unterstützt die MFA Anwendung das Weiterleiten eines Remote-User Header nach erfolgreicher Authentifizierung so kann man diese mit paperless-ngx integrieren

Um dieses Feature zu aktivieren, muss man PAPERLESS_ENABLE_HTTP_REMOTE_USER und PAPERLESS_LOGOUT_REDIRECT_URL im Environment File mit aufnehmen.

PAPERLESS_URL=<URL>
USERMAP_UID=1010
USERMAP_GID=100
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu
PAPERLESS_SECRET_KEY=<SECRETKEY>
PAPERLESS_ENABLE_HTTP_REMOTE_USER=true
PAPERLESS_LOGOUT_REDIRECT_URL=<AUTHURL>

Authelia - Installation & Konfiguration

Nach dem Befragen der Suchmaschine des Vertrauens und dem Durchforsten etlicher Blogposts, kam ich auf Authelia, einem Open Source Autorisierungs- und Authentifizierungsserver.

In Verbindung mit einem NGINX Proxy, bietet Authelia SSO für alle angebunden Anwendungen (insofern dieses das unterstützen) ebenso wie 2FA via, Google Authenticator, Duo, and Yubikey.

Die Dokumentation von Authelia ist sehr ausführlich und es werden von Haus aus einige Deployment Beispiele für ein lokales, leichtgewichtiges oder HA Setup mitgeliefert. Für meine ersten Tests machte ich mir die Lite Option zu Nutze.

Authelia - Basiskonfiguration

Nachdem man sich das Repo via https://github.com/authelia/authelia.git gezogen hat, wirft man am Besten einen Blick in das docker-compose.yml File unter authelia/examples/compose/lite. Für einen ersten Test sind in der Regel keinerlei Anpassungen nötig.

version: '3.3'

networks:
  net:
    driver: bridge

services:
  authelia:
    image: authelia/authelia
    container_name: authelia
    volumes:
      - ./authelia:/config
    networks:
      - net
    ports:
      - 9091:9091
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin

  redis:
    image: redis:alpine
    container_name: redis_authelia
    volumes:
      - ./redis:/data
    networks:
      - net
    expose:
      - 6379
    restart: unless-stopped
    environment:
      - TZ=Europe/Berlin
      - PUID=1000
      - PGID=1000

Authelia - Applikations Konfiguration

Der weitaus spannendere Teil ist der Teil der Konfiguration, in dem man definiert welche URL mittels welchem 2FA Verfahren geschützt werden soll.

Im authelia Verzeichnis findet sich die Datei namens configuration.yml, die man entsprechend Anpassen muss.

server:
  host: 0.0.0.0
  port: 9091
log:
  level: info
jwt_secret: <SECRETKEY>
default_redirection_url: <DEFAULTURL>

authentication_backend:
  file:
    path: /config/users_database.yml

access_control:
  default_policy: deny
  rules:
    - domain:
        - "auth.<YOURDOMAIN>"
        - "<BYPASSDOMAIN>"
      policy: bypass
    - domain:
        - "<PAPERLESSDOMAIN>"
      policy: two_factor

webauthn:
  disable: false
  display_name: Authelia
  attestation_conveyance_preference: indirect
  user_verification: preferred
  timeout: 60s

session:
  name: authelia_session
  secret: <SESSIONKEY>
  expiration: 12h           # 12 hours
  inactivity: 45m           # 45 minutes
  remember_me_duration: 1M  # 1 month
  domain: mobux.de

  redis:
    host: redis_authelia
    port: 6379

regulation:
  max_retries: 3
  find_time: 5m
  ban_time: 15m

storage:
  encryption_key: <STORAGEKEY>
  local:
    path: /config/db.sqlite3

notifier:
  #filesystem:
    #filename: /config/notification.txt
  smtp:
    username: <EMAILUSER>
    password: <EMAILPASSWORD>
    host: <MAILHOST>
    port: <MAILPORT>
    sender: <MAILSENDER>

Als erstes passt man die variablen Teile ala URL für standarmässige Redirects und die Secret Keys an (Details zu diesen und deren Verwendung finden sich in der Authelia Dokumentation), am wichtigsten ist aber die Konfiguration der Access Controls, sprich welche URL wie geschützt werden soll.

Im Beispiel ist ersichtlich, dass auth.<YOURDOMAIN> sowie <BYPASSDOMAIN> keine weitere Authenifizierung benötigen, wohin gegen <PAPERLESSDOMAIN> ein 2FA erfordert.

access_control:
  default_policy: deny
  rules:
    - domain:
        - "auth.<YOURDOMAIN>"
        - "<BYPASSDOMAIN>"
      policy: bypass
    - domain:
        - "<PAPERLESSDOMAIN>"
      policy: two_factor

Um diese angesprochenen Keysequenzen zu erzeugen, kann man übrigens folgendes Kommando nutzen.

head /dev/urandom | tr -dc A-Za-z0-9 | head -c64

Sobald wir diese Schritte abgeschlossen habe, geht es als letztes daran die Userdatabase zu generieren.

Im config Verzeichnis in dem auch die configuration.yml Datei liegt, legen wir ein neues File namens user_database.yml und fügen unsere Benutzer nach folgdendem Schema ein.

users:
  <USERNAME>:
    displayname: "<FULLNAME>"
    # Generate with docker run authelia/authelia:latest authelia hash-password <your-password>
    password: "<USERPASSWORD>"
    email: <USEREMAIL>
    groups:
      - admins

<USERNAME> , <FULLNAME> , <USEREMAIL> and <USERPASSWORD> sind mit den jeweiligen Informationen des Accounts zu ersetzen. Ggf. sollte man auch nicht jedem User die Admins Gruppe geben :-)

Während die ersten 3 genannten Attribute, im Klartext eingetragen werden, muss man das Benutzerpasswort erzeugen.

Hierzu nutzt man die im Image vorhandene hash-password Methode.

docker run authelia/authelia:latest authelia hash-password <your-password>

Nach Abschluss der Arbeiten starten wir Authelia mittels

docker-compose up -d

und verifzieren ob wir die Anwendung unter Port 9091 erreichen. Ist alles in Ordnung sollte man Authelia's login prompt zu sehen bekommen.

Authelia - NGINX

Im nächsten Schritt verknüpfen wir Authelia und Nginx.

Nachdem wir ein SSL Zertificate für auth.<YOURDOMAIN> benötigen, kommt erneut letsencrypt/certbot zum Einsatz

sudo certbot --nginx -d auth.<YOURDOMAIN> 

Im Anschluss erzeigen wir die Webserver Konfiguration für auth.<YOURDOMAIN>

server {
    server_name auth.<YOURDOMAIN>;
    listen 80;
    return 301 https://$server_name$request_uri;
}

server {
    server_name auth.<YOURDOMAIN>;
    listen 443 ssl http2;
    ssl_certificate           /etc/letsencrypt/live/auth.<YOURDOMAIN>/fullchain.pem;
    ssl_certificate_key       /etc/letsencrypt/live/auth.<YOURDOMAIN>/privkey.pem;

    location / {
        set $upstream_authelia http://127.0.0.1:9091;
        proxy_pass $upstream_authelia;

        client_body_buffer_size 128k;

        #Timeout if the real server is dead
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

        # Advanced Proxy Config
        send_timeout 5m;
        proxy_read_timeout 360;
        proxy_send_timeout 360;
        proxy_connect_timeout 360;

        # Basic Proxy Config
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-Uri $request_uri;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_redirect  http://  $scheme://;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_cache_bypass $cookie_session;
        proxy_no_cache $cookie_session;
        proxy_buffers 64 256k;

        # If behind reverse proxy, forwards the correct IP
        set_real_ip_from 10.0.0.0/8;
        set_real_ip_from 172.0.0.0/8;
        set_real_ip_from 192.168.0.0/16;
        set_real_ip_from fc00::/7;
        real_ip_header X-Forwarded-For;
        real_ip_recursive on;
    }
}

Natürlich müssen alle Vorkommen von <YOURDOMAIN> mit der tatsächlichen URL angepasst werden.

Schnell die Seite aktivieren

sudo ln -s /etc/nginx/sites-available/auth.<YOURDOMAIN> /etc/nginx/sites-enabled/auth.<YOURDOMAIN>
sudo systemctl reload nginx

und den Zugriff auf auth.<YOURDOMAIN> testen. Wenn alles korrekt angepasst wurde, dann sollte man den Login prompt sehen

Um die Konfiguration modular und wiederverwendbar zu halten, habe ich ein paar Snippets angelegt.

authelia.conf

# Virtual endpoint created by nginx to forward auth requests.
location /authelia {
    internal;
    set $upstream_authelia http://127.0.0.1:9091/api/verify;
    proxy_pass_request_body off;
    proxy_pass $upstream_authelia;    
    proxy_set_header Content-Length "";

    # Timeout if the real server is dead
    proxy_next_upstream error timeout invalid_header http_500 http_502 http_503;

    # [REQUIRED] Needed by Authelia to check authorizations of the resource.
    # Provide either X-Original-URL and X-Forwarded-Proto or
    # X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both.
    # Those headers will be used by Authelia to deduce the target url of the     user.
    # Basic Proxy Config
    client_body_buffer_size 128k;
    proxy_set_header Host $host;
    proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr; 
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Uri $request_uri;
    proxy_set_header X-Forwarded-Ssl on;
    proxy_redirect  http://  $scheme://;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_cache_bypass $cookie_session;
    proxy_no_cache $cookie_session;
    proxy_buffers 4 32k;

    # Advanced Proxy Config
    send_timeout 5m;
    proxy_read_timeout 240;
    proxy_send_timeout 240;
    proxy_connect_timeout 240;
}

authentication.conf

# Basic Authelia Config
# Send a subsequent request to Authelia to verify if the user is authenticated
# and has the right permissions to access the resource.
auth_request /authelia;
# Set the `target_url` variable based on the request. It will be used to build the portal
# URL with the correct redirection parameter.
auth_request_set $target_url $scheme://$http_host$request_uri;
# Set the X-Forwarded-User and X-Forwarded-Groups with the headers
# returned by Authelia for the backends which can consume them.
# This is not safe, as the backend must make sure that they come from the
# proxy. In the future, it's gonna be safe to just use OAuth.
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Name $name;
proxy_set_header Remote-Email $email;
# If Authelia returns 401, then nginx redirects the user to the login portal.
# If it returns 200, then the request pass through to the backend.
# For other type of errors, nginx will handle them as usual.
error_page 401 =302 https://auth.<YOURDOMAIN>/?rd=$target_url;

Die snippets verwendend, passen wir unsere Konfiguration wie folgt an

  • authelia.conf wird in den server Block mit aufgenommen
  • authentication.conf wird an jede Domain / URL angefügt, die wir schützen wollen

Beispiel

server {
  listen *:80;
  server_name           <YOURDOMAIN>;
  client_max_body_size 500M;
  return 301 https://$host$request_uri;
  access_log            /var/log/nginx/<YOURDOMAIN>.access.log combined;
  error_log             /var/log/nginx/<YOURDOMAIN>.error.log;
}
 
server {
  listen       *:443 ssl;
  server_name  <YOURDOMAIN>;

  ssl_certificate           /etc/letsencrypt/live/<YOURDOMAIN>/fullchain.pem;
  ssl_certificate_key       /etc/letsencrypt/live/<YOURDOMAIN>/privkey.pem;
  ssl_session_cache         shared:SSL:10m;
  ssl_session_timeout       5m;
  ssl_protocols             TLSv1.2;
  ssl_ciphers               CDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256;
  ssl_prefer_server_ciphers on;
  ssl_stapling              on;
  ssl_stapling_verify       on;

  client_max_body_size 500M;
  index  index.html index.htm index.php;

  access_log            /var/log/nginx/ssl-<YOURDOMAIN>.access.log combined;
  error_log             /var/log/nginx/ssl-<YOURDOMAIN>.error.log;

  include snippets/authelia.conf;

  location /documents {
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header Host $host;
   proxy_pass            http://127.0.0.1:8000;
   proxy_buffering off;
   proxy_http_version 1.1;
   proxy_set_header Upgrade $http_upgrade;
   proxy_set_header Connection "upgrade";
   proxy_set_header X-Real-IP $remote_addr;

	 include snippets/authentication.conf; # Protect this endpoint
  }
}

Nach Abschluss aller Arbeiten, laden wir die Konfiguration erneut.

sudo nginx -t
sudo systemctl reload nginx

Paperless & Nginx

Nach Abschluss aller arbeiten sprechen wir nun unsere paperless URL an.

Nach erfolgter Anmeldung mit den Credentials, die wir bei der Konfiguration von authelia hinterlegt haben, werden wir aufgefordert die 2. Authentifizierungsmethode zu verwenden.

Falls alles geklappt hat, werden wir ohne weitere Anmeldung nach paperless-ngx geleitet.


Nützliche Resourcen