Paperless NGX fremgehostet

Einleitung

Was tun, wenn ich keine Server zuhause betreiben aber trotzdem Paperless-NGX nutzen möchte? Nun, es gibt Anbieter, die Paperless NGX hosten und das als Dienstleistung anbieten. Ich möchte aber eine Paperless-NGX-Installation bei einem Hoster aufsetzen und sie ansonsten selbst betreiben. Dabei sind folgende Merkmale gewünscht:


Folgende Funktionalitäten sind gewünscht:



Lösungsdesign

Die Lösung, die ich auch in einem Youtube-Video beschrieben habe, besteht aus einem Server, der bei einem Hoster angemietet wird. Auf diesem läuft zunächst Wireguard, um eine VPN-Verbindung zu Clients im lokalen Netzwerk aufzubauen. Das wird ausschließlich für die Administration des Systems benötigt. Normale Userzugriffe auf Paperless-NGX werden später über die öffentlich zugängliche Domain stattfinden. Dann läuft auf dem Server unter Docker der Reverse Proxy traefik, Paperless-NGX und die Datensicherungslösung Duplicati. Zum Schutz vor unbefugten Zugriffen und Manipulationen kommen restriktive Firewalleinstellungen zum Einsatz und mit fail2ban wird ein Schutz vor Brute Force Attacken realisiert. Die Datensicherung wird später auf einen Cloud-Speicher durchgeführt; natürlich verschlüsselt.

Abbildung: Lösungsdesign
Abb. 1: Lösungsdesign



Zutaten

Man nehme:



Hosting

Ich werde hier keine Empfehlungen für oder gegen das eine oder andere Hosting-Angebot aussprechen. Die Angebote der verschiedenen Hoster unterscheiden sich hinsichtlich der Speichermengen, der Systemressourcen, der Bandbreite, mit der die Server ans Internet angebunden sind und auch hinsichtlich der jeweils angebotenen Services (mit oder ohne Firewall vor dem Server, mit oder ohne Snapshots, Nutzung eigener Bootmedien oder ausschließlich vom Anbieter bereit gestellter Medien...). Hier muss man sich überlegen, was man braucht und möchte. Meine Paperless-NGX-Installationen kommen in der Regel mit 2 Prozessorkernen und 4 GB RAM aus. Beim Massenspeicher muss man überlegen, wie viel man vielleicht braucht. Wenn man davon ausgeht, am Anfang nicht sehr viel zu brauchen, dann mögen vielleicht 32 GB Massenspeicher ausreichen. Wenn der aber irgendwann knapp wird, dann ist ein Umzug auf einen besser ausgestatteten Server zumindest nicht unaufwändig. Für den Anfang sollte aber eine Kapazität von etwa 80 GB reichen.

Für diese recht geringen Anforderungen werden von den meisten Hostern (z.B. Hetzner, Ionos, Netcup oder Strato) Produkte in der Preisklasse um die 4 - 5 € pro Monat angeboten. Für die Datensicherung empfehle ich einen Cloudspeicher bei einem anderen Anbieter, als dem bei dem der Server gemietet wird. Da die Sicherungen verschlüsselt übertragen und gespeichert werden, ist das aus Sicherheits- und Datenschutzgesichtspunkten her unbedenklich. Solche Speicherlösungen werden für 3 - 5 € im Monat angeboten (z.B. die Hetzner Storage-Box für 3,20 € für 1 TB oder das Strato HiDrive Start für 3,5 € für 500 GB). Somit summieren sich die monatlichen laufenden Kosten für die vorgesehene Lösung auf 7 - 10 € im Monat.



Absicherung des Servers

Die Basisinstallation des Servers beschreibe ich hier nicht, weil sich das zum Einen von Hoster zu Hoster geringfügig unterscheidet und zum anderen recht selbst erklärend ist. Die folgenden Schritte basieren auf einem Ubuntu Linux Server oder einem Debian, jeweils in aktueller Long Term Service Version. Ich beschreibe hier die Absicherung des Servers. Zur Basisabsicherung des Servers s. hier: https://tutorials.kernke.koeln/sicherheit/server-absichern.html. Wer seinem Provider seine Daten nicht anvertrauen möchte, kann zusätzlich eine verschlüsselte Partition einrichten. Darauf verzichte ich hier, weil ich Angebote von zertifizierten Providern in Deutschland nutze.



Unprivilegierten User anlegen

Wenn man Bootmedien bzw. entsprechende Templates von Providern für das Betriebssystem des Server nutzt, dann wird bei der Basisinstallation oftmals nur ein Root-User angelegt und kein normaler User ohne Administrationsprivilegien. Die folgenden Anweisungen gehen jeweils davon aus, dass ein normaler User genutzt wird. Wenn der noch nicht existiert, muss man sich also erst einmal als root anmelden und einen unprivilegierten User anlegen. Das macht man mit dem Befehl:


adduser --shell /bin/bash [Benutzername]
            
In den darauf folgenden Dialogen wird man aufgefordert, ein Passwort für den User zu vergeben. Ggf. muss man noch das Tool sudo installieren, mit dem man dem normalen User das Recht einräumen kann, bedarfsweise für einzelne Anweisungen Root-Rechte zu nutzen.

usermod -aG sudo [Benutzername]
            
Danach kann man sich ab- und mit dem neuen Useraccount wieder anmelden.



Installation Fail2Ban

Zunächst installiere ich auf Servern, die ins Internet exponiert werden immer fail2ban und schütze das System auf diese Weise vor Brute Force Attacken gegen die SSH-Schnittstelle. Die Installation geht einfach mit dem Befehl


sudo apt install fail2ban
            
Damit ist bereits die Absicherung der SSH-Schnittstelle standardmäßig aktiv. Hier muss zunächst nichts weiter gemacht werden. Später werde ich fail2ban u.a. noch auf die Anmeldung an der Weboberfläche von Paperless-NGX ansetzen.



Konfiguration der Firewall(s)

Wenn der Hoster eine Firewall anbietet, die bereits zwischen dem Server und dem Internet arbeitet, dann sollte diese auf jeden Fall genutzt werden. Alles, was abgefangen wird, bevor es überhaupt den Server erreicht, belastet natürlich nicht die Systemressourcen unseres Servers. Aber auch wenn es eine solche Firewall des Hosters gibt, sollte noch eine Firewall auf dem Server eingesetzt werden. IT-Sicherheit basiert immer auf dem Prinzip der möglichst vielen Schichten. Ich verwende die ufw, die uncomplicated firewall. Soweit sie noch nicht auf dem Server installiert ist, hole ich das mit


sudo apt install ufw
            
nach. Dann schließe ich zunächst alle eingehenden Verbindungen aus

sudo ufw default deny incoming
            
und erlaube dann die Verbindungen, die ich unbedingt brauche

sudo ufw allow 51820
sudo ufw allow 22
sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow from 100.11.0.0/24 to any port 22 proto tcp
sudo ufw allow from 100.11.0.0/24 to any port 8080 proto tcp
sudo ufw allow from 100.11.0.0/24 to any port 8200 proto tcp
sudo ufw allow from 100.11.0.0/24 to any port 445 proto tcp
            
Hier nehme ich bereits die Adressen des VPN vorweg. Dieses werde ich für den Adressbereich 10.11.0.1 bis 100.11.0.255 einrichten. Um das VPN überhaupt etablieren zu können, wird ein offener Port 51820 benötigt. Zunächst brauche ich auch einen offenen Port 22. Später soll nur aus dem VPN heraus soll auf die SSH-Shell (Port 22) zugegriffen werden können. Dann schließe ich die allgemeine Freigabe von Port 22 wieder. Ebenso soll auch auf die Weboberfläche des Reverse Proxy (Port 8080) und die Weboberfläche von Duplicati (Port 8200) nur aus dem VPN heraus zugegriffen werden können. Außerdem soll das Consume-Verzeichnis von Paperless NGX später per Samba für das VPN freigegeben werden. Dafür wird Port 445 benötigt. Diese Einstellungen aktiviere ich mit

sudo ufw enable
            
und nach einer Sicherheitsabfage sind die Firewalleinstellungen aktiv. Wenn ich mich jetzt nicht ausgesperrt habe, dann bleibt die Verbindung bestehen und ich kann weiter arbeiten.



Konfiguration Fail2Ban für ufw

Wenn die ufw unbefugte Zugriffe auf den Server unterbindet, dann kann man sehr schnell sehen, dass es massenhaft Zugriffsversuche aus dem Netz gibt. Fail2Ban kann diese Zugriffsversuche bereits eine Ebene früher abfangen und die ufw damit entlasten. Dazu definiere ich einen entsprechenden Jail


sudo nano /etc/fail2ban/jail.d/ufw.conf
            
mit folgendem Inhalt:

[ufw-block]
enabled = true
logpath = /var/log/ufw.log
# Der Filter sucht nach dem UFW-Block-Muster in den Log-Zeilen
filter = ufw-block

# Aktionsdefinition: UFW (iptables) zum Blockieren verwenden
banaction = ufw
maxretry = 3               ; Maximale Versuche (UFW-Blocks) in der Findtime
findtime = 600             ; Zeitraum in Sekunden (10 Minuten)
bantime = 3600             ; Dauer der Sperre in Sekunden (1 Stunde)
            
Die Kommentare im Code erklären, was passiert. Dazu brauche ich eine entsprechende Filterdatei.

sudo nano /etc/fail2ban/filter.d/ufw-block.conf
            
mit folgendem Inhalt:

[INCLUDES]
before = common.conf

[Definition]
# Die RegEx sucht nach den typischen Kernel-Log-Einträgen mit UFW BLOCK,
# gefolgt von der Quell-IP-Adresse (SRC=) als Hostname-Feld.
failregex = \[UFW BLOCK\].*SRC=

ignoreregex =
            
Eine Aktion, die die Übeltäter dann auch blockiert sollte mit der Installation von fail2ban bereits in der Datei /etc/fail2ban/action.d/ufw.conf definiert sein:

# Fail2Ban action configuration file for ufw
#
# You are required to run "ufw enable" before this will have any effect.
#
# The insert position should be appropriate to block the required traffic.
# A number after an allow rule to the application won't be of much use.

[Definition]

actionstart = 

actionstop = 

actioncheck = 

# ufw does "quickly process packets for which we already have a connection" in before.rules,
# therefore all related sockets should be closed
# actionban is using `ss` to do so, this only handles IPv4 and IPv6.

actionban = if [ -n "<application>" ] && ufw app info "<application>"
            then
              ufw <add> <blocktype> from <ip> to <destination> app "<application>" comment "<comment>"
            else
              ufw <add> <blocktype> from <ip> to <destination> comment "<comment>"
            fi
            <kill>

actionunban = if [ -n "<application>" ] && ufw app info "<application>"
              then
                ufw delete <blocktype> from <ip> to <destination> app "<application>"
              else
                ufw delete <blocktype> from <ip> to <destination>
              fi

# Option: kill-mode
# Notes.: can be set to ss or conntrack (may be extended later with other modes) to immediately drop all connections from banned IP, default empty (no kill)
# Example: banaction = ufw[kill-mode=ss]
kill-mode =

# intern conditional parameter used to provide killing mode after ban:
_kill_ =
_kill_ss = ss -K dst "[<ip>]"
_kill_conntrack = conntrack -D -s "<ip>"

# Option: kill
# Notes.: can be used to specify custom killing feature, by default depending on option kill-mode
# Examples: banaction = ufw[kill='ss -K "( sport = :http || sport = :https )" dst "[<ip>]"']
#           banaction = ufw[kill='cutter "<ip>"']
kill = <_kill_<kill-mode>>

[Init]
# Option: add
# Notes.: can be set to "insert 1" to insert a rule at certain position (here 1):
add = prepend

# Option: blocktype
# Notes.: reject or deny
blocktype = reject

# Option: destination
# Notes.: The destination address to block in the ufw rule
destination = any

# Option: application
# Notes.: application from sudo ufw app list
application = 

# Option: comment
# Notes.: comment for rule added by fail2ban
comment = by Fail2Ban after <failures> attempts against <name>

# DEV NOTES:
# 
# Author: Guilhem Lettron
# Enhancements: Daniel Black
            
Diese muss ich nicht ändern. Mit

sudo systemctl restart fail2ban
            
stoße ich die Aktualisierung von fail2ban an. Server, die unmittelbar im Internet stehen, werden sofort und ständig von Bots angesprochen, die versuchen, irgendwie in das System einzudringen. Nach kurzer Zeit kann man mit

sudo fail2ban-client status ufw-block
            
nachsehen, was fail2ban schon so alles erkannt und blockiert hat. Die Ausgabe sieht dann in etwa so aus:

Status for the jail: ufw-block
|- Filter
|  |- Currently failed:    34
|  |- Total failed:    5551
|  `- Journal matches:    
`- Actions
   |- Currently banned:    20
   |- Total banned:    157
   `- Banned IP list:    78.128.114.86 106.55.180.162 167.94.138.38 79.124.62.126 79.124.62.134 167.94.138.187 79.124.58.18 167.94.146.49 185.242.226.76 193.163.125.181 206.168.34.199 162.142.125.212 82.156.52.230 167.94.138.196 78.128.114.126 199.45.155.93 206.168.34.198 162.142.125.217 213.209.143.88 167.94.138.43
            
Es wurden also 5551 Zugriffe innerhalb der letzten 24 Stunden von ufw abgewehrt, davon 34 innerhalb der letzten 10 Minuten und 20 Adressen sind aktuell gesperrt, weil sie mehr als drei Fehlversuche unternommen haben. Man kann erkennen, dass es durchaus sinnvoll ist, das System zu schützen. Hätte ich vielleicht die gesperrten IP-Adressen unkenntlich machen sollen? Nö. Es handelt sich um echte Adressen von Systemen, die versucht haben, meinen Server zu hacken.



Installation und Konfiguration der Software

Installation und Konfiguration von Wireguard

Wireguard ist eine leichtgewichtige und effiziente VPN-Lösung. Ein VPN, also ein virtuelles privates Netzwerk stellt eine Möglichkeit dar, innerhalb eines öffentlichen Netzwerks oder sogar über Netzwerkgrenzen hinweg mittels Verschlüsselungstechnologie ein abgeschottetes Netzwerk zu etablieren. Das ist ein starkes Sicherheitsfeature, um Zugriffe auf sensible Dienste und Daten abzusichern. Zunächst installiere ich wireguard mit


sudo apt install wireguard
            
In Datei /etc/sysctl.conf ist die Raute vor dem Eintrag „net.ipv4.ip_forward=1“ und zu entfernen. Wenn es diese Datei noch nicht geben sollte, dann muss sie angelegt werden mit diesem Inhalt:

net.ipv4.ip_forward=1
            
Das ist erforderlich, damit der Server IP-Forwarding erlaubt.
Dann wechsele ich in den Root-Modus, weil ich in den geschützten Ordner /etc/wireguard wechseln und außerdem mehrere Befehle mit Adminrechten ausführen muss.

sudo su
            
und wechsele in den Ordner /etc/wireguard

cd /etc/wireguard
            
Zunächst erstelle ich ein Schlüsselpaar für die Nutzung von Wireguard

umask 077; wg genkey | tee privatekey | wg pubkey > publickey

            
Die Schlüssel lasse ich mir anzeigen, um sie für die spätere Verwendung über die Zwischenablage wegzusichern:

cat privatekey
cat publickey
            
Dann schränke ich die Zugriffsrechte auf den privaten Schlüssel auf den Besitzer der Datei ein, sonst arbeitet Wireguard nicht damit:

chmod 600 privatekey
            
Nun erstelle ich eine Datei zur Konfiguration des VPN-Tunnels. Ich nenne sie wg10011.conf, weil ich das Subnetz 100.11.0.0/24 nutzen möchte.

nano wg10011.conf
            
mit folgendem Inhalt:

[Interface]
Address = 100.11.0.1/32
SaveConfig = true
PostUp = iptables -A FORWARD -i wg10011 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens18 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg10011 -j ACCEPT; iptables -t nat -D POSTROUTING -o ens18 -j MASQUERADE
ListenPort = 51820
PrivateKey = Private Key des Servers
            
Der Server wird hier als VPN-Router fungieren und erhält die VPN-Adresse 100.11.0.1. Die Parameter PostUp und PostDown definieren, was beim Start des VPN-Tunnels und bei dessen Beendigung passieren soll. Es wird ein internes Nating, also ein virtuelles Netzwerk generiert. Hier muss die richtige Bezeichnung der Netzwerkschnittstelle eingetragen werden. Bei meinem Beispiel ist es "ens18". Die individuell richtige kann man mit dem Befehl

ip a
            
ermitteln. Es ist in der Regel die zweite Schnittstelle nach "lo", der Loopback-Schnittstelle. Den ListenPort kann man ändern, wenn man das möchte. Hier nehme ich 51820, was Standard ist für Wireguard. Dass ich unter PrivateKey den soeben kopierten privaten Schlüssel eintrage, ist klar. Nun kann ich mit

wg-quick up wg10011
            
den Tunnel starten und mit

systemctl enable wg-quick@wg1011
            
dafür sorgen, dass er bei künftigen Neustarts des Servers direkt mit gestartet wird.

Zur Einrichtung des Wireguard-Tunnels auf dem Client wechsele ich in die Linux-Konsole dieses Clients und installiere auch dort, wenn nicht bereits geschehen Wireguard, erstelle das Schlüsselpaar und setze die entsprechenden Dateirechte auf den privaten Schlüssel:

sudo apt install wireguard
sudo su
cd /etc/wireguard
umask 077; wg genkey | tee privatekey | wg pubkey > publickey
cat privatekey
cat publickey
            
dann erstelle ich die Konfigurationsdatei für den VPN-Tunnel mit

sudo nano /etc/wireguard/wg10011.conf
            
mit folgendem Inhalt:

[Interface] 
PrivateKey = {privater Schlüssel des Clients}
Address = 100.11.0.36/32 

[Peer] 
PublicKey = {öffentlicher Schlüssel des Servers}
Endpoint = {IP-Adresse des Servers}:51820
AllowedIPs = 100.11.0.0/24
PersistentKeepalive = 25
            
Den neue Tunnel aktiviere ich und mache ihn bootfest mit

wg-quick up wg10011
systemctl enable wg-quick@wg10011
            
Danach verlasse ich den Root-Modus wieder mit

exit
            
Nun muss ich auf dem Server noch den Client in das VPN aufnehmen. Ich melde mich also wieder remote auf dem Server an und setze den Befehl

sudo wg set wg1011 peer {öffentlicher Schlüssel des Clients} allowed-ips 100.11.0.36/32
            
ab. Dann veranlasse ich Wireguard, die Konfiguration neu einzulesen:

wg-quick strip wg10011 > /tmp/wg10011_new.conf
wg syncconf wg10011 /tmp/wg10011_new.conf
rm /tmp/wg10011_new.conf
            
Was passiert da? Das teste ich mit

ping 100.11.0.36
            
auf dem Server und/oder mit

ping 100.11.0.1
            
auf dem Client. Beides sollte funktionieren, wenn ich keinen Fehler gemacht habe. Wenn das nicht klappt, muss ich die Konfigurationsdateien prüfen. Ein häufiger Fehler besteht darin, privaten und öffentlichen Schlüssel zu verwechseln. Wenn alles klappt, dann kann ich den Port 22 für die große weite Welt schließen, so dass er künftig nur noch aus dem VPN heraus genutzt werden kann. Ich schaue mir die aktuellen Firewalleinstellungen mit

sudo ufw status numbered 
            
und identifiziere den Eintrag

[xx]  22    ALLOW IN    Anywhere
            
angenommen, der Eintrag ist Nummer 2 in der Liste, dann entferne ich ihn mit

sudo ufw delete 2
            
und bestätige die Sicherheitsabfrage. Um das wirksam zu machen, muss ich den Befehl

sudo ufw enable
            
absetzen, weil ein Reload nicht möglich ist. Jetzt fliege ich vermutlich aus der SSH-Session heraus, weil sie über den weltweit offenen Port 22 aufgebaut wurde. Mit

ssh -l andreas 100.11.0.1
            
kann ich mich aber sofort wieder anmelden.



Installation Docker

Da die mit den gängigen Linux-Distributionen mitgelieferten Docker-Implementierungen nicht immer ganz kompatibel mit den auf Github bereitgestellten Containern ist, installiere ich Docker und Docker Compose aus den Originalquellen. Dazu habe ich mir ein kleines Script geschrieben:

# Shell-Skript zur Installation von Docker (mit sudo ausführen:
#!/bin/bash
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
apt-get remove $pkg;
done
apt-get update
apt-get install ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
 "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
 $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl start docker
systemctl enable docker
usermod -aG docker "$SUDO_USER"
            
Wichtig: Wenn man keinen Ubuntu-Server nutzt, sondern ein Debian, dann muss man in dem Script die Zeichenkette ubuntu gegen debian austauschen. Dieses Script muss zunächst gespeichert werden, z.B. unter dem Namen installdocker.sh. Dann muss es ausführbar gemacht werden mit

chmod +x installdocker.sh
            
Danach muss es mit Rootrechten ausgeführt werden:

sudo ./installdocker.sh
            
Das Script deinstalliert zunächst ggf. vorhandene Bibliotheken, die Probleme machen könnten und installiert dann die benötigten Pakete. Außerdem fügt es den aufrufenden Benutzer (was trotz "sudo" nicht root ist) der Gruppe docker hinzu, damit ich nachher ohne sudo auch docker-Befehle ausführen kann. Nun muss man sich einmal kurz ab- und wieder anmelden, damit die neue Dockerberechtigung auch greift. Ob das alles funktioniert hat prüfe ich mit dem Befehl:

docker ps
            
Damit würden laufende Docker-Container aufgelistet. Hier laufen zwar noch keine, aber wenn ich hierauf keine Fehlermeldung erhalte, sondern die Überschriften einer leeren Tabelle, dann ist alles richtig.



Installation traefik Reverse Proxy

Der Reverse Proxy traefik wird ganz einfach mit docker und docker-compose installiert. Ich erstelle zunächst einen Ordner traefik und wechsele in selbigen


mkdir traefik && cd traefik
            
Hier erstelle ich eine Datei namens docker-compose.yml

nano docker-compose.yml
            
mit folgendem Inhalt:

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    network_mode: host
    restart: unless-stopped
    command:
      # Entrypoints definieren (Ports, auf denen Traefik lauscht)
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.forwardedheaders.trustedIPs=0.0.0.0/0
      - --entrypoints.websecure.forwardedheaders.trustedIPs=0.0.0.0/0
      - --api.dashboard=true
      - --entrypoints.traefik.address=10.11.0.1:8080
      - --providers.providersThrottleDuration=0

      # Provider: Traefik soll Docker-Labels überwachen
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false # Nur Container mit 'traefik.enable=true' beachten
      - --api.insecure=true

      # HTTPS-Zertifikate automatisch von Let's Encrypt holen
      - --certificatesresolvers.letsencrypt.acme.email=mail@sechzig-veedel.de
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.letsencrypt.acme.tlschallenge=true

    ports:
       - "8080"

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro # Lesezugriff auf Docker-Events
      - /home/andreas/traefik/traefik-data:/letsencrypt

volumes:
  traefik-data:
            
Hier sind folgende Besonderheiten zu beachten: Aus dem Internet hört traefik auf die Ports 80 und 443. Zwar soll Paperless-NGX später nur verschlüsselt, also über Port 443 erreichbar sein, Port 80 (http) wird aber benötigt, um ein Let's Encrypt-Zertifikat beziehen zu können. Mit dem Parameter "forwardedheaders.trustedIPs=0.0.0.0/0" wird traefik angewiesen, die Header der aufrufenden Systeme zu prüfen und ihnen genug zu vertrauen, um ihre Adressen aufzunehmen. Das ist wichtig, damit später fail2ban auf die Adressen der Systeme angesetzt werden kann, die versuchen, die Seite aufzurufen.

Mit "api.dashboard=true" aktiviere ich das Dashboard von trafik und Port 8080, der für das Web-Dashboard von traefik genutzt wird, wird an die IP-Adresse aus dem VPN gebunden.

Dann kann ich traefik starten mit:

docker compose up -d
            
Wer Docker nicht mit dem oben aufgeführten Script oder sonst wie aus den Originalquellen installiert hat, sondern zum Beispiel aus den Repositories der Linux-Distribution, muss ggf. den Befehl docker-compose (also mit Bindestrich) verwenden. Nun sollte traefik laufen. Das kann man sich mit

docker ps
            
ansehen. Hier sollten nun der Container "traefik" mit dem Status "up" zu sehen sein. Dann hat alles geklappt.



Installation und Konfiguration Paperless-NGX

Auch Paperless-NGX wird mit docker und docker-compose aufgesetzt. Zunächst wechsele ich aus dem Verzeichnis traefik wieder zurück in mein Homeshare


cd ..
            
und erstelle ein Verzeichnis paperless-ngx, in das ich dann auch wechsele

mkdir paperless-ngx && cd paperless-ngx
            
Darin erstelle ich zunächst eine Datei docker-compose.env

nano docker-compose.env
            
mit folgendem Inhalt:

PAPERLESS_URL=https://paperless.domain.de
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
PAPERLESS_SECRET_KEY='V<_dddxkOW[.l,VF{:%cQAT|$uAIZY)n;MWH/LU?MJ_hBV7HGb%7KhY)+dSethRvWSV'
            
Dabei ist beim URL idie Domain einzutragen, unter der Paperless-NGX später erreichbar sein soll und für die ich auf meinen Server einen A-Record eingetragen habe. Für den Secret-Key kann eine beliebige, aber möglichst lange Zeichenkette genutzt werden. Die anderen Parameter sind wohl selbst erklärend. Nun brauche ich natürlich noch eine Datei docker-compose.yml

nano docker-compose.yml
            
mit folgendem Inhalt:

services:
  broker:
    image: docker.io/library/redis:8
    restart: unless-stopped
    volumes:
      - redisdata:/data
  db:
    image: docker.io/library/postgres:17
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: paperless
  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on:
      - db
      - broker
      - gotenberg
      - tika
    labels:
         # 1. Traefik aktivieren
      - "traefik.enable=true"

      # 2. Router definieren (Definiert die Domain und das Protokoll)
      - "traefik.http.routers.paperless.entrypoints=websecure" # Nur über 443
      - "traefik.http.routers.paperless.rule=Host(`paperless.domain.de`)" # Ihre Subdomain
      - "traefik.http.routers.paperless.tls=true"
      - "traefik.http.routers.paperless.tls.certresolver=letsencrypt" # Nutze den oben definierten Resolver

      # 3. HTTP-zu-HTTPS-Umleitung (optional, aber empfohlen)
      - "traefik.http.routers.paperless-insecure.entrypoints=web" # Lausche auf Port 80
      - "traefik.http.routers.paperless-insecure.rule=Host(`paperless.domain.de`)"
      - "traefik.http.routers.paperless-insecure.middlewares=redirect-to-https"

      # 4. Middleware (Die Weiterleitung selbst)
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.middlewares.paperless-realip.headers.customrequestheaders.X-Forwarded-For={client.ip}"
      - "traefik.http.routers.paperless.middlewares=paperless-realip@docker"

      # 5. Service definieren (Wo Traefik die Anfrage hinschicken soll)
      - "traefik.http.services.paperless.loadbalancer.server.port=8000"

    volumes:
      - /home/andreas/paperless-ngx/data:/usr/src/paperless/data
      - /home/andreas/paperless-ngx/media:/usr/src/paperless/media
      - /home/andreas/paperless-ngx/export:/usr/src/paperless/export
      - /home/andreas/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
      TRUSTED_PROXIES: "172.18.0.0/16" # das Docker-Subnetz
      USE_X_FORWARDED_FOR: True
      PAPERLESS_ALLOWED_HOSTS: '*'
      PAPERLESS_LOG_IP_ADDRESS_HEADER: X-Forwarded-For
      PAPERLESS_LOGLEVEL: DEBUG

  gotenberg:
    image: docker.io/gotenberg/gotenberg:8.20
    restart: unless-stopped
    command:
      - "gotenberg"
      - "--chromium-disable-javascript=true"
      - "--chromium-allow-list=file:///tmp/.*"
  tika:
    image: docker.io/apache/tika:latest
    restart: unless-stopped
volumes:
  pgdata:
  redisdata:
            
Hier ist jetzt einiges erläuterungsbedürftig. Mit den Labels werden die Parameter für die Auslieferung der Seiten durch den Reverse Proxy definiert. Dabei ist "traefik.enable=true" natürlich die Aktivierung von Trafik. Im Router-Teil werden die Domain und Protokolle definiert. Auch das ist weitestgehend selbsterklärend. Dann wird eine Umleitung von http zu https definiert und vor allem die eigentliche Weiterleitung von der ungeschützten auf die verschlüsselte Seite. Schließlich wird der Service selbst definiert.

Die gemounteten Volumes beginnen hier immer mit "/home/andreas/paperless-ngx". Ein symbolischer Pfad auf das Home-Share ("~/) würde im Docker-Container nicht aufgelöst werden können. Wenn der Nutzer, in dessen Kontext das Ganze läuft, nicht "andreas" heißt, dann muss das natürlich angepasst werden.

Unter Environment sind noch die Parameter

TRUSTED_PROXIES: "172.18.0.0/16" # das Docker-Subnetz
USE_X_FORWARDED_FOR: True
PAPERLESS_ALLOWED_HOSTS: '*'
PAPERLESS_LOG_IP_ADDRESS_HEADER: X-Forwarded-For
PAPERLESS_LOGLEVEL: DEBUG
            
wichtig, damit die von traefik übermittelten IP-Adressen der aufrufenden Systeme von Paperless-NGX auch verarbeitet werden können und im Log landen (wegen fail2ban).
Die restlichen Parameter sind Standard. Diese Konfiguration kann dann ebenfalls mit

docker compose up -d
            
gestartet werden. Wenn keine Fehler aufgetreten sind, dann starten nun die benötigten Container. Das dauert ein wenig. Mit

docker ps
            
kann man sich den aktuellen Status ansehen und mit

docker logs paperless-ngx-webserver-1
            
kann man sich die Logs des Webservers ansehen. Sollte etwas nicht korrekt sein, dann würden hier Fehlermeldungen oder zumindest Warnungen auftreten.



Absicherung Paperless-NGX mit Fail2Ban

Noch ist der Server nicht vor Brute Force Attacken, also dem massenhaften, ggf. automatisierten Versuch, Benutzernamen und Passworte zu erraten, gesichert. Einen wirksamen Schutz vor Brute Force Attacken bieten eine Multifaktor-Authentifizierung, also die Anmeldung an dem System mit mindestens zwei Authentifizierungsfaktoren, nach dem Prinzip "Kennen und Haben". Allerdings muss das im jeweiligen Anwender*innen-Kontext eingerichtet werden und man kann die Anwender*innen nicht zwingen, das zu tun. Darum ist meines Erachtens zusätzlich eine serverseitige Absicherung erforderlich. Das bietet fail2ban. Ganz am Anfang hatte ich ja fail2ban installiert, ohne weitere Konfigurationen daran vorzunehmen. Auf diese Weise sichert fail2ban zunächst die SSH-Schnittstelle gegen Brute Force Attacken ab, aber nicht mehr. Nach der Firewall-Konfiguration habe ich fail2ban auf die ufw-Protokolle angesetzt und nun setze ich es auf die Anmeldung an Paperless-NGX an. Paperless-NGX speichert Logfiles in der Datei .../data/log/paperless.log. Das data-Verzeichnis habe ich in der docker-compose.yml auf den Pfad /home/andreas/paperless-ngx gemounted. Ich erstelle nun eine Filterdatei, die dieses Logfile auswertet und dort verzeichnete fehlerhafte Anmeldeversuche erkennt. Dazu erstelle ich die Datei /etc/fail2ban/filter.d/paperless-block.conf


sudo nano /etc/fail2ban/filter.d/paperless-block.conf
            
mit folgendem Inhalt:

[INCLUDES]
before = common.conf

[Definition]
# Paperless-Zeitstempel sind nicht nötig
datepattern = 

# Regex für fehlgeschlagene Logins
failregex = \[paperless\.auth\]\s*Login failed for user `.*` from private IP ``\.

ignoreregex =
            
Diese Filterdefinition erkennt nun das Vorkommen der Zeichenkette "Login failed for user... from private IP..." und ermittelt in der Variable die IP-Adresse des "Angreifers". Unter ignoregex könnte man noch IP-Adressen erfassen, die von einer Sperre ausgenommen werden sollen. Darauf habe ich hier verzichtet, damit ich das Ganze auch testen kann. Später könnte ich dort die IP-Adressen aus meinem privaten Netzwerk ausschließen. Noch wird diese Filterdefinition aber nicht angewendet. Dazu muss erst ein Jail angelegt werden. Ich erstelle dazu die Datei /etc/fail2ban/jail.d/paperless.conf

sudo nano /etc/fail2ban/jail.d/paperless.conf
            
mit folgendem Inhalt:

[paperless]
enabled = true
filter = paperless-block

# Verwende die Multiport-Action für Container-Netz
banaction = paperless-reject

# Ports innerhalb des Docker-Netzes blockieren, Host bleibt unberuehrt
port = 443

# Logfile, das vom Host erreichbar ist
logpath = /home/andreas/paperless-ngx/data/log/paperless.log
backend = polling

# auch hier IPs vom Bann ausschliessen
ignoreip = 

maxretry = 3
findtime = 600
bantime = 3600
            
Hier wird zunächst der eben definierte Filter verwendet. Dann wird eine banaction bestimmt. Das ist eine Aktion, die zum Aussperren der erkannten Übeltäter verwendet werden soll. Diese muss ich noch definieren. Geblockt werden sollen Zugriffe auf das Docker-Netzwerk. Das ist nun etwas schwer zu verstehen. Endanwender*innen kommen mit einer normalen, öffentliche IP-Adresse an. Diese ist zu sperren. Innerhalb des Docker-Netzwerks gibt es eigene Adressen aus dem Adressraum 172.17.0.0/24 und nicht zuletzt habe ich ja noch ein VPN definiert, mit dem Adressraum 10.11.0.0/24. Hier sollen aber die öffentlichen IPs der zugreifenden Systeme erkannt und ggf. geblockt werden.

Der Parameter logpath ist selbsterklärend. Wichtig ist danach der Parameter "backend = polling". Der bewirkt, dass tatsächlich dieses Logfile ausgewertet wird und nicht irgendwelche Systemprotokolle. Dann könnte ich auch hier noch einmal IP-Adressen vom Blockieren ausschließen. Der Parameter maxretry definiert natürlich die Anzahl der erlaubten Versuche. Findtime definiert den Suchzeitraum in Millisekunden, also den Zeitraum innerhalb dessen die Fehlerversuche gezählt werden. Hier habe ich mit 600 eine Minute definiert. Wenn ich innerhalb einer Minute dreimal versuche, mich mit falschen Credentials anzumelden, werde ich gesperrt. Bots, die durch das Netz schwirren, unternehmen sehr viele Zugriffsversuche in sehr kurzer Zeit und werden schnell erkannt. Wenn ich mich beim Anmelden zweimal vertippt habe, dann warte ich eine Minute und kann gefahrlos einen dritten Versuch unternehmen. Die bantime, also die Zeitspanne einer Sperrung beträgt hier 3600 Millisekunden, also eine Stunde. Diese Werte kann man nach Belieben ändern.

Nun muss ich aber auch noch die Banaction definieren. Das mache ich in der Datei /etc/fail2ban/action.d/docker-iptables.conf

sudo nano /etc/fail2ban/action.d/paperless-reject.conf
            
mit folgendem Inhalt:

[Definition]
actionstart = 
actionstop = 
actioncheck = 
# Nutze die INPUT Chain für den Host-Modus, um den Traffic am Host abzufangen
# Der REJECT sorgt für eine sofortige Ablehnung.
actionban = iptables -I INPUT -p tcp -s  --dport 443 -j REJECT --reject-with icmp-port-unreachable
actionunban = iptables -D INPUT -p tcp -s  --dport 443 -j REJECT --reject-with icmp-port-unreachable
            
User (bzw. deren IP-Adressen) werden auf der Firewall iptables gesperrt, indem ihre Zugriffsversuche gedropt werden. Da docker die ufw ignoriert, ist dieser Weg über iptables erforderlich. So funktioniert es aber. Wenn ich nun fail2ban mit

sudo systemctl restart fail2ban
            
neustarte, dann wird der neue Filter aktiv. Ich kann mir jederzeit den aktuellen Stand mit

sudo fail2ban-client status paperless
            
ansehen.



Scannen

Der Consume-Ordner der Paperless-Installation ist nur über SFTP und nur innerhalb des VPN erreichbar. Mit gängigen Scannern wird man wohl nicht einrichten können, dass in dieser Weise in den Consume-Ordner gescannt wird. Der beste Weg, direkt in das System zu scannen, ist daher Scan2Email am Scanner einzurichten und in Paperless-NGX entsprechende eMailkontoeinstellungen und eMailregeln zu definieren. Wenn der Scanner aber nicht in der Lage ist, an eine eMail-Adresse zu scannen, dann muss man über einen Umweg in den consume-Ordner scannen, wenn man die Digitalisate nicht zu Fuß übertragen möchte. Hierzu gebe ich den consume-Ordner mittels Samba frei. Dann mounte ich den freigegebenen Ordner an meinem Client und gebe diesen wiederum im Netzwerk frei. Das ist zugegebenermaßen kein besonders eleganter Weg. Aber er funktioniert und ein besserer fällt mir nicht ein.

Samba kann ganz einfach mit dem Befehl


sudo apt install samba
            
installiert werden. Dann muss ich zunächst einen Samba-User anlegen, weil Samba eine eigene Benutzerverwaltung hat. Den Benutzer muss es als User auf dem System aber auch bereits geben. Ich verwende daher meinen üblichen Benutzeraccount und wende den Befehl

sudo smbpasswd -a [username]
            
an. Dann werde ich nach einem neuen Passwort gefragt (2 mal) und der Benutzer wird angelegt. Nun brauche ich nur noch eine Freigabe einzurichten. Dazu bearbeite ich die Datei /etc/samba/smb.conf mit Adminrechten

sudo nano /etc/samba/smb.conf
            
und füge am Ende ein:

[scans]
path = /home/[username]/paperless-ngx/consume
read only = no
guest ok = no 
            
Mehr brauche ich hier nicht einzustellen. Ich habe ja den Zugriff auf den Server über die Firewall begrenzt und den Port für Samba ausschließlich für das VPN freigegeben. Mit

sudo systemctl daemon-reload
            
zwinge ich das System dazu, die Systemdateien neu einzulesen und mit

sudo systemctl restart smbd.service
            
starte ich den Samba-Server neu. Nun ist die Freigabe im VPN für den User andreas verfügbar. Wenn ich einen Windows-Client nutzen würde, dann würde ich nun über den Windows-Explorer diese Netzwerkfreigabe als Netzwerklaufwerk verbinden und im Netz freigeben. Unter Linux muss ich zunächst auf dem Client die Freigabe mounten. Ich bearbeite dazu mit root-Rechten (auf dem Client, nicht auf dem Server) die Datei /etc/fstab

sudo nano /etc/fstab
            
und trage dort neu ein:

//100.11.0.1/scans /home/andreas/scans cifs _netdev,x-systemd.requires=wg-quick@wg10011.service,credentials=/pfad/zu/deiner/.smbcredentials,uid=1000,gid=1000 0 0
            
Der Parameter _netdev kennzeichnet die Freigabe als Netzwerk-Gerät. Systemd weiß dadurch, dass es mit dem Mounten warten soll, bis das grundlegende Netzwerk aktiv ist. Mit x-systemd.requires=wg-quick@wg1011.service wird Systemd mitgeteilt, dass die Mount-Unit erst gestartet werden darf, nachdem die Unit wg-quick@wg1011.service erfolgreich gestartet wurde und bereit ist. Die Credentials für die Samba-Freigabe werden hier in der Datei "/root/.smbcredentials" erwartet. Diese muss ich zunächst mit

sudo nano /root/.smbcredentials
            
und dem Inhalt

username=[username]
password=[smb-password]
            
erstellen. Dann muss ich die Berechtigungen für den Root-User dafür vergeben mit

sudo chown root:root /root/.smbcredentials
            
und mit

sudo chmod 600 /root/.smbcredentials
            
dafür sorgen, dass nur der Root-User diese sensiblen Daten lesen kann. Außerdem muss ich den lokalen Ordner scans ich noch erstellen:

mkdir scans
            
und dann einmal mounten (beim nächsten Start des Clients wird das automatisch gemountet, falls das VPN verfügbar ist. Wenn ich bisher keine SMB-Freigaben auf diesem Client nutze, muss ich zunächst die cifs-utils installieren:

sudo apt install cifs-utils
            
Danach kann ich das Verzeichnis mounten.

sudo mount scans
            
oder mit

sudo mount -a
            
alles mounten, was in /etc/fstab steht und noch nicht gemountet wurde. Nun habe ich den Ordner consume des Paperless-NGX-Servers als Verzeichnis scans lokal verfügbar. Alle Dateien, die ich dort hineinschiebe und die Paperless-NGX verarbeiten kann, werden automatisch verarbeitet. Damit der Scanner auch das Verzeichnis nutzen kann, muss ich auf dem Client nun ebenfalls Samba installieren. Serverdienste auf einem Client sind natürlich nicht im Sinne des Erfinders. Das ist es, was ich mit nicht elegant gemeint habe. Aber nur so funktioniert es. Ich installiere also wie oben beschrieben auch auf dem Client den Samba-Server, füge ebenfalls den Samba-User andreas hinzu und erstelle in der Datei /etc/samba/smb.conf die Freigabe

sudo nano /etc/samba/smb.conf
            
mit folgendem Inhalt:

[scans] path = /home/[username]/scans
read only = no
guest ok = no
            
nach

sudo systemctl daemon-reload
            
und

sudo systemctl restart smbd.service
            
steht der Ordner scans im lokalen Netzwerk unter der lokalen IP-Adresse meines Clients zur Verfügung und kann vom Scanner genutzt werden. Der Scanner greift also über die lokale Netzwerkadresse meines Clients auf die Freigabe zu welche eine Verbindung über das VPN auf den entfernten Server darstellt.



Datensicherung

Wenn ich dem System Dokumente anvertraue, die ich vielleicht ansonsten nicht mehr aufbewahre, dann muss ich mich natürlich um eine zuverlässige Datensicherung kümmern. Hierfür bietet sich das Open Source Tool Duplicati an. Es lässt sich bequem über Docker installieren und bietet verschlüsselte, inkrementelle Backups auf verschiedenste Sicherungsziele an. Die Sicherungen auf dem selben System abzulegen, auf dem auch schon die Daten liegen, ist natürlich komplett sinnfrei. Daher brauche ich ein externes Sicherungsziel. Hier nutze ich, wie eingangs erklärt, Speicherlösungen bei Hosting-Anbietern. Die Einrichtung der Speichers bei externen Hostern führe ich hier nicht aus, weil es viele verschiedene Anbieter und Konfigurationsoberflächen gibt. Im Ergebnis setze ich im Folgenden voraus, dass es einen Speicher gibt, der per SFTP (SSH) angesprochen werden kann.



Export der Inhalte und Metadaten

Paperless-NGX bietet mit dem document-exporter eine komfortable Möglichkeit, alle Dokumente, Datenbankinhalte und Strukturen zu exportieren. Das Ergebnis dieses Exports kann dann gesichert werden und damit ist auch eine Wiederherstellung des Systems jederzeit möglich. Der document-exporter wird mit


docker exec -it paperless-ngx-webserver-1 document_exporter /usr/src/paperless/export
            
aufgerufen bzw. genutzt. Hierbei muss der Pfad angegeben werden, den der Export-Ordner innerhalb des Docker-Containers nutzt. Gemountet ist dieser Ordner auf den Pfad /home/andreas/paperless-ngx/export. Dieser Export soll natürlich einmal am Tag ausgeführt werden, also definiere ich einen Cronjob dafür. Da der Job im normalen User-Kontext also ohne Adminitrationsrechte ausgeführt werden soll, kann ich ihn in der Crontab für den Standarduser einrichten. Ich rufe die crontab-Bearbeitung mit

crontab -e
            
auf. Wenn ich bisher keinen Standard-Editor für die Bearbeitung der Crontab angegeben habe, werde ich nun danach gefragt. Hier wähle ich mit Enter den vorgeschlagenen Editor nano aus:

no crontab for andreas - using an empty one

Select an editor.  To change later, run 'select-editor'.
  1. /bin/nano        <---- easiest
  2. /usr/bin/vim.basic
  3. /usr/bin/vim.tiny
  4. /bin/ed

Choose 1-4 [1]: 1
            
An das Ende dieser Datei trage ich dann ein:

0 7 * * * docker exec -it paperless-ngx-webserver-1 document_exporter /usr/src/paperless/export
            
Am Anfang der Zeile wird definiert, wann der Cronjob ausgeführt werden soll. An erster Stelle werden die Minuten, dann die Stunden, dann der Tag des Monats (1-31), der Monat (1-12), der Wochentag (Sonntag = 0, Samstag = 6) angegeben. Darauf folgt das auszuführende Kommando. In diesem Fall wird jeden Morgen um 7:00 Uhr ein Export ausgeführt. Dabei werden vorhandene Dateien überschrieben, wenn im System eine neuere verfügbar ist. Es werden die Ursprungsdateien, die PDF-Repräsentationen, Web-Vorschauen und in einer Manifest-Datei alle Metadaten und Datenbankinhalte gespeichert. Den Inhalt des Export-Ordner muss ich nun natürlich sichern.



Duplicati installieren

Auch Duplicati installiere ich per Docker und Docker-Compose. Dazu erstelle ich ein Verzeichnis duplicati und wechsele in selbiges:


mkdir ~/duplicati && cd ~/duplicati
            
Darin erstelle ich dann die Datei docker-compose.yml

nano docker-compose.yml
            
mit folgendem Inhalt:

services:
    duplicati:
      image: lscr.io/linuxserver/duplicati:latest
      container_name: duplicati
      environment:
        - PUID=0
        - PGID=0
        - TZ=Europe/Berlin
        - SETTINGS_ENCRYPTION_KEY=strenggeheimerundkomplexerkey
        - DUPLICATI__WEBSERVICE_PASSWORD=strenggeheimesundkomplexespasswort
      volumes:
        - ./config:/config
        - /home/andreas/paperless-ngx/export:/paperless-export
        - /home/andreas/sicherungsverzeichnis1:/source1
      ports:
        - 10.11.0.1:8200:8200
      restart: unless-stopped
            
Der Encryption-Key wird als Teil der Verschlüsselung der Sicherungsdaten verwendet und das Webservice-Passwort wird benötigt, um sich an der Weboberfläche anzumelden (wer hätte das gedacht?).

Hier habe ich das Exportverzeichnis von Paperless-NGX als Volume gemountet und zusätzlich ein Verzeichnis namens "sicherungsverzeichnis1". Hier kann man beliebige Verzeichnisse angeben, die dann später von der Weboberfläche aus und für den Dockercontainer erreichbar sind. Das zweite Verzeichnis habe ich angegeben, damit ich später aus duplicati heraus einzelne Files an anderer Stelle zurücksichern kann, als dem Ursprungspfad. Mit der Angabe unter "ports" binde ich die Duplicati-Weboberfläche vor allem an die VPN-Adresse.

Duplicati starte ich nun mit

docker compose up -d
            
und kann mich dann an der Weboberfläche anmelden. Diese rufe ich mit 10.11.0.1:8200 auf. Sie ist nur über das VPN erreichbar.



Sicherung in Duplicati einrichten

Wenn ich mich an der Weboberfläche von Duplicati anmelde, dann moniert mein Browser natürlich die unsichere Verbindung. Das kann ich aber getrost ignorieren, weil ich mich ja innerhalb meines VPN bewege. Der Dialog für das Hinzufügen eines neuen Backup-Jobs ist größtenteils selbst erklärend.

Screenshot: Allgemeine Einstellungen
Abb. 2: Screenshot: Allgemeine Einstellungen
Unter den allgemeinen Sicherungseinstellungen vergebe ich zunächst einen Namen für die Sicherung. Der darf ruhig sprechend sein. Eine Sicherungsbeschreibung kann ich auch noch eingeben. Das bewirkt aber nichts. Interessanter ist da schon die Verschlüsselung. Ich kann hier wählen zwischen AES-256, was bereits integriert ist oder GNU Privacy Guard, was auf den allermeisten Linux-Systemen bereits installiert ist. Ich entscheide mich für die integrierte Variante. Außerdem muss ich mir ein Passwort ausdenken. Hier werden auch schwache Passworte akzeptiert, aber man sollte sich schon etwas Mühe geben.

Bei den Backup-Destinationen wähle ich natürlich SSH aus. Auf der nächsten Seite muss ich dann die Zugangsdaten für den externen Speicher eingeben. Bei einer Storage-Box von Hetzner ist das z.B. accountname.your-storagebox.de, ansonsten könnte das eine IP-Adresse eines entfernten Servers oder auch eine Domain sein. Der Standard-Port für SSH ist 22, bei der Storage-Box von Hetzner ist es 23. Auf der Firewall habe ich ausgehenden Traffik nicht begrenzt. Darum brauche ich mir keine Gedanken zu machen, ob ich einen Port auf der Firewall öffnen muss. Ordnerpfad ist natürlich der Pfad, in den auf dem entfernten System gesichert werden soll. Den hier angegebenen Ordner muss es im Ziel schon geben, sonst läuft die Sicherung auf einen Fehler. Das war in früheren Versionen von Duplicati noch anders. Da wurde man dann gefragt, ob der Ordner angelegt werden soll.. Username ist natürlich der Username in dessen Berechtigungskontext auf dem Zielsystem die Sicherungen abgelegt werden sollen.

Die Autentifizierungsmethode wählt man mit "Erweiterte Option hinzufügen" aus. Hier reicht eine Authentifizierung mit Passwort aus. Duplicati unterstützt auch SSH-Key-Authentifizierung. Allerdings werden nur Schlüssel im PEM-Format erlaubt und die Einbindung ist auch recht kompliziert. Da die Passwortübertragung verschlüsselt erfolgt, mache ich es mir einfach und wähle Passwortauthentifizierung aus. Neben dem Passwort prüft Duplicati den Fingerprint der Gegenseite. Den erwartet es als MD5-Hash. Da es je nach Hosting-Anbieter nicht ganz einfach ist, den Fingerprint der Gegenseite im MD5-Format zu bekommen, wende ich einen kleinen Trick an. Ich füge zunächst unter "Erweiterte Option hinzufügen" "SSH Fingerprint" hinzu und schreibe in das entsprechende Feld irgend etwas hinein. Später, wenn ich mit diesen unsinnigen Daten zu sichern versuche, erhalte ich eine Fehlermeldung, in der mir der korrekte Fingerprint angezeigt wird. Dazu komme ich später noch. Ich klicke also nicht auf "Test destination", sondern erst einmal auf Weiter. Hier wähle ich die zu sichernden Daten aus. In meinem Fall habe ich ja den Export-Ordner von Paperless-NGX in das Verzeichnis "source" gemountet und darum wähle ich das natürlich aus.

Die Parameter auf der Seite Zeitplan sind selbsterklärend. Ich stelle meine Sicherungen auf z.B. 8:00 Uhr morgens ein, weil der Export ja jeden Morgen um 7:00 Uhr läuft. Auf der Seite Optionen brauche ich bei der Zielvolumengröße nichts zu ändern. Interessanter sind die Retention-Regeln, die ich unter "Sicherungsaufbewahrung" einstellen kann. Alle Sicherungen immer aufzubewahren ist natürlich nicht sinnvoll.
Screenshot: Retention-Regel
Abb. 2: Screenshot: Retention-Regel
Ich könnte auswählen, dass alle Sicherungen gelöscht werden, die älter sind als n Tage, Wochen, Monate oder Jahre. Ich könnte aber auch vorgeben, dass eine bestimmte Anzahl von Sicherungen aufbewahrt werden und alle anderen (älteren) gelöscht werden. Wenn ich "intelligente Sicherungsaufbewahrung" auswähle, dann werden die Sicherungen der letzten 7 Tage, jeweils eine der letzten 4 Wochen und jeweils eine der letzten 12 Monate aufbewahrt. Alternativ kann ich eine individuelle Aufbewahrungsregel definieren. Die Syntax wird hier ganz gut erklärt. Mir reicht hier für's Erste die intelligente Aufbewahrungsregel. Unter "Optionen für Profis" könnte ich noch sehr viele erweiterte Optionen hinzufügen. Das ist aber wirklich etwas für Profis, darum lasse ich einstweilen die Finger davon. Wenn ich auf "Senden" klicke, dann wird der Sicherungsjob gespeichert. Die Sicherung würde dann um 6:00 Uhr starten und zunächst auf einen Fehler laufen, wegen des falschen SSH-Fingerprints. Das warte ich natürlich nicht ab, sondern klicke auf der Startseite auf "Start". Nun läuft der Job auf den erwarteten Fehler. In der Fehlermeldung wird mir auch ein Link auf das Protokoll angezeigt, den ich anklicke. Dort wird ein Fehler ausgewiesen, der auch den erwarteten SSH-Fingerprint beginnend mit "ssh-ed25519 256" enthält. Den kopiere ich einfach aus der Fehlermeldung heraus. Ich gehe erneut in die Bearbeitung des Sicherungsjobs und trage den korrekten Schlüssel auf der Seite mit der Sicherungszieldefinition ein. Wenn ich nun auf "Test destination" klicke, wird mir angezeigt, dass die Verbindung funktioniert. Ich kann die Bearbeitung der Sicherung abschließen und die erste Sicherung anstoßen. Das sollte nun funktionieren.



Fazit

Nun habe ich also einen Server bei einem Hoster, auf dem Paperless-NGX läuft. Die Weboberfläche von Paperless-NGX ist aus dem Internet über eine (Sub-)Domain erreichbar und mit einem gültigen SSL-Zertifikat von Let's Encrypt abgesichert. Die SSL-Absicherung und die Verbindung mit dem Zertifikat werden durch einen Reverse-Proxy besorgt. Paperless-NGX ist ausschließlich über den Reverse-Proxy ansprechbar. Somit kann die Absicherung mit SSL niemand umgehen. Die Remote-Konsole des Servers sowie die Weboberflächen des Reverse Proxys und von Duplicati sind ausschließlich über das VPN erreichbar. Die Firewall blockiert unerlaubte Zugriffe aus dem Netz und fail2ban sperrt IP-Adressen aus, wenn sie mehr als dreimal versuchen, mit falschen Zugangsdaten auf die Remoteshell oder Paperless-NGX zuzugreifen. Die Datensicherung erfolgt auf ein System bei einem anderen Hoster, also auch in einem anderen Rechenzentrum. Die Sicherungen finden verschlüsselt, komprimiert und inkrementell statt. Auch der Übertragungsweg der Sicherungsdaten ist verschlüsselt.

Damit habe ich ein System geschaffen, das eine sichere und zuverlässige Verarbeitung meiner Dokumente ermöglicht, ohne dass ich zuhause Server dafür betreiben muss. Die laufenden Kosten für die beiden Betreiber belaufen sich auf knapp 10,00 Euro im Monat. Die in der Einleitung genannten Ziele sind alle erreicht.