WsgiDAV 4.3.5 on AWS User Guide
Overview
This guide covers launching and using the WsgiDAV 4.3.5 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.
WsgiDAV is a generic, extensible WebDAV server written in Python. It publishes a directory as a WebDAV share that you can mount straight from the file manager you already use (Windows File Explorer, macOS Finder, GNOME Files) or from clients such as Cyberduck, WinSCP, davfs2 and rclone. It implements WebDAV class 2: create, read, update, delete, copy and move, resource locking and custom properties, plus a browsable web view of the share.
Upstream's quick start serves the current directory anonymously and writeably on every interface. The cloudimg image does the opposite:
- 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. Every request needs a login, including the web view and its style sheets; there is no anonymous access. The account is
webdav, its password is generated on first boot and written to/root/wsgidav-credentials.txt, and the server stores only a bcrypt hash of it. No password is baked into the image. - Closed until first boot, and fail closed. The image holds no password database, so WsgiDAV cannot start, and nginx ships switched off. First boot creates the password and the certificate, starts WsgiDAV, and only then opens the public listener; if anything in that sequence fails, the listener is closed again.
- One directory only. WsgiDAV serves
/srv/webdav, never the whole filesystem, does not follow symbolic links, and listens on loopback127.0.0.1:8080behind nginx. - Hardened defaults. WsgiDAV runs as its own unprivileged account in a systemd sandbox that can write only to the share; WsgiDAV and nginx are both blocked from the instance metadata service; the AMI requires IMDSv2; requests for any host name the instance does not serve are dropped; and nginx refuses state-changing requests that a web browser sends on behalf of another site.
- Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.
What is on the image: WsgiDAV 4.3.5 with the Cheroot web server in a Python virtual environment at /opt/wsgidav/venv (every package pinned by version and checksum), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent. The share ships empty.
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 web view) 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 WsgiDAV 4.3.5 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (WsgiDAV needs little CPU; size for your users), your key pair, and a security group as above. Size the root EBS volume for the files you plan to store; the share 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=wsgidav}]'
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| WsgiDAV 4.3.5 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 WsgiDAV, opens nginx and proves a full WebDAV round trip before it finishes (well under a minute):
systemctl is-active wsgidav nginx wsgidav-firstboot
sudo test -f /var/lib/cloudimg/wsgidav-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; WsgiDAV 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:(("wsgidav",...))
[::]:22 users:(("sshd",...))
[::]:443 users:(("nginx",...))
[::]:80 users:(("nginx",...))
Step 4: Get your URL and password
sudo cat /root/wsgidav-credentials.txt
The file is readable by root only and holds the URL, the user name and this instance's password:
WEBDAV_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
WEBDAV_USER=webdav
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 your 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). Without a login, every page answers 401:

Sign in as webdav with the password from Step 4. The web view lists the folders and files in /srv/webdav with their type, size and date, and names the signed in user and access level. The share ships empty; the files below were uploaded for the screenshots:

Open a folder to browse into it, and a file to read it:


The web view is for browsing and downloading. To upload, create folders, rename, move and delete, mount the share (Step 6) or use any WebDAV client.
Step 6: Mount the share from your desktop
These run on your own computer. Use the URL, the user webdav and the password from Step 4. Clients that check certificates (Windows always does) need a certificate they trust: either a certificate from your own certificate authority (Step 10), or the instance's own certificate imported as trusted. Its fingerprint, to compare with what your client shows:
openssl x509 -noout -subject -fingerprint -sha256 -in /etc/ssl/wsgidav/wsgidav.crt
Windows File Explorer
This PC > Map network drive. Folder: https://<your-domain>/
Tick "Connect using different credentials", then sign in as webdav.
macOS Finder
Go > Connect to Server (Command K): https://<your-domain>/
Sign in as webdav.
Linux (GNOME Files)
Other Locations > Connect to Server: davs://<your-domain>/
Linux (davfs2)
sudo apt-get install -y davfs2
sudo mkdir -p /mnt/webdav
sudo mount -t davfs https://<your-domain>/ /mnt/webdav
rclone
rclone config create myshare webdav url=https://<your-domain>/ vendor=other user=webdav pass=$(rclone obscure -)
rclone ls myshare:
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 the share):
H=$(sudo grep '^WEBDAV_URL=' /root/wsgidav-credentials.txt | sed -E 's#^WEBDAV_URL=https://([^/]+)/.*#\1#')
curl -s -o /dev/null -w 'http:// -> %{http_code} %{redirect_url}\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 'healthz -> HTTP %{http_code}\n' --connect-to "$H:443:127.0.0.1:443" "https://$H/healthz"
http:// -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
no login -> HTTP 401
healthz -> HTTP 200
With the password, create a folder, upload a file, copy it, read the copy back, list the folder with a WebDAV PROPFIND and delete it all 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/wsgidav-credentials.txt
P=$(grep "^WEBDAV_PASSWORD=" $F | cut -d= -f2-)
H=$(grep "^WEBDAV_URL=" $F | sed -E "s#^WEBDAV_URL=https://([^/]+)/.*#\1#")
dav() { printf "user = \"webdav:%s\"\n" "$P" | curl -K - -s --cacert /etc/ssl/wsgidav/wsgidav.crt --connect-to "$H:443:127.0.0.1:443" "$@"; }
echo "hello from cloudimg" > /run/report.txt
dav -o /dev/null -w "MKCOL -> HTTP %{http_code}\n" -X MKCOL "https://$H/reports/"
dav -o /dev/null -w "PUT -> HTTP %{http_code}\n" -T /run/report.txt "https://$H/reports/report.txt"
dav -o /dev/null -w "COPY -> HTTP %{http_code}\n" -X COPY -H "Destination: https://$H/reports/copy.txt" "https://$H/reports/report.txt"
echo "GET -> $(dav "https://$H/reports/copy.txt")"
dav -X PROPFIND -H "Depth: 1" "https://$H/reports/" | grep -oE "<[a-zA-Z0-9]+:href>[^<]*</[a-zA-Z0-9]+:href>"
dav -o /dev/null -w "DELETE -> HTTP %{http_code}\n" -X DELETE "https://$H/reports/"
rm -f /run/report.txt
'
MKCOL -> HTTP 201
PUT -> HTTP 201
COPY -> HTTP 201
GET -> hello from cloudimg
<ns0:href>/reports/</ns0:href>
<ns0:href>/reports/copy.txt</ns0:href>
<ns0:href>/reports/report.txt</ns0:href>
DELETE -> HTTP 204
From your own machine, use the URL and password from Step 4. -u webdav makes curl prompt for the password instead of taking it on the command line; -k accepts the self signed certificate:
curl -k -u webdav -X PROPFIND -H 'Depth: 1' https://<public-ip>/
curl -k -u webdav -T ./photo.jpg https://<public-ip>/photo.jpg
Files uploaded over WebDAV are owned by the wsgidav account. You can also copy files straight into /srv/webdav over SSH; make them readable by that account (sudo chown -R wsgidav:wsgidav /srv/webdav).
Step 8: Run the built in WebDAV 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 real protocol over HTTPS through nginx, with the instance's certificate pinned:
sudo wsgidav-verify-webdav.sh
port 80 -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ ; an unknown Host is dropped on 80 and 443
anonymous GET/PROPFIND/OPTIONS/PUT and the browse assets refused (401); /healthz answers ok
every guessed credential refused (401), including the right password for another user
OPTIONS advertises DAV: 1,2; no CORS header
PROPFIND multistatus OK: 1 response element(s), the root is a collection
PUT/GET byte-compare identical: 262144 bytes, sha256 <sha256>
COPY and MOVE completed; the MOVE source is gone (404)
COPY via <private-ip> works (Destination rewritten to ec2-<public-ip-dashed>.<region>.compute.amazonaws.com); a foreign Destination is refused (502)
cross-origin PUT refused (403); same-origin PUT accepted
LOCK held (a tokenless write got 423) and UNLOCK released it
PROPPATCH dead property stored and read back
DELETE removed the collection; GET and PROPFIND both return 404
WEBDAV_ROUNDTRIP_OK https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/ via <private-ip>: anonymous refused; authenticated PROPFIND, PUT, GET+byte-compare, MKCOL, COPY, MOVE (also via <private-ip>), LOCK/UNLOCK, PROPPATCH and DELETE verified over HTTPS
It leaves the share exactly as it found it.
Step 9: Security checks
The server stores only a bcrypt hash of the password, and the files that matter are locked down:
sudo sed -E 's/^([^:]+):\$2y\$12\$.*/\1:$2y$12$<bcrypt hash>/' /etc/wsgidav/htpasswd
sudo stat -c '%a %U:%G %n' /etc/wsgidav/htpasswd /etc/wsgidav/wsgidav.yaml /srv/webdav /root/wsgidav-credentials.txt
webdav:$2y$12$<bcrypt hash>
640 root:wsgidav /etc/wsgidav/htpasswd
640 root:wsgidav /etc/wsgidav/wsgidav.yaml
750 wsgidav:wsgidav /srv/webdav
600 root:root /root/wsgidav-credentials.txt
The certificate was generated on this instance and names its own addresses:
H=$(sudo grep '^WEBDAV_URL=' /root/wsgidav-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
WsgiDAV runs unprivileged in a sandbox that can write only to the share, and neither WsgiDAV nor nginx can reach the instance metadata service:
systemctl show wsgidav -p User -p ProtectSystem -p ReadWritePaths -p NoNewPrivileges -p IPAddressDeny
systemctl show nginx -p IPAddressDeny
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
User=wsgidav
ReadWritePaths=/srv/webdav
ProtectSystem=strict
NoNewPrivileges=yes
IPAddressDeny=fd00:ec2::254/128 168.63.129.16/32 169.254.169.254/32
nginx passes WsgiDAV a fixed host name and the real client address from the connection, never values a client sends. A WebDAV Destination header (used by copy and move) is rewritten to the configured host only when it names one of this server's own names, so copy and move work whichever of them a client used, while a destination on any other host is refused:
grep -E 'proxy_set_header (Host|X-Forwarded-For|X-Real-IP|Destination)' /etc/nginx/sites-available/cloudimg-wsgidav
proxy_set_header Host $wsgidav_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Destination $cloudimg_wsgidav_destination;
No per instance password reached a log, the journal, shell history, backups, /etc or the share:
sudo wsgidav-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, /tmp or the share; the password database holds a bcrypt hash only
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, copy and move rules and the URL in /root/wsgidav-credentials.txt, and keeps the name across reboots:
sudo wsgidav-set-hostname <your-domain>
To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running wsgidav-set-hostname again replaces it with a self signed one):
sudo install -m 0644 <your-domain>.crt /etc/ssl/wsgidav/wsgidav.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/wsgidav/wsgidav.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 wsgidav-set-hostname; a request for a name the instance does not serve is dropped.
Step 11: Passwords and accounts
Set a new password for webdav (12 to 72 characters). The tool prompts twice without echoing, writes a fresh bcrypt hash and restarts WsgiDAV; the password never appears on a command line, in the logs or in your history:
sudo wsgidav-set-password webdav
Then update or remove WEBDAV_PASSWORD in /root/wsgidav-credentials.txt so it does not hold an old password. The same tool adds accounts: give it a new name and it is added to /etc/wsgidav/htpasswd.
sudo wsgidav-set-password alice
Every account sees the same share with read and write access. To publish more than one directory, add entries under provider_mapping in /etc/wsgidav/wsgidav.yaml, add the directory to ReadWritePaths with sudo systemctl edit wsgidav, and restart.
Step 12: Configuration
WsgiDAV reads /etc/wsgidav/wsgidav.yaml. The image sets:
| Setting | Meaning | Image default |
|---|---|---|
host / port |
Bind address | 127.0.0.1 / 8080 (nginx fronts it) |
provider_mapping |
Directory served | / → /srv/webdav, read-write |
fs_dav_provider.follow_symlinks |
Follow symbolic links in the share | false |
http_authenticator.domain_controller |
Account store | HtpasswdDomainController (/etc/wsgidav/htpasswd, bcrypt) |
accept_basic / accept_digest |
Authentication methods | Basic only (over HTTPS); digest off |
cors.allow_origin |
Cross-origin access | null (off) |
dir_browser |
Browsable web view | on, login required |
property_manager / lock_storage |
Dead properties and locks | in memory |
verbose |
Log level | 2 (warnings and errors) |
After a change, run sudo systemctl restart wsgidav. Locks and custom properties are kept in memory, so a restart releases locks and clears custom properties (file contents are unaffected). nginx streams uploads with no size cap (client_max_body_size 0 in /etc/nginx/sites-available/cloudimg-wsgidav); set a value there and reload nginx if you want one.
Step 13: Server components
| Component | Version | Path |
|---|---|---|
| WsgiDAV | 4.3.5 | /opt/wsgidav/venv (wsgidav --version) |
| Cheroot | 11.1.2 | /opt/wsgidav/venv |
| Python | 3.12.3 | /opt/wsgidav/venv/bin/python |
| nginx | 1.24.0 | /etc/nginx/sites-available/cloudimg-wsgidav |
| Ubuntu | 24.04 LTS |
| Path | Purpose |
|---|---|
/srv/webdav |
The share (0750 wsgidav:wsgidav), ships empty |
/etc/wsgidav/wsgidav.yaml |
Configuration |
/etc/wsgidav/htpasswd |
Accounts (bcrypt hashes) |
/etc/ssl/wsgidav/ |
Certificate and key |
/root/wsgidav-credentials.txt |
URL, user and password (mode 0600) |
/usr/local/sbin/wsgidav-firstboot.sh |
First boot |
/usr/local/sbin/wsgidav-set-hostname |
Host name and certificate |
/usr/local/sbin/wsgidav-set-password |
Passwords and accounts |
/usr/local/sbin/wsgidav-verify-webdav.sh |
End to end check (Step 8) |
/usr/local/sbin/wsgidav-secret-leak-check.sh |
Proves no password reached a log |
/opt/wsgidav/venv/bin/wsgidav --version
sudo journalctl -u wsgidav-firstboot --no-pager -o cat | grep -E 'Serving as|First boot complete'
WsgiDAV logs warnings and errors only, so file names are not copied into the journal on every request; nginx's /var/log/nginx/access.log is the access record. Raise verbose to 3 in the configuration while you troubleshoot a client, then set it back.
Step 14: Back up the share
Archive the share into a root only directory, and never copy /etc/wsgidav or the credentials file into a backup location:
sudo install -d -o root -g root -m 0700 /var/backups/wsgidav
sudo bash -c 'umask 077; tar -czf /var/backups/wsgidav/webdav-$(date -u +%Y%m%d).tar.gz -C /srv/webdav .'
sudo ls /var/backups/wsgidav/
sudo wsgidav-secret-leak-check.sh
Copy the archive off the instance (for example to Amazon S3), or snapshot the root volume. To restore, extract the archive into /srv/webdav and run sudo chown -R wsgidav:wsgidav /srv/webdav.
Troubleshooting
- Every request returns 401, even with the right password. The accounts are read when WsgiDAV starts. If you edited
/etc/wsgidav/htpasswdby hand, runsudo systemctl restart wsgidav, or usewsgidav-set-password, which restarts it for you. - 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 withwsgidav-set-hostname(Step 10). - Copy or move returns 502. The destination names a different server. Copy and move work within this share, using any of this server's names.
- Windows will not map the drive. Windows requires a certificate it trusts and a trailing slash on the address; install your own certificate (Step 10) or import the instance's certificate as trusted.
- A symbolic link in the share returns 403. Links are not followed, so a link cannot reach files outside the share.
- Nothing answers after launch. Check
systemctl status wsgidav-firstbootandjournalctl -u wsgidav-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 per instance password secret, change it if it may have been shared, and give each person their own account (Step 11).
- Use your own DNS name and a certificate your clients trust (Step 10).
- Treat the share as shared content: anyone with an account can place files another user will open. nginx sends
X-Content-Type-Options: nosniff, so browsers do not reinterpret 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 share (Step 14).
Support and licensing
WsgiDAV is open source under the MIT licence; there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support. WsgiDAV is a trademark of its respective owners; cloudimg is not affiliated with or endorsed by the WsgiDAV project.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA