Nextcloud hinter einer OPNSense Firewall mit Nginx Reverse-Proxy
Heute dachte ich mir, ich möchte gerne meine Nextcloud auf meinem Heimserver betreiben. Da ich bei mir eine OpnSense als Firewall verwende und diese ein Plugin für einen Nginx Reverse-Proxy anbietet, bot sich an, dieses für diesen Zweck zu verwenden. Der Vorteil ist, ich kann SSL auf der OpnSense Terminieren, so dass ich den dort sowieso bereits eingerichteten Let’s Encrypt Client verwenden kann. Da die Konfiguration nicht ganz trivial war, zeige ich hier einmal, wie ich alles eingerichtet habe.
Einrichtung der Nextcloud auf dem Server
Um Nextcloud auf meinem Server einzurichten, habe ich das offizelle Docker-Image unter Verwendung der dort verlinkten Beispiel-Konfigurationen verwendet. Da Redhat/Fedora inzwischen auf Podman als Alternative zu Docker setzt, habe ich auch direkt Podman und Podman Compose installiert. Dies ist (mehr oder weniger) ein 1 zu 1 Ersatz für Docker, was eine Migration sehr leicht gestaltet.
Um später die Compose-Dateien aller Services an einem Ort zu haben, habe den Ordner /etc/containers/compose.d/nextcloud
angelegt. Sämtliche Konfigurationsdateien in den nächsten Schritten liegen also in diesem Ordner.
Nun habe ich die Compose-Datei „container-compose.yml“ angelegt:
version: '3'
volumes:
data:
db:
services:
db:
image: mariadb
restart: always
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
volumes:
- db:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=CHANGEME
- MYSQL_PASSWORD=CHANGEME
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
redis:
image: redis:alpine
restart: always
app:
build: /etc/containers/compose.d/nextcloud/app
restart: always
volumes:
- data:/var/www/html
environment:
- MYSQL_PASSWORD=CHANGEME
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
- MYSQL_HOST=db
- REDIS_HOST=redis
- NEXTCLOUD_TRUSTED_DOMAINS=cloud.example.com
- OVERWRITEHOST=cloud.example.org
- OVERWRITEPROTOCOL=https
links:
- db
- redis
depends_on:
- db
- redis
web:
build: /etc/containers/compose.d/nextcloud/web
restart: always
ports:
- 30000:8443
volumes:
- data:/var/www/html:ro
links:
- app
depends_on:
- app
cron:
image: nextcloud:fpm-alpine
restart: always
volumes:
- data:/var/www/html
entrypoint: /cron.sh
depends_on:
- db
- redis
Dies baut die Images für app und web und verwendet für mariadb, redis und cron die offiziellen Images aus dem Docker-Hub. Dementsprechend habe ich für app und web Unterordner angelegt, auf die ich nun eingehe:
app/Dockerfile:
FROM nextcloud:fpm-alpine
COPY www.conf /usr/local/etc/php-fpm.d/www.conf
app/www.conf:
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 120
pm.start_servers = 12
pm.min_spare_servers = 6
pm.max_spare_servers = 18
Dies habe ich dafür angelegt, damit ich die Werte unter pm.max_children, pm.start_servers, usw. anpassen kann, die in der Original-Datei sehr niedrig angesetzt sind.
web/Dockerfile:
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY cert.pem /etc/nginx/cert.pem
COPY key.pem /etc/nginx/key.pem
web/nginx.conf:
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
upstream php-handler {
server app:9000;
}
server {
listen 8443 ssl http2;
ssl_certificate /etc/nginx/cert.pem;
ssl_certificate_key /etc/nginx/key.pem;
# Add headers to serve security related headers
# Before enabling Strict-Transport-Security headers please read into this
# topic first.
#add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;
#
# WARNING: Only add the preload option once you read about
# the consequences in https://hstspreload.org/. This option
# will add the domain to a hardcoded list that is shipped
# in all major browsers and getting removed from this list
# could take several months.
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Remove X-Powered-By, which is an information leak
fastcgi_hide_header X-Powered-By;
# Path to the root of your installation
root /var/www/html;
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# The following 2 rules are only needed for the user_webfinger app.
# Uncomment it if you're planning to use this app.
#rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
#rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
# The following rule is only needed for the Social app.
# Uncomment it if you're planning to use this app.
#rewrite ^/.well-known/webfinger /public.php?service=webfinger last;
location = /.well-known/carddav {
return 301 $scheme://$host:$server_port/remote.php/dav;
}
location = /.well-known/caldav {
return 301 $scheme://$host:$server_port/remote.php/dav;
}
# set max upload size
client_max_body_size 10G;
fastcgi_buffers 64 4K;
# Enable gzip but do not remove ETag headers
gzip on;
gzip_vary on;
gzip_comp_level 4;
gzip_min_length 256;
gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
# Uncomment if your server is build with the ngx_pagespeed module
# This module is currently not supported.
#pagespeed off;
location / {
rewrite ^ /index.php;
}
location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ {
deny all;
}
location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) {
deny all;
}
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
# fastcgi_param HTTPS on;
# Avoid sending the security headers twice
fastcgi_param modHeadersAvailable true;
# Enable pretty urls
fastcgi_param front_controller_active true;
fastcgi_pass php-handler;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
}
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
try_files $uri/ =404;
index index.php;
}
# Adding the cache control header for js, css and map files
# Make sure it is BELOW the PHP block
location ~ \.(?:css|js|woff2?|svg|gif|map)$ {
try_files $uri /index.php$request_uri;
add_header Cache-Control "public, max-age=15778463";
# Add headers to serve security related headers (It is intended to
# have those duplicated to the ones above)
# Before enabling Strict-Transport-Security headers please read into
# this topic first.
#add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;
#
# WARNING: Only add the preload option once you read about
# the consequences in https://hstspreload.org/. This option
# will add the domain to a hardcoded list that is shipped
# in all major browsers and getting removed from this list
# could take several months.
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Optional: Don't log access to assets
access_log off;
}
location ~ \.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$ {
try_files $uri /index.php$request_uri;
# Optional: Don't log access to other assets
access_log off;
}
}
}
Dies steht so nahezu identisch in den offiziellen Beispielen des Nextcloud-Images, außer dass ich auch intern SSL verwenden möchte. Dementsprechend habe ich unter web/cert.pem
und web/key.pem
noch SSL-Zertifikate abgelegt, die ich über die OPNSense erstellt habe.
Erstellung einer internen CA und eines internen Server-Zertifikats
In der OPNSense Weboberfläche habe ich unter System > Trust > Authorities
eine neue interne CA angelegt. Dies ist relativ selbsterklärend daher werde ich nicht näher darauf eingehen. Anschließend habe ich für meinen Server unter System > Trust > Certificates
ein Zertifikat erstellt, welches mit der zuvor erstellten Authority signiert wurde. Zertifikat und Schlüssel können anschließend einfach über die Weboberfläche exportiert werden und entsprechend in die o.g. stellen auf dem Server eingefügt werden.
Erstellen eines podman Benutzers
Da ich die Container rootless betreiben wollte, benötigte ich noch einen Benutzer, unter dem die Container laufen sollen. Ich habe mich dazu entschieden, diesen einfach podman zu nennen:
groupadd podman
useradd -m -g podman -s /bin/bash -d `/mnt/pool/services/podman` podman
Wobei mein Festplattenraid unter /mnt/pool
eingehangen ist und ich deshalb auch die Container dort hinlegen wollte.
Außerdem musste die Anzahl der User-Namespaces noch erhöht werden:
echo "user.max_user_namespaces=28633" > /etc/sysctl.d/userns.conf
sysctl -p /etc/sysctl.d/userns.conf
Systemd zum Starten von Nextcloud verwenden
Nun war Nextcloud bereit, gestartet zu werden. Es fehlte nur noch eine Systemd-Unit, damit dies auch beim Reboot des Servers automatisch funktioniert. Dazu habe ich folgende Unitfile unter /etc/systemd/system/compose@.service
angelegt:
[Unit]
Description=%i service with podman-compose
[Service]
Restart=always
TimeoutStartSec=1200
User=podman
WorkingDirectory=/etc/containers/compose.d/%i
ExecStartPre=/usr/bin/podman-compose down
ExecStartPre=/usr/bin/podman-compose pull
ExecStart=/usr/bin/podman-compose up
ExecStop=/usr/bin/podman-compose down
[Install]
WantedBy=multi-user.target
Die Unitfile bedient sich der Interface-Funktion von Systemd, bei der %i
gegen die Zeichenkette ersetzt wird, die hinter dem @
des Services angegeben wird. Die Nextcloud kann nun also folgendermaßen gestartet und aktiviert werden:
systemctl enable compose@nextcloud.service
systemctl start compose@nextcloud.service
Damit war die Einrichtung des Servers soweit abgeschlossen und nun konnte der Nginx Reverse-Proxy auf der OPNSense eingerichtet werden.
Einrichtung der OPNSense
Damit Nginx die Ports 80 und 443 nutzen kann, musste ich als erstes den Webkonfigurator auf einen anderen Port einstellen. Diese Einstellung ist in der Weboberfläche unter System > Settings > Administration zu finden:
Hier habe ich den TCP-Port auf 8443 geändert und den HTTP Redirect deaktiviert. Es zeigte sich, dass daraufhin ein Reboot der Firewall nötig ist, da die Ports ansonsten nicht freigegeben wurden.
Nun habe ich das Nginx Plugin unter System > Firmware > Plugins
installiert.
Nginx Einrichtung
Unter Services erschien nun der neue Service Nginx, auf dessen Konfiguration ich nun eingehe.
Upstream Server und Upstream Konfiguration
Im Dropdown-Menü Upstream ließen sich nun Upstream-Server und Upstreams konfigurieren. Als erstes habe ich den Upstream-Server folgendermaßen konfiguriert:
Anschließend habe ich folgenden Upstream eingerichtet:
Hier musste ich unter Trusted Certificate die CA wählen, die ich zum Signieren des Server-Zertifikats verwendet habe.
Security Header konfigurieren
Nun bin ich unter HTTP(S) > Security Headers gegangen und habe dort für die Nextcloud Security Headers erstellt, die dem entsprechen, was in der Nextcloud-Dokumentation angegeben ist:
Erstellung einer Rewrite-Regel
Nextcloud braucht standardmäßig Rewrite-Regeln für Cal- und Carddav. Deshalb habe ich dafür unter HTTP(S) > URL Rewriting eine Regel angelegt:
Erstellung der Location
Nun habe ich eine Location für / erstellt:
Erstellung des HTTP-Servers
Zuletzt war nun alles vorbereitet um den eigentlichen HTTP-Server unter HTTP(S) > HTTP-Server
anzulegen:
Ein entsprechendes Let’s Encrypt Zertifikat hatte ich zuvor bereits über das Let’s Encrypt Plugin erzeugt, welches ich sowieso bereits installiert und konfiguriert hatte. Dazu gibt es im Internet bereits brauchbare Anleitungen, weshalb ich darauf nicht weiter eingehe.
Wichtig: Mir ist später aufgefallen, dass mein Handy, das ich via DAVx mit Card- und Caldav verbinden wollte, ständig auf der Banlist aufgetaucht ist. Scheinbar wird DAVx durch die Bot Protection als Bot erkannt. Diese ließ sich in der Server Konfiguration in den Advanced Options deaktivieren:
Firewall Regeln erstellen
Damit der Nginx nun auch aus dem Internet erreichbar ist, müssen natürlich noch Firewall-Regeln für HTTP und HTTPS unter Firewall > Rules > WAN
angelegt werden:
Schlusswort
Nun war alles soweit fertig konfiguriert. Unter General musste ich nun noch einmal auf Apply klicken und dann den Nginx-Service restarten und dann konnte ich auf meine Nextcloud auch von außen zugreifen.
Was in diesem Artikel nicht erwähnt wurde:
- Ich habe zuvor für die Nextcloud einen DNS-Eintrag als CNAME auf die Adresse meiner Firewall angelegt.
- Möchte man das OPNSense Webinterface weiterhin unter Port 443 erreichen, ist dies möglich, indem man im Nginx eine Proxy-Konfiguration für das Webinterface anlegt, die entsprechend auf
::1
zeigt. Hier ist nur zu beachten, dass dann auf jeden Fall darauf geachtet werden muss, ACLs zu erstellen, die den Zugriff auf das Webinterface auf den lokalen Adressraum einschränken.
Ich hoffe euch hilft der Artikel. Mir wird er auf jeden Fall helfen, falls ich dies irgendwann mal erneut aufsetzen muss.