WebDAV Server 5.17.3 on AWS User Guide
Overview
This guide covers launching and using the WebDAV Server 5.17.3 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg SFTP and file server listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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, and 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. The cloudimg image adds them:
- 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); 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 the same way.
- The share is confined. 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:8080behind 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. nginx drops requests for any host name the instance does not serve, passes the server the real client address, refuses state-changing requests that a web browser sends on behalf of another site, and 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
- 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 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.
Step 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 and your Region, then Launch through EC2. Choose m5.large (the server needs little CPU; size for your users and transfer volume), your key pair, and a security group as above. Size the root EBS volume for the files you plan to store; the shared folder lives on the root volume.
With the AWS CLI (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}]'
Step 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>
Step 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 (usually well under a minute):
systemctl is-active webdav nginx webdav-firstboot
sudo test -f /var/lib/cloudimg/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",...))
Step 4: Get your URL and password
sudo cat /root/webdav-credentials.txt
The file is readable by root only and 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 (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.
Step 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 (or install your own certificate, Step 10). Nothing is shown without a login: the server asks for a user name and password, and if you cancel you get only this:

Sign in as admin with the password from Step 4. The browser shows a read-only listing of the share; the folders below were added for the screenshots, and a new instance holds only README.txt:

Click a folder to browse into it and a file to open or download it:


A browser can only read. To upload, rename, move and delete, connect a WebDAV client (Step 6).
Step 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: your own (Step 10), or this instance's certificate imported as trusted. Its fingerprint, to compare with what your client shows:
openssl x509 -noout -subject -fingerprint -sha256 -in /etc/ssl/webdav/webdav.crt
Windows' WebDAV client also limits downloads to 50 MB by default (the FileSizeLimitInBytes value of the WebClient service); raise it in the registry if you move larger files with Explorer, or use rclone, WinSCP or Cyberduck.
Step 7: Use it from the command line
Port 80 redirects to HTTPS, a request without a login is refused, and the health check stays open for load balancers (it is answered by nginx and 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 cloudimg" > /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 cloudimg
delete -> HTTP 204
From your own machine, use the URL and password from Step 4. -u admin makes curl prompt for the password instead of taking it on the command line; -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 (sudo chown -R webdav:webdav /srv/webdav).
Step 8: Run the built in check
The image ships an end to end check you can run at any time; first boot runs it too. It 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.
Step 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:
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 = cloudimg
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, chrooted into /var/lib/webdav/root, may only talk to the loopback interface, and neither it 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; this is why 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 :-)
(etc, root, run, usr and var 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, so an encoded line break in a URL 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, blanks the forwarded host, prefix and port headers, and reduces a WebDAV Destination header to its path, so the server never sees values a client made up and a COPY or MOVE always stays inside the share:
grep -E 'proxy_set_header' /etc/nginx/sites-available/cloudimg-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 $cloudimg_webdav_dest;
proxy_set_header Connection "";
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 with the client address and the user name tried (never the password) to its journal: sudo journalctl -u webdav | grep -E 'invalid (password|username)'.
Step 10: Your own DNS name and certificate
Point a DNS record at the instance, then set the name the share is served as. This 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 one):
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
Behind an Application Load Balancer with a certificate from AWS Certificate Manager, register the instance on port 443 (HTTPS), use /healthz as the health check path, and set the instance's name to your domain with webdav-set-hostname; a request for a name the instance does not serve is dropped. 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.
Step 11: Passwords and accounts
Change the password with the tool the image ships. It prompts twice without echoing (or reads one line from standard input), 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 it does not hold an old password. Connected clients have to 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> (it prompts for the password; copy the part after the colon into password: "{bcrypt}..."), and give each account its own directory: or rules: if they should not all share the same folder. Note that running webdav-set-password later rewrites the file with a single account.
Step 12: Configuration
The server reads /etc/webdav/config.yml (owned by root, readable by the webdav group); paths in it 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 editing it, 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, mount the volume at /srv/webdav (add it to /etc/fstab), make it 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/cloudimg-webdav); set a value there and reload nginx if you want one.
Step 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/cloudimg-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'
Take new versions of the server from a new version of this listing.
Step 14: Back up the shared folder
Archive the shared folder and the configuration into a root only directory, and 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, extract the archive at /, 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, the instance's IP addresses, or set your own name withwebdav-set-hostname(Step 10). - Windows cannot map the drive. Windows' WebDAV client only connects over HTTPS to a certificate it trusts and 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 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-firstbootandjournalctl -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 your clients trust (Step 10).
- Treat the shared folder as shared content: anyone who can upload can place files another user will open. nginx sends
X-Content-Type-Options: nosniffand 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 upgradeapplies everything else. - Back up the shared folder (Step 14).
Support and licensing
WebDAV Server (hacdias/webdav) is open source under the MIT License (the licence text is at /usr/share/doc/webdav/LICENSE; source: https://github.com/hacdias/webdav/tree/v5.17.3). There is no per user or per CPU licence fee; the AMI price covers cloudimg's hardening, first boot setup and support. hacdias/webdav is the work of its respective authors; cloudimg is not affiliated with or endorsed by the project or its author.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA