Paperless-ngx & Authelia
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