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:

Administration overview in OPNSense

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:

Upstream-Server Konfiguration

Anschließend habe ich folgenden Upstream eingerichtet:

Upstream Konfigurieren

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:

Security Headers

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:

Rewrite Regel

Erstellung der Location

Nun habe ich eine Location für / erstellt:

Location Konfiguration

Erstellung des HTTP-Servers

Zuletzt war nun alles vorbereitet um den eigentlichen HTTP-Server unter HTTP(S) > HTTP-Server anzulegen:

HTTP Server Konfiguration

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:

Bot Protection 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:

Firewall Regeln

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.