Deployment guide

WebDAV Server 5.17.3

Overview

WebDAV Server (hacdias/webdav) shares a folder over WebDAV, the file protocol built into Windows File Explorer (Map network drive), macOS Finder (Connect to Server) and Linux file managers. It is also spoken by rclone, Cyberduck, WinSCP, cadaver and davfs2. Clients list, upload, download, rename, copy, move and delete files and folders. A web browser gets a read-only listing of the same share.

Upstream WebDAV Server is a single binary with a configuration file. It has no TLS certificate, no account and no sandbox of its own, and the SecureAnyCloud image adds them. This guide covers launching and using the WebDAV Server 5.17.3 on Ubuntu 24.04 AMI on Amazon EC2. It is one version of the SecureAnyCloud SFTP and file server listing on AWS Marketplace, which you pick as the software version when you subscribe and launch.

What you get:

  • HTTPS from the first boot. nginx serves the share on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A password unique to your instance. The account is admin. Its password is generated on first boot and written to /root/webdav-credentials.txt. The server stores only a bcrypt hash of it (cost 12), and the plain password never reaches the configuration file. No password is baked into the image.
  • No anonymous access. Every request (listings, downloads, uploads and every WebDAV method) needs a login, and a wrong user name or password is refused in the same way.
  • A confined share. The server runs as its own unprivileged account inside an empty chroot that holds only its binary, its configuration (read only) and the shared folder. A symbolic link to a file elsewhere on the instance, or a .. in a path, cannot reach outside the share.
  • Closed until first boot, and fail closed. The image holds no account, so the server cannot start, and nginx ships switched off. First boot creates the password and the certificate, starts the server, proves an authenticated round trip, and only then opens the public listener. If anything in that sequence fails, the listener is closed again.
  • Hardened defaults. The server listens on loopback 127.0.0.1:8080 behind nginx, in a systemd sandbox with no capabilities that may talk to the loopback interface only. Both the server and nginx are blocked from the instance metadata service, and the AMI requires IMDSv2.
  • Request filtering in nginx. nginx drops requests for any host name that the instance does not serve and passes the server the real client address. It refuses state-changing requests that a web browser sends on behalf of another site. It serves files with a sandboxing Content Security Policy, so an uploaded web page cannot run script.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: WebDAV Server 5.17.3 at /usr/local/bin/webdav (the official release binary, checked against its published checksum), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The shared folder holds a short README.

Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.

Prerequisites

Before you launch the instance, make sure you have:

  • An AWS account subscribed to the listing, an EC2 key pair, and a VPC subnet with a route to the internet (or a private route that you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (WebDAV and the browser listing) and TCP 80 (redirects to HTTPS) from your own networks only.
1

Launch the instance

In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick WebDAV Server 5.17.3 on Ubuntu 24.04 as the software version, select your Region, and choose Launch through EC2.

Choose m5.large, your key pair, and a security group as described above. The server needs little CPU, so size the instance for your users and transfer volume. The shared folder lives on the root volume, so size the root EBS volume for the files you plan to store.

To launch with the AWS CLI instead, replace the values in angle brackets:

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --block-device-mappings 'DeviceName=/dev/sda1,Ebs={VolumeSize=30,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=webdav}]'
2

Connect to your instance

Version SSH user

WebDAV Server 5.17.3 on Ubuntu 24.04 ubuntu

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot generates the password and the certificate, starts the server, opens nginx, and proves an authenticated round trip before it finishes. This usually takes well under a minute:

systemctl is-active webdav nginx webdav-firstboot
sudo test -f /var/lib/sac/data/webdav-firstboot.done && echo FIRSTBOOT_DONE

All three units are active, and the first-boot marker is present:

active active active FIRSTBOOT_DONE

Only SSH and nginx listen on public addresses. The WebDAV server listens on loopback:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sort
0.0.0.0:22 users:(("sshd",...)) 0.0.0.0:443 users:(("nginx",...)) 0.0.0.0:80 users:(("nginx",...)) 127.0.0.1:8080 users:(("webdav",...)) [::]:22 users:(("sshd",...)) [::]:443 users:(("nginx",...)) [::]:80 users:(("nginx",...))
4

Get your URL and password

sudo cat /root/webdav-credentials.txt

Only root can read the file. It holds the URL, the user name and this instance's password:

# WebDAV Server 5.17.3 - generated on first boot by webdav-firstboot.service
# These credentials are unique to this instance; none was baked into the image. The server stores only a bcrypt
# hash of the password (/etc/webdav/config.yml). Keep this file secret. WEBDAV_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ WEBDAV_USER=admin WEBDAV_PASSWORD=<WEBDAV_PASSWORD> ...

The URL uses the instance's public DNS name from EC2 instance metadata, or the public IP address when the subnet has no public DNS names. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve the share under your own DNS name, see Step 10.

5

Open the share in a browser

Open the WEBDAV_URL in your browser. The certificate is self-signed, so your browser asks you to confirm it the first time. Alternatively, install your own certificate (Step 10).

The server shows nothing without a login. It asks for a user name and password, and if you cancel you see no content.

Sign in as admin with the password from Step 4. The browser shows a read-only listing of the share. A new instance holds only README.txt.

Click a folder to browse into it, and click a file to open or download it.

A browser can only read. To upload, rename, move and delete files, connect a WebDAV client (Step 6).

6

Connect a WebDAV client

Every client uses the WEBDAV_URL, the user admin and the password from Step 4:

Client How to connect

Windows File Explorer This PC, Map network drive, folder https://<your-host>/

macOS Finder Go, Connect to Server, https://<your-host>/

GNOME Files, Dolphin Other Locations or the address bar: davs://<your-host>/

rclone rclone config, storage type webdav, vendor other, URL https://<your-host>/

cadaver cadaver https://<your-host>/

davfs2 sudo mount -t davfs https://<your-host>/ /mnt/webdav

Cyberduck, WinSCP protocol WebDAV (HTTPS), port 443

Clients that check certificates (Windows always does) need a certificate they trust. Use your own (Step 10), or import this instance's certificate as trusted. To compare with what your client shows, print the certificate fingerprint:

openssl x509 -noout -subject -fingerprint -sha256 -in /etc/ssl/webdav/webdav.crt

The Windows WebDAV client also limits downloads to 50 MB by default (the FileSizeLimitInBytes value of the WebClient service). To move larger files with Explorer, raise the limit in the registry. Alternatively, use rclone, WinSCP or Cyberduck.

7

Use it from the command line

Port 80 redirects to HTTPS, and a request without a login is refused. The health check stays open for load balancers. nginx answers it, and it reveals nothing about your files:

H=$(sudo grep '^WEBDAV_URL=' /root/webdav-credentials.txt | sed -E 's#^WEBDAV_URL=https://([^/]+)/.*#\1#')
curl -s -o /dev/null -w 'http:// -> HTTP %{http_code}\n' --connect-to "$H:80:127.0.0.1:80" "http://$H/"
curl -sk -o /dev/null -w 'no login -> HTTP %{http_code}\n' --connect-to "$H:443:127.0.0.1:443" "https://$H/"
curl -sk -o /dev/null -w 'PROPFIND -> HTTP %{http_code}\n' -X PROPFIND --connect-to "$H:443:127.0.0.1:443" "https://$H/"
curl -sk -o /dev/null -w 'healthz -> HTTP %{http_code}\n' --connect-to "$H:443:127.0.0.1:443" "https://$H/healthz"
http:// -> HTTP 301 no login -> HTTP 401 PROPFIND -> HTTP 401 healthz -> HTTP 200

With the password, upload a file, list the share with a WebDAV PROPFIND, read the file back, and delete it again. The password is passed to curl on standard input, so it never appears in the process list or your shell history:

sudo bash -c ' F=/root/webdav-credentials.txt P=$(grep "^WEBDAV_PASSWORD=" $F | cut -d= -f2-) H=$(grep "^WEBDAV_URL=" $F | sed -E "s#^WEBDAV_URL=https://([^/]+)/.*#\1#") IP=$(hostname -I | cut -d" " -f1) dav() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -s --cacert /etc/ssl/webdav/webdav.crt --connect-to "$H:443:$IP:443" "$@"; } echo "hello from sac" > /run/report.txt dav -o /dev/null -w "upload -> HTTP %{http_code}\n" -T /run/report.txt "https://$H/report.txt" echo "list -> $(dav -X PROPFIND -H "Depth: 1" "https://$H/" | grep -o "<D:displayname>[^<]*" | cut -d">" -f2 | grep . | sort | tr "\n" " ")" echo "download -> $(dav "https://$H/report.txt")" dav -o /dev/null -w "delete -> HTTP %{http_code}\n" -X DELETE "https://$H/report.txt" rm -f /run/report.txt '
upload -> HTTP 201 list -> README.txt report.txt download -> hello from sac delete -> HTTP 204

From your own machine, use the URL and password from Step 4. The -u admin option makes curl prompt for the password instead of taking it on the command line, and -k accepts the self-signed certificate:

curl -k -u admin -T ./photo.jpg https://<public-ip>/photo.jpg
curl -k -u admin -O https://<public-ip>/photo.jpg
curl -k -u admin -X MKCOL https://<public-ip>/Projects/

Files uploaded over WebDAV are owned by the webdav account. You can also copy files straight into /srv/webdav over SSH. Make them readable by that account with sudo chown -R webdav:webdav /srv/webdav.

8

Run the built-in check

The image ships an end-to-end check that you can run at any time. First boot runs it too. The check reads the credentials file itself and drives the server over HTTPS through nginx, with the instance's certificate pinned:

sudo webdav-verify.sh
port 80 -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ ; an unknown Host is dropped on 80 and 443; /healthz answers ok an encoded CR/LF in the path stays encoded in the redirect (no injected header on 80 or 443) anonymous: GET, HEAD, PROPFIND, OPTIONS, PUT, DELETE, MKCOL, PROPPATCH, LOCK, COPY, MOVE all refused (401), also with a forged X-Forwarded-For IP admin: PROPFIND 207 lists README.txt; the browser listing renders PUT/GET byte-compare identical (262144 bytes, sha256 <sha256>); MKCOL, COPY (https://<host> Destination) and MOVE (https://<ip> Destination) 201; cross-origin PUT 403; files carry a sandbox CSP; DELETE then 404 confined: symlinks to /etc/passwd, the credentials file and /etc are not served; '..' and encoded '..' never leave the share guessed credentials (admin/admin, admin/webdav, webdav/webdav, the right password for another user, a near miss) refused (401) WEBDAV_ROUNDTRIP_OK https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ via <private-ip>: anonymous refused for every method; PROPFIND, listing, upload, download+byte-compare, MKCOL, COPY, MOVE and delete verified over HTTPS; symlinks and '..' confined to the share

It leaves the shared folder exactly as it found it.

9

Security checks

The configuration holds only a bcrypt hash of the password (the command below hides it), and the files that matter are locked down. Check the permissions:

sudo sed -E 's/(\{bcrypt\}).*/\1<hash>"/' /etc/webdav/config.yml | grep -vE '^\s*#' | sed -n '/^users:/,$p'
sudo stat -c '%a %U:%G %n' /etc/webdav/config.yml /etc/webdav /srv/webdav /root/webdav-credentials.txt /etc/ssl/webdav/webdav.key
users: - username: admin password: "{bcrypt}<hash>" permissions: CRUD 640 root:webdav /etc/webdav/config.yml 750 root:webdav /etc/webdav 750 webdav:webdav /srv/webdav 600 root:root /root/webdav-credentials.txt 600 root:root /etc/ssl/webdav/webdav.key

The certificate was generated on this instance and names its own addresses:

H=$(sudo grep '^WEBDAV_URL=' /root/webdav-credentials.txt | sed -E 's#^WEBDAV_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = sac X509v3 Subject Alternative Name: DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

The server runs unprivileged with no capabilities, is chrooted into /var/lib/webdav/root, and may talk only to the loopback interface. Neither the server nor nginx can reach the instance metadata service:

systemctl show webdav -p User -p RootDirectory -p ProtectSystem -p NoNewPrivileges -p CapabilityBoundingSet -p IPAddressAllow -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressAllow=::1/128 127.0.0.0/8 IPAddressDeny=::/0 0.0.0.0/0 RootDirectory=/var/lib/webdav/root CapabilityBoundingSet= User=webdav ProtectSystem=strict NoNewPrivileges=yes IPAddressDeny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32

Inside the chroot, the server sees its own binary, its configuration (mounted read only) and the share at /data, and nothing else from the instance. For this reason, a symbolic link in the share that points at /etc/passwd or the credentials file serves nothing:

sudo ls /var/lib/webdav/root systemd-analyze security webdav.service | tail -1
config.yml data dev etc proc root run sys usr var webdav → Overall exposure level for webdav.service: 1.4 OK :-)

The etc, root, run, usr and var directories are empty mount points that systemd uses for its sandbox.

The port 80 redirect copies the request path exactly as it was sent, still percent-encoded. An encoded line break in a URL therefore cannot add a header of its own to the response:

H=$(sudo grep '^WEBDAV_URL=' /root/webdav-credentials.txt | sed -E 's#^WEBDAV_URL=https://([^/]+)/.*#\1#')
N=$(curl -s -o /dev/null -D - --connect-to "$H:80:127.0.0.1:80" "http://$H/x%0d%0aX-Inj:%201" | tr -d '\r' | grep -ic '^x-inj' || true)
echo "injected headers: $N"
injected headers: 0

nginx passes the server a fixed host name and the real client address from the connection. It blanks the forwarded host, prefix and port headers, and it reduces a WebDAV Destination header to its path. The server therefore never sees values that a client made up, and a COPY or MOVE always stays inside the share:

grep -E 'proxy_set_header' /etc/nginx/sites-available/sac-webdav
proxy_set_header Host $webdav_host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Host ""; proxy_set_header X-Forwarded-Port ""; proxy_set_header X-Forwarded-Prefix ""; proxy_set_header Forwarded ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Destination $sac_webdav_dest; proxy_set_header Connection "";

Confirm that no per-instance password reached a log, the journal, shell history, backups, /etc or the shared folder:

sudo webdav-secret-leak-check.sh
LEAKCHECK_OK no per-instance secret in the journal, auth.log, syslog, cloud-init, nginx logs, shell history, MOTD, /var/backups, /etc, the share, /tmp; config.yml holds bcrypt hashes only

The server logs every failed login to its journal, with the client address and the user name tried (never the password). To list the failures, run sudo journalctl -u webdav | grep -E 'invalid (password|username)'.

10

Your own DNS name and certificate

Point a DNS record at the instance, then set the name that the share is served as. The command mints a new self-signed certificate for the name, updates the nginx host and same-origin rules and the URL in /root/webdav-credentials.txt, reloads nginx, and keeps the name across reboots:

sudo webdav-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx. Running webdav-set-hostname again replaces it with a self-signed certificate:

sudo install -m 0644 <your-domain>.crt /etc/ssl/webdav/webdav.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/webdav/webdav.key
sudo nginx -t && sudo systemctl reload nginx

To run behind an Application Load Balancer with a certificate from AWS Certificate Manager, register the instance on port 443 (HTTPS) and use /healthz as the health check path. Set the instance's name to your domain with webdav-set-hostname, because the instance drops a request for a name it does not serve.

If you put another proxy in front, keep it the only path to the instance. nginx records the load balancer's address as the client.

11

Passwords and accounts

Change the password with the tool that the image ships. The tool prompts twice without echoing, or reads one line from standard input. It requires at least 12 characters, writes a fresh bcrypt hash into /etc/webdav/config.yml and restarts the server. The password never appears on a command line, in the logs or in your history:

sudo webdav-set-password

Then update or remove WEBDAV_PASSWORD in /root/webdav-credentials.txt, so that the file does not hold an old password. Connected clients must sign in again with the new password.

webdav-set-password <user> renames the single account to <user>.

For several accounts, edit /etc/webdav/config.yml by hand (Step 12). Add an entry per person under users:, with a hash made by htpasswd -nBC 12 <user>. The command prompts for the password. Copy the part after the colon into password: "{bcrypt}...". If the accounts should not all share the same folder, give each account its own directory: or rules:.

Running webdav-set-password later rewrites the file with a single account.

12

Configuration

The server reads /etc/webdav/config.yml, which is owned by root and readable by the webdav group. Paths in the file are inside the chroot, where /data is /srv/webdav. The image sets:

Setting Meaning Image default

address / port Bind address 127.0.0.1 / 8080 (nginx fronts it)

tls The server's own TLS false (nginx terminates TLS)

behindProxy Take the client address from nginx true

directory The shared folder /data (/srv/webdav on the instance)

permissions Rights of a request without an account none

users Accounts, bcrypt hashes and their rights admin with CRUD (create, read, update, delete)

noSniff Content types from the file name only true

listing The browser listing on

cors Cross-origin requests from web pages off

log Log format and output console lines to the journal

After you edit the file, restart the server and run the built-in check:

sudo systemctl restart webdav
sudo webdav-verify.sh

The server can see nothing outside its chroot, so keep shared files in /srv/webdav.

To hold the share on a separate EBS volume, stop the server and mount the volume at /srv/webdav. Add the mount to /etc/fstab, make the directory 0750 webdav:webdav, copy the README back if you want it, and start the server again.

nginx streams uploads with no size cap (client_max_body_size 0 in /etc/nginx/sites-available/sac-webdav). To set a cap, change the value there and reload nginx.

13

Server components

Component Version Path

WebDAV Server (hacdias/webdav) 5.17.3 /usr/local/bin/webdav

nginx 1.24.0 /etc/nginx/sites-available/sac-webdav

Ubuntu 24.04 LTS

Path Purpose

/srv/webdav The shared folder (0750 webdav:webdav)

/etc/webdav/config.yml Server configuration (accounts as bcrypt hashes)

/var/lib/webdav/root The empty chroot the server runs in

/etc/ssl/webdav/ Certificate and key

/root/webdav-credentials.txt URL, user and password (mode 0600)

/usr/share/doc/webdav/SOURCE Where the binary came from, with its checksum

/usr/local/sbin/webdav-firstboot.sh First boot

/usr/local/sbin/webdav-set-hostname Host name and certificate

/usr/local/sbin/webdav-set-password Password change

/usr/local/sbin/webdav-verify.sh End to end check (Step 8)

/usr/local/sbin/webdav-secret-leak-check.sh Proves no password reached a log

webdav version cat /usr/share/doc/webdav/SOURCE
sudo journalctl -u webdav-firstboot --no-pager -o cat | grep -E 'Serving as|First boot complete'

New versions of the server arrive as new versions of this listing.

14

Back up the shared folder

Archive the shared folder and the configuration into a root-only directory. Never copy the credentials file into a backup location:

sudo install -d -o root -g root -m 0700 /var/backups/webdav
sudo bash -c 'umask 077; tar -czf /var/backups/webdav/webdav-$(date -u +%Y%m%d).tar.gz -C / srv/webdav etc/webdav/config.yml'
sudo ls /var/backups/webdav/
sudo webdav-secret-leak-check.sh

Copy the archive off the instance (for example to Amazon S3), or snapshot the root volume. The archive holds your accounts as bcrypt hashes, so keep it private.

To restore, stop the server and extract the archive at /. Then run sudo chown -R webdav:webdav /srv/webdav and start the server.

Troubleshooting

  • The browser or client closes the connection. nginx drops requests for host names it does not serve. Use the name in WEBDAV_URL or the instance's IP addresses, or set your own name with webdav-set-hostname (Step 10).
  • Windows cannot map the drive. The Windows WebDAV client connects only over HTTPS to a certificate it trusts, and it needs the WebClient service running. Install your own certificate (Step 10), or import this instance's certificate into Trusted Root Certification Authorities.
  • Uploads from a web page are refused (403). nginx refuses state-changing requests that a browser sends on behalf of another site. WebDAV clients are not affected.
  • A file opened in the browser does not run its scripts. Files are served with a sandboxing Content Security Policy on purpose. Download the file instead.
  • Nothing answers after launch. Check systemctl status webdav-firstboot and journalctl -u webdav-firstboot. Until first boot completes, the share is deliberately closed.

Security recommendations

  • Allow ports 22, 80 and 443 in the security group from your own networks only.
  • Keep the password secret, change it if it may have been shared (Step 11), and give each person their own account.
  • Use your own DNS name and a certificate that your clients trust (Step 10).
  • Treat the shared folder as shared content. Anyone who can upload can place files that another user will open. nginx sends X-Content-Type-Options: nosniff and a sandboxing Content Security Policy, so browsers do not reinterpret or run an uploaded file.
  • Keep the operating system patched. Unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.
  • Back up the shared folder (Step 14).

Support and licensing

WebDAV Server (hacdias/webdav) is open source under the MIT License. The license text is at /usr/share/doc/webdav/LICENSE, and the source is at https://github.com/hacdias/webdav/tree/v5.17.3. There is no per-user or per-CPU license fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.

hacdias/webdav is the work of its respective authors. SecureAnyCloud is not affiliated with or endorsed by the project or its author.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24 hour response SLA

Deploy on AWS

Launch WebDAV Server 5.17.3 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS