Looks like everything is made of clouds and farts now, so we cannot be out of it we need to keep up on the trend. I maintain the nextcloud port since ever, and I am using since then, to be honest it's pretty cool and handy, despite all the php slowness and other flavors the system has and the OpenBSD's issues as client and server (in comparation with loonix and others), it works really really well in every way I use it; which is as "multimedia cloud", "CalDAV/CardDAV" server for my android phone, "Password Manager" and "Backups" in general.
As I explained before in other articles, I like to keep using base system as much as I can, I like minimal stuff, I like minimalism and OpenBSD is very minimal and full of tools for the task. The setup will be with relayd(8) in front, and httpd(8) behind, the only package we need it's nextcloud, the rest will come along with it, so we do:
$ doas pkg_add nextcloud
quirks-6.101 signed on 2023-02-14T11:05:49Z
quirks-6.101: ok
Ambiguous: choose package for nextcloud
a 0: <None>
1: nextcloud-23.0.12p0
2: nextcloud-24.0.9p0
3: nextcloud-25.0.3p0
Your choice: 3
---
PORTS MAGIC
---
nextcloud-25.0.3p0: ok
Running tags: ok
The following new rcscripts were installed: /etc/rc.d/php81_fpm
See rcctl(8) for details.
New and changed readme(s):
/usr/local/share/doc/pkg-readmes/femail-chroot
/usr/local/share/doc/pkg-readmes/glib2
/usr/local/share/doc/pkg-readmes/nextcloud
/usr/local/share/doc/pkg-readmes/php-8.1
I recommend if you are using -current to use the higher one, it will be the one keeping get upgrades and changing to the new major one in the port and probably the one getting more updates from upstream. As always I will not explain you in details what it's already in the README, which btw, you should know that you can find as the others here: /usr/local/share/doc/pkg-readmes/. If you read the file, you will see that you have an example file for httpd(8), and I will show you my version of it, which not differ much from the one in the README file:
$ doas cat /etc/httpd.conf
server "cloud.x61.sh" {
listen on * tls port 443
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
root "/nextcloud"
hsts max-age 15768000
tls {
certificate "/etc/ssl/cloud.x61.sh.crt"
key "/etc/ssl/private/cloud.x61.sh.key"
}
# Set max upload size
connection max request body 537919488
connection max requests 1000
connection request timeout 3600
connection timeout 3600
tcp nodelay
gzip-static
# First deny access to the specified files
location "/db_structure.xml" { block }
location "/README" { block }
location "/config*" { block }
location "/build*" { block }
location "/tests*" { block }
location "/lib*" { block }
location "/3rdparty*" { block }
location "/templates*" { block }
location "/data*" { block }
location "/.ht*" { block }
location "/.user*" { block }
location "/autotest*" { block }
location "/occ*" { block }
location "/issue*" { block }
location "/indie*" { block }
location "/db_*" { block }
location "/console*" { block }
location "/core/*" {
gzip-static
pass
}
location "/apps/*" {
gzip-static
pass
}
location "/dist/*" {
gzip-static
pass
}
location "/.well-known/carddav" {
block return 301 "/remote.php/dav/"
}
location "/.well-known/caldav" {
block return 301 "/remote.php/dav/"
}
location match "/oc[ms]%-provider/*" {
directory index index.php
pass
}
location "/.well-known/webfinger" {
block return 301 "/index.php$REQUEST_URI"
}
location "/.well-known/nodeinfo" {
block return 301 "/index.php$REQUEST_URI"
}
location "/.well-known/host-meta" {
block return 301 "/public.php?service=host-meta"
}
location "/.well-known/host-meta.json" {
block return 301 "/public.php?service=host-meta-json"
}
location "/*.php*" {
fastcgi socket "/run/php-fpm.sock"
}
}
For the DB I use postgresql and redis as in-memory data store, I have no special setup for those more than the one on their READMEs, so I will move to the "/var/www/nextcloud/config/config.php" file of my nextcloud setup, I will breakdown the important parts that are not there by default:
...
'trusted_domains' =>
array (
0 => '10.10.0.9',
1 => 'cloud.x61.sh',
),
'trusted_proxies' =>
array (
0 => '10.10.0.1',
1 => '10.10.0.9',
2 => '127.0.0.1',
),
...
"trusted_domains" and "trusted_proxies", are just that, those domains, IPs or proxies that nextcloud will mark as safe for your instance, I have an internal IP for my local network and the public domain, as I have the proxy/ies in front of my nextcloud to try different things, you probably don't need this last one.
...
'overwriteprotocol' => 'https',
'memcache.local' => '\\OC\\Memcache\\Redis',
'memcache.locking' => '\\OC\\Memcache\\Redis',
'redis' =>
array (
'host' => '127.0.0.1',
'port' => 6379,
'timeout' => 1.5,
),
...
"overwriteprotocol" so let's make it always go over "https" specially if you have it facing the interwebz, at home in a local network, well, maybe you can use "http" but why?. The next options are for our cache (nextcloud cache), remember that we installed redis and you followed the README to make it work, so now that you have it listening on the default port in "127.0.0.1" you can add it to your conf.
...
'filesystem_check_changes' => 1,
...
"filesystem_check_changes" as config_sample_php_parameters explains this option specifies how often the local filesystem (the Nextcloud data/ directory, and NFS mounts in data/) is checked for changes made outside Nextcloud, I have it enable since my "files" in nextcloud reside in a crypto partition on another disk, you might not need it in a different setup but remember that if you "scp" or copy raw files from some drive to the "nextcloud-data" directory inside your user on the instance without uploaded them over the page, nextcloud won't see those files, so for those cases, you should enable "filesystem_check_changes".
...
'mail_from_address' => 'cloud',
'mail_smtpmode' => 'smtp',
'mail_smtpauthtype' => 'PLAIN',
'mail_domain' => 'cloud.x61.sh',
'mail_smtphost' => '127.0.0.1',
'mail_sendmailmode' => 'smtp',
...
These are pretty clear I believe, and yes, get emails with random information it's not cool, but I found those in this particular case important, since enabling the email notification you can get emails of modified files, login attempts, brute force attacks and many other things, I recommend to enable it, but it's not need it.
...
'app_install_overwrite' =>
array (
0 => 'calendar',
1 => 'bruteforcesettings',
2 => 'twofactor_yubikey',
3 => 'twofactor_u2f',
4 => 'side_menu',
5 => 'impersonate',
6 => 'duplicatefinder',
),
...
Well this is not something you should have, but those are some of the apps I use, in case you need it, a calendar (which is really useful for your android or ios phone to keep things in sync), bruteforcesettings blocks stupid attemps of login, twofactor_* are apps to use yubikey, solokeys or freeopt and have a 2fa, side_menu because I don't know who was the genious that created that awful default menu on top with all the titles together, impersonate which is useful to impersonate your mum user and re-arrange all the photos she has so then you can use the duplicatefinder to delete those 332 copies of the same photo she uploaded in different places.
...
'maintenance' => false,
);
...
This is an important one, if your upgrade or installation went wrong for X reason and you cannot access again your instance, you should probably look at this flag, fix the issue and turn it into "false" again, for more options you should check: config_sample_php_parameters.
So far at this point, if you have the services up (db, php and httpd(8)) your nextcloud should be running nicely, but what about those gzip on the httpd.conf(5)? what about all the Content Security Policy (CSP) we need to make it secure? what about let's encrypt certs?
Around OpenBSD 7.1, httpd(8) got the feature to serves pre-compressed files, so as you saw on the httpd.conf(5) above we have something like this:
...
location "/core/*" {
gzip-static
pass
}
...
Exactly, there is where nextcloud has a lot of huge .js and .css files, so why not "gzip" them to make them go faaaaaster? After every update I usually run something similar to this:
# find /var/www/nextcloud \( -name "*.js" -o -name "*.css" \) -size +100000c -exec gzip -f -k "{}" \;
If you check on those files now, you will see that are .gz and you will notice some speed up on your instance.
To keep nextcloud package up-to-date on OpenBSD we just need to run pkg_add -Vu on -release and pkg_add -Vu -Dsnap on -current, these will upgrade your Nextcloud's files, and then we need to upgrade the db and other things inside of it, you have 2 ways to do this; over the browser opening your domain it will guide you through the process or you can use the occ command. On OpenBSD we have chroot(2) and our httpd(8) is working under it, so to update the instance with the command line we should do something like:
$ doas -u www /usr/local/bin/php-8.1 /var/www/nextcloud/occ upgrade
This will manage the whole update and get the instance ready to use with the new version, if you are gziping *.js and *.css files you should re-run the find line to compress the new ones.
Let's put some other layer of security by adding some CSP headers, if you don't know what they are, follow the previous link, you will have plenty of details there. httpd(8) has no way to add headers itself so to give it some help I will put relayd(8) in front of it and play around with headers and the certificate for our nextcloud.
Yes, I specified my certificates in our httpd.conf(5) because after created them over relayd(8) I scp them to the cloud server, just in case, but you don't need it, relayd(8) will manage them for you, anyway it is the right conf for people without using relayd(8) on front.
relayd(8) as the man says: is a daemon to relay and dynamically redirect incoming connections to a target host. Its main purposes are to run as a load-balancer, application layer gateway, or transparent proxy, and that is what we gonna do, I will show my relayd.con(5) and explain a bit parts of it as example:
table <honk> { 10.10.0.7 }
table <matrix> { 10.10.0.8 }
table <certs> { 127.0.0.1 }
table <cloud> { 10.10.0.9 }
log state changes
log connection
http protocol "http" {
block
match header set "X-Client-IP" value "$REMOTE_ADDR:$REMOTE_PORT"
match header set "X-Forwarded-For" value "$REMOTE_ADDR"
match request header set "X-Forwarded-Port" value "$REMOTE_PORT"
match header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match header set "Keep-Alive" value "$TIMEOUT"
match query hash "sessid"
match response header remove "Server"
match response header set "X-Robots-Tag" value "none"
match response header set "Permissions-Policy" value "interest-cohort=()"
match response header set "Cache-Control" value "public, no-cache, must-revalidate, max-age=1814400"
match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
match response header set "X-Content-Type-Options" value "nosniff"
match response header set "X-XSS-Protection" value "1; mode=block"
match response header set "Referrer-Policy" value "no-referrer"
match response header set "Permissions-Policy" value "autoplay 'self'; geolocation 'none';payment 'none'"
match response header set "Content-Security-Policy" value "default-src https:; style-src 'self' 'unsafe-inline';img-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'self'; frame-ancestors 'self';base-uri 'self';connect-src 'self';media-src 'self'; manifest-src 'self';font-src 'self' data:;worker-src 'self' blob:;form-action 'self';"
match request quick path "/.well-known/acme-challenge/*" tag "certs"
pass request quick tagged "certs" forward to <certs>
match request quick header "Host" value "cloud.x61.sh" tag "cloud"
pass request quick tagged "cloud" forward to <cloud>
match request quick header "Host" value "honk.x61.sh" tag "honk"
pass request quick tagged "honk" forward to <honk>
match request quick header "Host" value "m.x61.sh" tag "matrix"
match request quick path "/_matrix/*" tag "matrix"
pass request quick tagged "matrix" forward to <matrix>
tls keypair "m.x61.sh"
tls keypair "honk.x61.sh"
tls keypair "cloud.x61.sh"
tls { no tlsv1.0, ciphers "HIGH" }
tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
tcp { nodelay, socket buffer 65536, backlog 100 }
}
relay "https" {
listen on egress port 443 tls
listen on egress port 80
protocol "http"
forward to <honk> port 31337 check tcp
forward to <matrix> port 8448 check tcp
forward to <certs> port 3080 check tcp
forward to <cloud> port 80 check tcp
}
On the top of the file you will see my tables with their IPs for each services behind my relayd, if you read my other articles you will remember those. We "log" connections and state changes, then the fun part starts where we define a "http protocol" called "http" (you can set any name here), to manage all our http connections and add what we need for our setup, in this case headers.
...
match header set "X-Client-IP" value "$REMOTE_ADDR:$REMOTE_PORT"
match header set "X-Forwarded-For" value "$REMOTE_ADDR"
match request header set "X-Forwarded-Port" value "$REMOTE_PORT"
match header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match header set "Keep-Alive" value "$TIMEOUT"
match query hash "sessid"
...
Here we tell to relayd (our reverse proxy) to use X-Forwarded-For and X-Forwarded-By in the connections it passes to httpd (honk, certs and cloud) without those you will not see any entries in your access log (and for this you will need "log style forwarded" in your httpd.conf from above), we also set the "Keep-Alive" header and "sessid".
...
match response header remove "Server"
match response header set "X-Robots-Tag" value "none"
match response header set "Permissions-Policy" value "interest-cohort=()"
match response header set "Cache-Control" value "public, no-cache, must-revalidate, max-age=1814400"
match response header set "Strict-Transport-Security" value "max-age=31536000; includeSubDomains; preload"
match response header set "X-Content-Type-Options" value "nosniff"
match response header set "X-XSS-Protection" value "1; mode=block"
match response header set "Referrer-Policy" value "no-referrer"
match response header set "Permissions-Policy" value "autoplay 'self'; geolocation 'none';payment 'none'"
match response header set "Content-Security-Policy" value "default-src https:; style-src 'self' 'unsafe-inline';img-src 'self' data: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'self'; frame-ancestors 'self';base-uri 'self';connect-src 'self';media-src 'self'; manifest-src 'self';font-src 'self' data:;worker-src 'self' blob:;form-action 'self';"
...
Big long block now, all the headers that make CSP happy. The syntax it's very simple, take a look at the manual, but basically we "match response" from the manual "the request, a client initiating a new connection to a server via the relay, and the response, the server accepting the connection." then we "set header" to our needs depending of what we want, in this case a bunch of them are for nextcloud in particular but the rest are for example to provide a layer to mitigate XSS attacks by restricting which scripts can be executed by the page. There are tons of different ones, and depends on what are you trying to do or which kind of software your website is using you will need to adjust accordingly to it, another good site to look at all the variations is this one.
...
match request quick path "/.well-known/acme-challenge/*" tag "certs"
pass request quick tagged "certs" forward to <certs>
...
I will explain this chunk first but the rest following are kinda the same. To get acme-client to find the "/.well-known/acme-challenge/" for all our sites behind, we need to tell relayd what to do when a http request ask for "/.well-known/acme-challenge/*". For this I have in my 127.0.0.1 machine an httpd running just to serve the certificate directories, the httpd.conf looks like this:
server "foobar.x61.sh" {
listen on 127.0.0.1 port 3080
alias "honk.x61.sh"
alias "m.x61.sh"
alias "cloud.x61.sh"
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
log { access "certs-httpd.log", style combined }
root "/htdocs"
}
The acme-client will try to renew or generate a certificate of oursites it will ask our DNS, it will hit our public IP through relayd it will match the "certs" rule and it will send the request to the local httpd above, the certificate will be generate or renew in the same server and ready to use by relayd (remember to reload it after it) by the next piece of code:
...
tls keypair "m.x61.sh"
tls keypair "honk.x61.sh"
tls keypair "cloud.x61.sh"
tls { no tlsv1.0, ciphers "HIGH" }
tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
...
At the bottom of it we disable a weak tls version and set most secure ciphers. The final part, the relay, which will forward traffic between a client and a target server.
...
relay "https" {
listen on egress port 443 tls
listen on egress port 80
protocol "http"
forward to <honk> port 31337 check tcp
forward to <matrix> port 8448 check tcp
forward to <certs> port 3080 check tcp
forward to <cloud> port 80 check tcp
}
...
Our relay is called "https" it will listen on "egress" ports 80 and 443, of course we will use the protocol "http" created previusly. The requests over this relay to each one of the previous matches tagged will be forwarded to the right table over the set port. The forward like will check constantly the port of that server to make sure that it's alive and working, so for example for the <cloud> line, relayd will check the port 80 (by tcp) of the IP 10.10.0.9 in the table <cloud>, if it's alive the request to "cloud.x61.sh" will end up in the right place. Can we check the health of these hosts? Sure:
# relayctl show hosts
Id Type Name Avlblty Status
1 table cloud:80 active (1 hosts)
1 host 10.10.0.9 100.00% up
total: 252/252 checks
2 table honk:31337 active (1 hosts)
2 host 10.10.0.7 100.00% up
total: 252/252 checks
3 table certs:3080 active (1 hosts)
3 host 127.0.0.1 100.00% up
total: 252/252 checks
4 table matrix:8448 active (1 hosts)
4 host 10.10.0.8 100.00% up
total: 252/252 checks
# relayctl show relays
Id Type Name Avlblty Status
1 relay https active
total: 109 sessions
last: 1/60s 109/h 109/d sessions
average: 2/60s 0/h 0/d sessions
2 relay https2:80 active
total: 7 sessions
last: 0/60s 7/h 7/d sessions
average: 0/60s 0/h 0/d sessions
That's all, now you have relayd in front of your cloud managing headers and certs, let's do a fast configtest and enable it on boot time:
# relayd -nf /etc/relayd.conf
configuration OK
# rcctl enable relayd
# rcctl start relayd
relayd(ok)
Now you are in trend with your own cloud. If you want to try your headers, you can use this site or this one. Have fun!.