Install Nextcloud on Arch Linux

From Open Source Ecology
Jump to: navigation, search

Introduction

This document describes how to set up Nextcloud on a virtual private server (VPS) hosted by a cloud service provider that provides images for Arch Linux. We assume there is a DNS record in place that points the address cloud.mydomain.net to the IP address provided by the cloud service provider. Note that this is not a hardened installation, it is merely used for experimentation. More configuration is required to harden the installation.

Basic setup Arch Linux

Initial setup

Since the system comes preinstalled with Arch Linux on it, the first thing to do is to update the system:

pacman -Syu

reboot

pacman -S base base-devel

Add an administration user

We add a day-to-day administrator account:

useradd -m -g users -s /bin/bash admin
passwd admin

Make sure that pacman can be executed as user admin:

visudo

And add the following:

admin ALL=(ALL) NOPASSWD: /usr/bin/pacman

Create a swap partition

The swap partition has already been activated, so there is nothing to do.

Configure the language and locales

In file /etc/locale.gen:

en_US.UTF-8 UTF-8

Then execute:

locale-gen

echo LANG=en_US.UTF-8 > /etc/locale.conf
export LANG=en_US.UTF-8

Add the correct time zone, for example:

ln -s /usr/share/zoneinfo/Europe/Amsterdam /etc/localtime

Time configuration

Since Arch sits in a virtual machine, we assume that the time is always correct.

Random number generation

Random number generation is important for security, so we install the following tools:

pacman -S rng-tools

systemctl start rngd
systemctl enable rngd

To check the level of entropy we can do:

cat /proc/sys/kernel/random/entropy_available

Since it runs on a virtual machine, we are not really sure how representative the entropy is.

We can also run the following tests which should have very few failures:

cat /dev/random | rngtest -c 1000
cat /dev/urandom | rngtest -c 1000

The rng-tools service is preferred over haveged.

Setting the hostname

Assuming we have a hostname mydomainname.net that points to our server's IP address:

In /etc/hostname:

cloud.mydomainname.net

In /etc/hosts:

127.0.1.1 cloud.mydomainname.net

After a reboot, we can check whether everything is set with the command hostname -f.

Setting up the network

The network has been set up fine by the cloud provider.

Setting up the firewall

We use iptables to set up the firewall. A chain is a set of rules. The chain INPUT is predefined for packets that enter the system.

First we set the policy for chain INPUT to target ACCEPT:

iptables -P INPUT ACCEPT
ip6tables -P INPUT ACCEPT

We then flush all the chains:

iptables -F
ip6tables -F

A table in this context is a packet matching table. There are five predefined tables. The default table filter, the tables nat, mangle, raw, and security. We flush the table nat:

iptables -t nat -F
ip6tables -t nat -F

We then delete all the user-defined chains:

iptables -X
ip6tables -X

We then set the policy to drop every packet that enters the system (make sure you are logged in via a video display and not SSH):

iptables -P INPUT DROP
ip6tables -P INPUT DROP

We append to chain INPUT on the loopback interface lo to jump to target ACCEPT. This is important for applications that use local servers.

iptables -A INPUT -i lo -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT

We then append to chain INPUT on interface ens3 a rule to accept connections with states ESTABLISHED and RELATED:

iptables -A INPUT -i ens3 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
ip6tables -A INPUT -i ens3 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

We then make sure that incomming TCP connections are SYN packets to prevent SYN flooding:

iptables -A INPUT -p tcp ! --syn -m state --state NEW -j DROP
ip6tables -A INPUT -p tcp ! --syn -m state --state NEW -j DROP

We also drop packets with incoming fragments. This is only for IPv4:

iptables -A INPUT -f -j DROP

We also drop bogons:

iptables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP
iptables -A INPUT -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP
ip6tables -A INPUT -p tcp --tcp-flags ALL ALL -j DROP
ip6tables -A INPUT -p tcp --tcp-flags ALL FIN,PSH,URG -j DROP
ip6tables -A INPUT -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP

We also reject malformed NULL packets:

iptables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP
ip6tables -A INPUT -p tcp --tcp-flags ALL NONE -j DROP

We can then store the firewall rules with the following commands:

iptables-save > /etc/iptables/iptables.rules
ip6tables-save > /etc/iptables/ip6tables.rules

To make the changes permanent, we can enable and start the Systemd service:

systemctl start iptables.service
systemctl enable iptables.service
systemctl status iptables.service

systemctl start ip6tables.service
systemctl enable ip6tables.service
systemctl status ip6tables.service

Fail2ban

The service fail2ban is a service for detecting login attempts and blocking it.

We install it with:

pacman -S fail2ban

Using systemd we can sandbox the fail2ban service with capabilities. To do so, we create a new file /etc/systemd/system/fail2ban.service.d/capabilities.conf with the following contents:

[Service]
PrivateDevices=yes
PrivateTmp=yes
ProtectHome=read-only
ProtectSystem=strict
NoNewPrivileges=yes
ReadWritePaths=-/var/run/fail2ban
ReadWritePaths=-/var/lib/fail2ban
ReadWritePaths=-/var/log/fail2ban
ReadWritePaths=-/var/spool/postfix/maildrop
ReadWritePaths=-/run/xtables.lock
CapabilityBoundingSet=CAP_AUDIT_READ CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW

Create configuration file /etc/fail2ban/fail2ban.local with the correct logtarget path:

[Definition]
logtarget = /var/log/fail2ban/fail2ban.log

Create the /var/log/fail2ban/ directory as root.

We have to set up paths for Arch Linux in a jail.local file:

[INCLUDES]
before = paths-arch.conf

Yay

With yay, we can keep also the AUR packages up to date.

git clone https://aur.archlinux.org/yay.git
tar xvzf yay.tar.gz
cd yay
makepkg -s
pacman -U yay*.pkg.tar.xz

SSH access

Configuration

The first service we would like to set up is SSH. We first configure the SSH daemon itself in file /etc/ssh/sshd_config:

PermitRootLogin no
TCPKeepAlive no
ClientAliveInterval 60
Ciphers aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512,hmac-sha2-256
KexAlgorithms diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1

We then restart the daemon:

systemctl restart sshd

The firewall

We have to open port 22 for SSH in our firewall:

iptables -A INPUT -i ens3 -p tcp --dport 22 -j ACCEPT
iptables-save > /etc/iptables/iptables.rules
ip6tables-save > /etc/iptables/ip6tables.rules

Fail2ban

We add the following jails in /etc/fail2ban/jail.d/:

In sshd.conf:

[sshd]
enabled = true

Installing Nextcloud

The version to install is 19.0.0. We are going to install it on top of nginx and MariaDB.

MariaDB

As a first step, we are going to install MariaDB with package mariadb. Before we start the service, we run:

mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql
systemctl start mariadb

Then add the database (substitute the password with a proper one):

CREATE DATABASE nextcloud DEFAULT CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextcloud'@'localhost' IDENTIFIED BY 'password';
FLUSH PRIVILEGES;

PHP

Install PHP with package php. We need to do additional configuration, in /etc/php/php.ini:

date.timezone = Europe/Amsterdam
memory_limit = 512M

For PHP we need to install the following modules:

module package comments
ctype php
curl php
dom php
GD php-gd enable in php.ini
iconv php enable in php.ini
json php
libxml php
mbstring php
openssl php
posix php
session php
SimpleXML php
xmlreader php
xmlwriter php
zip php
zlib php
pdomysql php enable in php.ini
mysqli php enable in php.ini
fileinfo php
bz2 php enable in php.ini
intl php-intl enable in php.ini
bcmath php enable in php.ini
gmp php enable in php.ini
imagick php-imagick enable in conf.d/imagick.ini

For further support for imagick, it is wise to install the following packages:

pacman -S ghostscript librsvg libwmf ocl-icd openexr openjpeg2 pango

We enable OPCache in php.ini:

zend_extension=opcache

And we set the following options:

opcache.enable=1
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=1

We enable APCu by installing php-apcu and enabling it in /etc/php/conf.d/apcu.ini:

extension=apcu.so

Nginx

We install nginx and set it up to obtain certificates by enabling the service, and open port 80:

iptables -A INPUT -i ens3 -p tcp --dport 80 -j ACCEPT
systemctl enable nginx
systemctl start nginx

We get a message that the max_types_hash_size is not enough. In /etc/nginx/nginx.conf add in the http block:

types_hash_max_size 4096;

We are now ready to install certificates.

Let's Encrypt certificates

Install the following packages to acquire certificates: certbot, certbot-nginx.

Then run:

certbot --nginx

We fill in:

email address <your email address>
terms agree
share info no
domain cloud.mydomain.net

It aquires the certificates but cannot find the server block for this domain.

PHP CGI

Install package php-fpm and in the configuration file /etc/php/php-fpm.d/www.conf uncomment env[PATH]. Also set up env[TMP] in /etc/php/php-fpm.d/www.conf and make sure the directory is writable for the Nginx user.

We also set the following values in /etc/php/php-fpm.d/www.conf, taken from the documentation and all values divided by 2.

pm.max_children = 60;
pm.start_servers = 6;
pm.min_spare_servers = 3;
pm.max_spare_servers = 9;
systemctl start php-fpm
systemctl enable php-fpm

Nextcloud

pacman -S nextcloud

We install the following pacman hook in /etc/pacman.d/hooks/nextcloud.hook:

[Trigger]
Operation = Install
Operation = Upgrade
Type = Package
Target = nextcloud
Target = nextcloud-app-*

[Action]
Description = Update Nextcloud installation
When = PostTransaction
Exec = /usr/bin/runuser -u http -- /usr/bin/php /usr/share/webapps/nextcloud/occ upgrade

Create a server block in the nginx.conf:

upstream php-handler {
    #server 127.0.0.1:9000;
    server unix:/run/php-fpm/php-fpm.sock;
}

server {
    listen 80;
    listen [::]:80;
    server_name cloud.mydomain.net;
    # enforce https
    return 301 https://$server_name:443$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name cloud.mydomain.net;

    ssl_certificate /etc/letsencrypt/live/cloud.mydomain.net/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.mydomain.net/privkey.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;
    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains;" 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 /usr/share/webapps/nextcloud;

    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 512M;
    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\/.+|.+\/richdocumentscode\/proxy)\.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;
    }
}

We have to create a data directory for Nextcloud:

mkdir /var/nextcloud
chown http:http /var/nextcloud
chmod 750 /var/nextcloud

We also have to change permissions for the apps directories:

mkdir -p /usr/share/webapps/nextcloud/data
chown -R http:http /usr/share/webapps/nextcloud/{apps,data}
chmod 750 /usr/share/webapps/nextcloud/{apps,data}

We have to give php-fpm permissions. We do this with the following command:

systemctl edit php-fpm.service

This will save the contents in /etc/systemd/system/php-fpm.service.d/override.conf.

The contents:

[Service]
ReadWritePaths = /usr/share/webapps/nextcloud/apps
ReadWritePaths = /usr/share/webapps/nextcloud/data
ReadWritePaths = /etc/webapps/nextcloud/config

# Replace the following path with the Nextcloud data directory
ReadWritePaths = /var/nextcloud

We then do:

systemctl restart php-fpm

We can then open the firewall on port 80 and 443:

systemctl stop fail2ban
iptables -A INPUT -i ens3 -p tcp --dport 80 -j ACCEPT
ip6tables -A INPUT -i ens3 -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -i ens3 -p tcp --dport 443 -j ACCEPT
ip6tables -A INPUT -i ens3 -p tcp --dport 443 -j ACCEPT
iptables-save > /etc/iptables/iptables.rules
ip6tables-save > /etc/iptables/ip6tables.rules
systemctl start fail2ban

Postinstall

It turns out that the cache was not enabled. We do that by adding the following key to /etc/webapps/nextcloud/config/config.php:

'memcache.local' => '\OC\Memcache\APCu',

Install the following apps:

  • talk
  • deck
  • notes

Maintenance

The services that are run are:

  • mariadb
  • nginx
  • php-fpm