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:
Die Lösung 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.

Man nehme:
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.
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. 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 er zunächst angelegt werden. 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
apt install sudo
und den Benutzer der Gruppe sudo hinzufügen, damit er das auch darf
usermod -aG sudo {Benutzername}
Danach kann man sich ab- und mit dem neuen Useraccount wieder anmelden.
Zunächst installiere ich 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.
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 10.11.0.0/24 to any port 22 proto tcp
sudo ufw allow from 10.11.0.0/24 to any port 8080 proto tcp
sudo ufw allow from 10.11.0.0/24 to any port 8200 proto tcp
sudo ufw allow from 10.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 10.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.
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
#backend = systemd
# Wir suchen direkt im systemd Journal
#journalmatch = _COMM=kernel
# Jetzt verwenden wir die Datei anstelle des Journals
logpath = /var/log/ufw.log
# Der Filter sucht nach dem UFW-Block-Muster in den Log-Zeilen
# Der Ausdruck *<ip>* extrahiert die Quell-IP (SRC=...).
# Die RegEx sollte das korrekte UFW-Muster erfassen.
filter = ufw-block
# Aktionsdefinition: UFW (iptables) zum Blockieren verwenden
banaction = ufw
#logpath = /var/log/ufw.log ; (Platzhalter, da journalmatch verwendet wird)
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=<HOST>
ignoreregex =
und 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.
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
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 ü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 wg1011.conf, weil ich das Subnetz 10.11.0.0/24 nutzen möchte.
nano wg1011.conf
mit folgendem Inhalt:
[Interface] Address = 10.11.0.1/32 SaveConfig = true PostUp = iptables -A FORWARD -i wg1011 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens18 -j MASQUERADE PostDown = iptables -D FORWARD -i wg1011 -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 10.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 wg1011
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 installiere ich auch dort, wenn nicht bereits geschehen Wireguard mit
sudo apt install wireguard
Ich wechsele in den Root-Modus mit
sudo su
und in den Wireguard-Ordner mit
cd /etc/wireguard
Dann erstelle ich ein Schlüsselpaar mit
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/wg1011.conf
mit folgendem Inhalt:
[Interface]
PrivateKey = {privater Schlüssel des Clients}
Address = 10.11.0.36/32
[Peer]
PublicKey = {öffentlicher Schlüssel des Servers}
Endpoint = {IP-Adresse des Servers}:51820
AllowedIPs = 10.11.0.0/24
PersistentKeepalive = 25
Den neue Tunnel aktiviere ich mit
wg-quick up wg1011
und mache ihn bootfest mit
systemctl enable wg-quick@wg1011
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 remote auf dem Server an und setze den Befehl
sudo wg set wg1011 peer {öffentlicher Schlüssel des Clients} allowed-ips 10.11.0.36/32
ab. Damit das auch wirksam wird, muss ich den VPN-Tunnel auf dem Server zunächst beenden und wieder starten mit
sudo wg-quick down wg1011 && sudo wg-quick up wg1011
Das teste ich mit
ping 10.11.0.36
auf dem Server und/oder mit
ping 10.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
an und identifiziere die Freigabe für Port 22 für alle. Das ist in meinem Fall die Nummer 2. Ich lösche sie 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 10.11.0.1
kann ich mich aber sofort wieder anmelden.
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.
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.
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-ngxcd 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(`paperlesstest02.kernke.koeln`)" # 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(`paperlesstest02.kernke.koeln`)"
- "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.
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 `<HOST>`\.
ignoreregex =
Diese Filterdefinition erkennt nun das Vorkommen der Zeichenkette "Login failed for user... from private IP..." und ermittelt in der Variable <HOST> 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 <ip> --dport 443 -j REJECT --reject-with icmp-port-unreachable
actionunban = iptables -D INPUT -p tcp -s <ip> --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.
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 andreas
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/andreas/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:
//10.11.0.1/scans /home/andreas/scans cifs _netdev,x-systemd.requires=wg-quick@wg1011.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=andreas
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/andreas/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.
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.
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 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.
Auch Duplicati installiere ich per Docker und Docker-Compose. Dann erstelle ich ein Verzeichnis duplicati:
mkdir 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.
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.

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.

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.
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.