WebDAV with nginx

This website has been hosted on an Online.net dedicated server since its creation. I've been one of their customers for the past 3 years now, and I still don't have anything bad to say about them.

They recently upgraded their personnal range, and I took the opportunity to upgrade from a single server running all of my services to 2 servers running LXC containers that are hosting my services.

It took me 2 days to migrate everything, but it was worth it. If I decide to switch servers again, I'll have to migrate the containers instead of the services themselves. Considering they are stored on a separate BTRFS volume, it shouldn't take me more than a few hours at most.

During the migration, I realized that I needed to make files that were hosted on one server accessible to the other. I could have gone with CIFS or NFS, but I wanted to have encryption built-in instead of having to rely on a VPN for that. Since I figured it was a good opportunity to learn something new, I ended up going with WebDAV.

In this tutorial, I'll explain how I've configured a read-only WebDAV share using nginx and Let'sEncrypt SSL certificates between two Debian Jessie containers.

Server configuration

Installing the required packages

First thing first, we need to install the packages we'll need for this configuration :

apt update

apt -t jessie-backports install nginx letsencrypt
apt install apache2-utils

Getting our first certificate from letsencrypt

letsencrypt configuration

Let's create a configuration file for letsencrypt :

mkdir /etc/letsencrypt

echo 'rsa-key-size = 3072
renew-by-default
text = True
agree-tos = True
renew-by-default = True
authenticator = webroot
email = admin@example.com
webroot-path = /var/www/letsencrypt/' > /etc/letsencrypt/cli.ini

Please do modify admin@example.com by your actual e-mail address.

We also need to create the directory structure where letsencrypt ACME challenge temporary files will be stored :

mkdir -p /var/www/letsencrypt/.well-known

nginx configuration

We now need to configure nginx by adding the following in the /etc/nginx/sites-available/default file, anywhere in the server{} block that is configured to listen on port 80.

location /.well-known/acme-challenge {
  root /var/www/letsencrypt;
}

Let's make sure that we haven't done anything wrong :

nginx -t

The command should give you the following output :

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If that's the case, you can safely reload the nginx daemon :

nginx -s reload

Certificate request

Now that letsencrypt and nginx are properly configured, we can request our certificate from letsencrypt :

letsencrypt --config /etc/letsencrypt/cli.ini certonly -w /var/www/letsencrypt -d www.example.com

Please do modify www.example.com by your server's FQDN, and please note that the letsencrypt servers need to be able to resolve that name to your server's IP.

If everything goes well, your certificates will be generated and stored in the /etc/letsencrypt folder.

WebDAV configuration

Now that we've obtained our certificate from letsencrypt, we can begin configuring nginx.

First, we need to comment two SSL directives from the default nginx configuration :

sed -i '/ssl_/ s/^/#/' /etc/nginx/nginx.conf

Let's now create a /etc/nginx/conf.d/ssl.conf with the following content :

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;

ssl_dhparam /etc/nginx/ssl/dhparam.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;

add_header Strict-Transport-Security max-age=15768000;
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

ssl_stapling on;
ssl_stapling_verify on;
resolver 127.0.0.1 valid=300s;
resolver_timeout 5s;

This configuration will work if you're using a single certificate on your server. If not, you'll have to remove the ssl_certificate, ssl_certificate_key and ssl_trusted_certificate directives from this file and move them to the correct server{} block.

We now need to generate a dhparam.pem file :

mkdir /etc/nginx/ssl && chmod 700 /etc/nginx/ssl
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 3072
chmod 600 /etc/nginx/ssl/dhparam.pem

Let's now generate a HTTP basic authentication file. This example creates a user named example :

mkdir /etc/nginx/auth

htpasswd -c /etc/nginx/auth/webdav example
New password: 
Re-type new password: 
Adding password for user user

This file has to be readable by the user running your webserver. For security reasons, we'll make it readable only by him :

chown -R www-data:nogroup /etc/nginx/auth
chmod 700 /etc/nginx/auth
chmod 400 /etc/nginx/auth/webdav

Let's now modify our /etc/nginx/sites-available/default file with the following content :

server {
  listen 80 default_server;
  listen [::]:80 default_server ipv6only=on;
  server_name "";

  return 444;
}

server {
  listen 443 default_server ssl http2;
  listen [::]:443 default_server ipv6only=on ssl http2;
  server_name "";

  return 444;
}

We now have to create a /etc/nginx/sites-available/example file that will contain our actual webdav configuration. This example makes a data folder stored in /var/www/ accessible.

server {
  listen 80;
  listen [::]:80;
  server_name www.example.com;
  return 301 https://$server_name$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name www.example.com;

  root /var/www;

  location / {
    index index.html;
  }

  location /.well-known/acme-challenge {
    root /var/www/letsencrypt;
  }

  location /data {
    client_body_temp_path /tmp;
    dav_methods PUT DELETE MKCOL COPY MOVE;
    dav_ext_methods PROPFIND OPTIONS;
    create_full_put_path on;
    dav_access user:r group:r;

    auth_basic "Restricted access";
    auth_basic_user_file auth/webdav;

    limit_except GET {
      allow <YOUR IP HERE>;
      deny all;
    }
  }

}

The last thing we have to do is to create a symlink so that nginx will load our configuration :

ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/example

Like before, let's make sure our configuration is correct and then reload the daemon :

nginx -t
nginx -s reload

That's it for the WebDAV configuration server-side !

nginx monitoring

If you're using monit, you can easily monitor the nginx daemon by copying the following in /etc/monit/conf.d/nginx :

check process nginx
  with pidfile "/run/nginx.pid"
  start program = "/bin/systemctl start nginx"
  stop program = "/bin/systemctl stop nginx"
  alert monit@example.com

Certificates auto-renewal

This goes beyond the scope of the article, but since letsencrypt certficates are only valid for 3 months, you'll need to renew them regularily. You can do so manually or you can setup a cron that does it for you.

I personnaly use the following script :

#!/bin/bash

PRG="/usr/bin/letsencrypt"
CONFIG="/etc/letsencrypt/cli.ini"
MAILDEST="admin@example.com"
GLOBAL=0

# www.example.com
$PRG --config $CONFIG certonly -w /var/www/letsencrypt -d www.example.com
[[ $? != 0 ]] && GLOBAL=$(( $GLOBAL + 1 ))

if [[ $GLOBAL == 0 ]]; then
  /usr/sbin/nginx -s reload
else
  echo "Something went wrong while renewing the certificates on $(hostname -f)
  Manual action needed." | mail -s "Letsencrypt error on $(hostname -f)" $MAILDEST
fi

You can add multiple domains in the script. As long as you add all 3 lines for each domain, it will not automatically reload nginx if one or more certificate could not be renewed and will send an e-mail to the address configured in the MAILDEST variable.

You can configure this script in the root user crontab using the crontab -e command :

## LETSENCRYPT CERTIFICATE AUTORENEWAL
30 03 01 */2 * /root/bin/tlsrenew

This will run the script every two months, on the first day of the month, at 3:30 AM.

Client configuration

Installing the required packages

A single package is required to mount a webdav volume on Debian :

apt update && apt install davfs2

Mounting the share manually

If like me, you want to mount your webdav share in a LXC container, you'll first need to make sure that the following line is present in its configuration file :

lxc.cgroup.devices.allow = c 10:229 rwm

You'll also need to create the /dev/fuse node in the container :

mknod /dev/fuse c 10 229

In any case, we have to edit the /etc/davfs2/secrets file to add the mount point, username and password that will be used to mount the share :

echo '/data webdav notanactualpassword' >> /etc/davfs2/secrets

Once that's done, we can mount our share with the following command :

mount -t davfs https://www.example.com/data /data -o ro,dir_mode=750,file_mode=640,uid=root,gid=root

You might need to edit the parameters depending on which users you want to make the share available to.

Mouting the share on boot

A davfs volume can be mounted via the /etc/fstab file, but I decided to use monit instead so that the volume would be mounted again automatically should my WebDAV server reboot.

In order to do so, I first created a davfs.txt file in the /var/www/data folder on my WebDAV server :

touch /var/www/data/davfs.txt

I then created the following /root/bin/mount_davfs script :

#!/bin/bash

mknod /dev/fuse c 10 229
mount -t davfs https://www.example.com/data /data -o ro,dir_mode=750,file_mode=640,uid=root,gid=root

The last thing I did was create a /etc/monit/conf.d/davfs file with the following content :

check file davfs with path /data/davfs.txt
  alert monit@example.com
  if does not exist then exec "/root/bin/mount_davfs"

That way, if monit notices that the /data/davfs.txt file becomes inaccessible for some reason, it will try remouting the share.

Conclusion

That's all ! Hopefully this has been useful to someone. Please do comment below if you have any question or if this has been helpful !