rusty-snake's Tips & Tricks

Collection of useful commands and configs.

View on GitHub
24 September 2023

Local Nextcloud with podman

by rusty-snake

Architecture

The overall architecture of this first concept contists of

  1. A caddy webserver on the host started by a hardened systemd service. The rule of this caddy instance is to proper terminate the https connection and forward everything to the Nextcloud pod. This is a grown construct to workaround some problems and might get/should be replaced with something better in the future.
  2. A caddy webserver does static file servering and acts as the reverse proxy to php-fpm.
  3. A Nextcloud php-fpm container.
  4. A Nextcloud container for cron.
flowchart LR
    caddy_host["Caddy (Host)"]---caddy
    subgraph Podman
        caddy("Caddy")-- nextcloud-fastcgi.network ---nextcloud
        nextcloud("Nextcloud")
        nextcloud_cron("Nextcloud (cron)")
    end

Caddy (Host)

Install caddy and harden caddy.service.

Configure caddy

/etc/caddy/Caddyfile

# The Caddyfile is an easy way to configure your Caddy web server.
#
# https://caddyserver.com/docs/caddyfile

# Global Options
# https://caddyserver.com/docs/caddyfile/options
{
	# Enable debug mode
	#debug

	# Disable the admin endpoint
	admin off

	# Disable persisted config. Admin API is disabled anyway.
	persist_config off

	log {
		format filter {
			wrap console
			fields {
				request>headers>Requesttoken replace "*** REDACTED ***"
			}
		}
	}

	local_certs
	skip_install_trust
	ocsp_stapling off
}

# As an alternative to editing the above site block, you can add your own site
# block files in the Caddyfile.d directory, and they will be included as long
# as they use the .caddyfile extension.
import Caddyfile.d/*.caddyfile

/etc/caddy/Caddyfile.d/nextcloud.caddyfile

# vim: set ft=caddyfile:

192.168.0.3:7777 {
	root * /tmp

	# TLS Options
	tls {
		# Increase minimal TLS version to 1.3.
		protocols tls1.3

		issuer internal {
			lifetime 365d
			sign_with_root
		}
	}

	# Enable HSTS with a duration of 1 year.
	header Strict-Transport-Security "max-age=31536000; includeSubDomains"

	# Restrict allowed ip ranges.
	@disallowed-ips not remote_ip 127.0.0.1 192.168.0.0/24
	abort @disallowed-ips

	@curl header User-Agent curl*
	respond @curl 403 {
		close
	}

	# Add a error page for errors generated by Caddy (e.g. 502 Bad Gateway).
	handle_errors {
		header Content-Type "text/html; charset=utf-8"
		respond "<h1>{err.status_code} {err.status_text}</h1>"
	}

	encode gzip
	reverse_proxy 127.0.0.1:7867
}

SELinux

Because you use SELinux, we have to adjust the policy of caddy (httpd_t) for our usecase.

Allow caddy (httpd_t) to bind to port 7777

semanage port -a -t http_port_t -p tcp 7777
semanage port -a -t http_port_t -p udp 7777

Allow caddy (httpd_t) to connect to port 7867

setsebool -P httpd_can_network_connect 1

TODO: Replace this boolean with an module allowing something like reverse_proxy_upstream_port_t and define port 7867 to be of this type.

Start caddy

systemctl enable --now caddy.service

Add caddys root certificate to the system trust store1

cp /var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt /etc/pki/ca-trust/source/anchors/caddy-root.pem
chmod 644 /etc/pki/ca-trust/source/anchors/caddy-root.pem
update-ca-trust extract

View certificate fingerprint

openssl x509 -noout -fingerprint -sha256 -inform pem -in /var/lib/caddy/.local/share/caddy/certificates/local/192.168.0.100/192.168.0.3.crt

Open port 7777 in your firewall

firewall-cmd --permanent --add-port=7777/tcp

Add new user for Nextcloud

useradd --comment Nextcloud --no-create-home --home-dir /var/lib/nextcloud --shell /sbin/nologin --system --add-subids-for-system --user-group nextcloud
mkdir /var/lib/nextcloud
cp /etc/skel/.bash* /var/lib/nextcloud
chown -R nextcloud:nextcloud /var/lib/nextcloud
chmod 0700 /var/lib/nextcloud
loginctl enable-linger nextcloud

tmux

To administrate the Nextcloud installation, we will run a tmux session in the background that you can attach to.

sudo -u nextcloud tmux attach-session -t nc-admin0

tmux.service

[Unit]
Description=tmux terminal multiplexer
Documentation=man:tmux(1)

[Service]
Type=forking
ExecStart=/usr/bin/tmux start-server; new-session -s nc-admin0 -d
ExecStop=/usr/bin/tmux kill-server
Restart=always

[Install]
WantedBy=default.target
mkdir -p /var/lib/nextcloud/.config/systemd/user/default.target.wants
ln -s /var/lib/nextcloud/.config/systemd/user/tmux.service /var/lib/nextcloud/.config/systemd/user/default.target.wants/tmux.service

/var/lib/nextcloud/.tmux.conf:

# Required because we configured /sbin/nologin as login shell.
set-option -g default-shell "/bin/bash"

#
## Optional, customizable to meet your own needs
#

bind-key c new-window -c "#{pane_current_path}"
bind-key % split-window -h -c "#{pane_current_path}"
bind-key '"' split-window -v -c "#{pane_current_path}"

set-option -g escape-time 0

bind-key -n M-w select-pane -U
bind-key -n M-a select-pane -L
bind-key -n M-s select-pane -D
bind-key -n M-d select-pane -R

/var/lib/nextcloud/.bashrc additions:

alias systemctl="systemctl --user"

Installation

Volumes

mkdir -p /var/lib/nextcloud/{app,data,caddy-data}

Because the space on my /var was a bit low I moved the data directory to /home.

mkdir /home/.nextcloud-data
chown nextcloud:nextcloud /home/.nextcloud-data
chmod 0700 /home/.nextcloud-data
ln -s /home/.nextcloud-data/data data

Images

caddy-rootless.containerfile:

FROM caddy:2

RUN apk --no-cache upgrade \
	&& adduser --home /var/lib/caddy --gecos "Caddy web server" --shell /sbin/nologin --ingroup www-data --system --uid 443 caddy \
	&& chown -R caddy:www-data /config /data

USER caddy
podman pull docker.io/library/caddy:2 docker.io/library/nextcloud:28-fpm-alpine
buildah bud -f caddy-rootless.containerfile -t caddy-rootless:2

Networks

~/.config/containers/systemd/nextcloud-fastcgi.network:

[Network]
NetworkName=nextcloud-fastcgi
# TODO
#Internal=true
#Options=isolate

Containers

~/.config/containers/systemd/nextcloud-caddy.container
[Unit]
Description=Caddy web server for Nextcloud
Documentation=man:podman-systemd.unit(5) man:caddy(8)
Wants=network-online.target
After=network-online.target

[Container]
Image=localhost/caddy-rootless:2
Pull=never
# TODO
#AutoUpdate=local
ContainerName=nextcloud-caddy

Volume=/var/lib/nextcloud/Caddyfile:/etc/caddy/Caddyfile:Z,ro
Volume=/var/lib/nextcloud/caddy-data:/data:Z,U,noexec
Volume=/var/lib/nextcloud/app:/var/www/html:z,ro,noexec
Network=nextcloud-fastcgi.network
Network=slirp4netns:port_handler=slirp4netns
PublishPort=127.0.0.1:7867:80

DropCapability=ALL
AddCapability=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
#PidsLimit=
ReadOnly=true
PodmanArgs=--read-only-tmpfs=false
#ReadOnlyTmpfs=false
#SeccompProfile=
# hidepid
PodmanArgs=--security-opt=proc-opts=subset=pid

LogDriver=k8s-file
PodmanArgs=--log-opt=path=/var/lib/nextcloud/logs/caddy.log

# TODO
#HealthCmd=
#HealthOnFailure=stop
#HealthStartPeriod=1m
#HealthStartupCmd=
#HealthStartupTimeout=
#HealthTimeout=

[Service]
Restart=always

# TODO:
#ExecStop=
#ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%N.cid
#ExecStopPost=
#ExecStopPost=/usr/bin/podman rm -f -t 10 --cidfile=%t/%N.cid

[Install]
WantedBy=default.target
~/.config/containers/systemd/nextcloud-app.container
[Unit]
Description=Nextcloud
Documentation=man:podman-systemd.unit(5)
Wants=network-online.target
After=network-online.target

[Container]
Image=docker.io/library/nextcloud:28-fpm-alpine
Pull=never
# TODO
#AutoUpdate=local
ContainerName=nextcloud-app

Volume=/var/lib/nextcloud/app:/var/www/html:z,noexec
Volume=/var/lib/nextcloud/data:/srv/nextcloud/data:z,noexec
Network=nextcloud-fastcgi.network

DropCapability=ALL
AddCapability=CAP_SETUID CAP_SETGID CAP_CHOWN CAP_DAC_OVERRIDE CAP_FOWNER
NoNewPrivileges=true
ReadOnly=true
# Temporary to fix inconsistencies
PodmanArgs=--read-only-tmpfs=true
#SeccompProfile=
PodmanArgs=--security-opt=proc-opts=subset=pid

LogDriver=k8s-file
PodmanArgs=--log-opt=path=/var/lib/nextcloud/logs/nextcloud.log

# TODO
#HealthCmd=
#HealthOnFailure=stop
#HealthStartPeriod=1m
#HealthStartupCmd=
#HealthStartupTimeout=
#HealthTimeout=

[Service]
Restart=always

# TODO:
#ExecStop=
#ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%N.cid
#ExecStopPost=
#ExecStopPost=/usr/bin/podman rm -f -t 10 --cidfile=%t/%N.cid

[Install]
WantedBy=default.target
~/.config/containers/systemd/nextcloud-cron.container
[Unit]
Description=Nextcloud (cron)
Documentation=man:podman-systemd.unit(5)
Wants=network-online.target
After=network-online.target

[Container]
Image=docker.io/library/nextcloud:28-fpm-alpine
Pull=never
# TODO
#AutoUpdate=local
ContainerName=nextcloud-cron
PodmanArgs=--entrypoint=/cron.sh

Volume=/var/lib/nextcloud/app:/var/www/html:z,noexec
Volume=/var/lib/nextcloud/data:/srv/nextcloud/data:z,noexec
Network=nextcloud-fastcgi.network

DropCapability=ALL
AddCapability=CAP_SETUID CAP_SETGID
NoNewPrivileges=true
ReadOnly=true
# Temporary to fix inconsistencies
PodmanArgs=--read-only-tmpfs=true
#SeccompProfile=
PodmanArgs=--security-opt=proc-opts=subset=pid

LogDriver=k8s-file
PodmanArgs=--log-opt=path=/var/lib/nextcloud/logs/nextcloud-cron.log

# TODO
#HealthCmd=
#HealthOnFailure=stop
#HealthStartPeriod=1m
#HealthStartupCmd=
#HealthStartupTimeout=
#HealthTimeout=

[Service]
Restart=always

# TODO:
#ExecStop=
#ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%N.cid
#ExecStopPost=
#ExecStopPost=/usr/bin/podman rm -f -t 10 --cidfile=%t/%N.cid

[Install]
WantedBy=default.target
systemctl daemon-reload

caddy

Caddyfile (click to expand)
# Global Options
{
	# Uncomment the next line and restart/reload caddy to enable debug logging.
	#debug

	# Disable the admin endpoint.
	admin off

	# Disable persisted config. Filesystem is readonly and admin API is disabled anyway.
	persist_config off

	log {
		format filter {
			wrap console
			fields {
				request>headers>Requesttoken replace "*** REDACTED ***"
			}
		}
	}

	auto_https off
	local_certs
	skip_install_trust
	ocsp_stapling off

	servers {
		trusted_proxies static 10.0.2.2
	}
}

:80 {
	root * /var/www/html

	# Ensure security and privacy related headers are always set.
	# This is based on Nextcloud's .htaccess and OWASP recommendations.
	header {
		?Referrer-Policy no-referrer
		?X-Content-Type-Options nosniff
		?X-Frame-Options sameorigin
		?X-Permitted-Cross-Domain-Policies none
		?X-Robots-Tag "noindex, nofollow"
		# TODO: Better understand COxP stuff and Sec-Fetch-*.
		# https://github.com/nextcloud/server/issues/37154
		-X-XSS-Protection
	}

	# Cache-Control
	@static-resources path *.css *.js *.mjs *.svg *.gif *.png *.jpg *.ico *.wasm *.tflite *.map *.ogg *.flac
	handle @static-resources {
		map {query.v} {immutable} {
			"" ""
			default "immutable"
		}
		header Cache-Control "max-age=15778463, {immutable}"
	}
	@woff path *.woff *.woff2
	header @woff Cache-Control "max-age=604800"

	# /.well-known/ redirects
	redir /.well-known/carddav /remote.php/dav/ 301
	redir /.well-known/caldav /remote.php/dav/ 301
	rewrite /.well-known/acme-challenge /index.php?{query}
	rewrite /.well-known/pki-validation /index.php?{query}
	@well-known {
		path /.well-known/*
		not path /.well-known/carddav
		not path /.well-known/caldav
		not path /.well-known/acme-challenge
		not path /.well-known/pki-validation
	}
	redir @well-known /index.php{path}?{query} 301

	# Special handling for Microsoft DAV clients
	@ms-dav {
		header User-Agent DavClnt*
		path /
	}
	redir @ms-dav /remote.php/webdav 302

	# Add a error page for errors generated by Caddy (e.g. 502 Bad Gateway).
	handle_errors {
		header Content-Type "text/html; charset=utf-8"
		respond "<h1>{err.status_code} {err.status_text}</h1>"
	}

	php_fastcgi nextcloud-app:9000 {
		# Pretty URLs
		env front_controller_active true
		# Leaks php version
		header_down -X-Powered-By
	}
	@static-files path /core/* /apps/* /custom_apps/* /resources/* /themes/* /dist/* /index.html /robots.txt
	file_server @static-files
}


Nextcloud

Installation

podman exec nextcloud-app chown www-data:www-data /srv/nextcloud/data

Open https://192.168.0.3:7777/ in a browser and install Nextcloud. Use /srv/nextcloud/data as your data directory. Afterwards you must/can futher via occ.

podman exec -i -u www-data nextcloud-app sh <<EOF
php ./occ config:system:set trusted_proxies 0 --value=127.0.0.1

php ./occ config:system:set overwritehost --value=192.168.0.3:7777
php ./occ config:system:set overwriteprotocol --value=https
#php ./occ config:system:set overwritewebroot --value=/
php ./occ config:system:set overwrite.cli.url --value=https://192.168.0.3:7777/

php ./occ config:system:set trusted_domains 0 --value=127.0.0.1:7867
php ./occ config:system:set trusted_domains 1 --value=127.0.0.1:7777
php ./occ config:system:set trusted_domains 2 --value=192.168.0.3:7777

# # # # #

php ./occ config:system:set default_phone_region --value=DE

php ./occ config:system:set default_language --value=de_DE
php ./occ config:system:set default_locale --value=de_DE

php ./occ config:app:set richdocuments disable_certificate_verification --value=yes
php ./occ config:app:set password_policy enforceHaveIBeenPwned --value=0
php ./occ config:app:set privacy readableLocation --value=de
php ./occ config:app:set privacy fullDiskEncryptionEnabled --value=1
EOF

More options can be found at https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/config_sample_php_parameters.html.

The reverse proxy config can also be set from the environment:

  • TRUSTED_PROXIES=127.0.0.1
  • OVERWRITEHOST=192.168.0.3:7777
  • OVERWRITEPROTOCOL=https
  • OVERWRITEWEBROOT=/
  • OVERWRITECONDADDR=???
  • OVERWRITECLIURL=https://192.168.0.3:7777/

Backup

TODO: This is a grown mess and should be replaced with something better.

~/backup.sh:

#!/bin/bash
set -e

cd "${HOME:=$(getent passwd "$(id -u)" | cut -d: -f6)}"

NOW="$(date --utc --iso-8601=seconds)"


BACKUP_DIR="$HOME/backups"
BACKUP_FILE="$BACKUP_DIR/nextcloud-snapshot-$NOW.tar.zstd"
BACKUP_SOURCES=(./app/ ./Caddyfile ./caddy-data/)

mkdir -p "$BACKUP_DIR"
# TODO: --zstd vs. -I"zstd -T0" vs. -Izstdmt vs. -Ipzstd
podman unshare \
	tar -C"$HOME" --create --zstd --file "$BACKUP_FILE" "${BACKUP_SOURCES[@]}"


DATA_DIR="/home/.nextcloud-data/data"
SNAPSHOT_DIR="/home/.nextcloud-data/snapshots/$NOW"

mkdir -p "$SNAPSHOT_DIR"
podman unshare \
	cp --reflink=always -a "$DATA_DIR" "$SNAPSHOT_DIR"


ln -sTf "$(basename "$BACKUP_FILE")" "$BACKUP_DIR/latest"
ln -sTf "$(basename "$SNAPSHOT_DIR")" "$(dirname "$SNAPSHOT_DIR")/latest"

TODO: Are permissions, SELinux labels, timestamps, … backed up correctly?

nextcloud-backup.service:

[Unit]
Description=Backup NextCloud
Before=nextcloud-caddy.service nextcloud-app.service nextcloud-cron.service
Conflicts=nextcloud-caddy.service nextcloud-app.service nextcloud-cron.service
RefuseManualStart=true

[Service]
Type=oneshot
ExecStart=/var/lib/nextcloud/backup.sh

[Install]
RequiredBy=nextcloud-caddy.service nextcloud-app.service nextcloud-cron.service

What’s next?

Maintainance

Performance

Hardening

Features


tags: