WsgiDAV
Overview
WsgiDAV is a generic and extensible WebDAV server written in Python. It publishes a directory on your server as a WebDAV share, so you can open, edit and save files straight from the file manager your team already uses. You need no client software and no proprietary sync agent. It also serves a browsable web view, so you can open and read the same share in an ordinary web browser.
WsgiDAV implements WebDAV class 2. This means it supports resource locking, the usual create, read, update, delete, copy and move operations, and custom properties on files and folders.
This SecureAnyCloud image runs WsgiDAV 4.3.5, a pinned upstream release, inside a dedicated Python virtual environment. It runs under a purpose-made unprivileged system account, is served by the Cheroot production WSGI server and sits behind an nginx reverse proxy. WsgiDAV is bound to loopback 127.0.0.1:8080 and is never exposed directly. nginx on port 80 is the only way in, plus an unauthenticated /healthz endpoint that returns the constant string ok for load balancer probes.
The image ships closed by design. The quick start of upstream is designed for local experimentation and serves a directory anonymously and writeably on every interface. This image does the opposite. Authentication is mandatory on every route, including the browsable web view and its own static assets. There is no anonymous access and no anonymous read-only fallback. Passwords are held only as bcrypt hashes, never as plain text in a configuration file. A dedicated directory, /srv/webdav, is published rather than the whole filesystem or a home directory, and the service account can write to that directory and nothing else.
No credential of any kind ships in the image, and nothing is listening on the public port in the image at all. There is no window in which a freshly launched VM is reachable with a password that somebody else could know. On the first boot of every VM, a unique WebDAV password is generated and written to a root-only file, and only then is the public listener brought up. 24/7 SecureAnyCloud support backs the image.
What you get:
- WsgiDAV 4.3.5 installed in a dedicated Python virtual environment and running as the
wsgidavsystemd service - Cheroot 11.1.2 as the production WSGI server, bound to
127.0.0.1:8080 - nginx 1.24 as the reverse proxy on port 80, with streamed uploads and downloads and no request size cap
- An empty share at
/srv/webdav, owned0750by the unprivilegedwsgidavsystem account - Password authentication on every route, backed by a bcrypt password database at
/etc/wsgidav/htpasswd - A unique WebDAV password generated on the first boot of every VM
wsgidav-set-password, a tool that changes the password without it ever reaching your shell history or the system logswsgidav-verify-webdav.sh, a built-in end-to-end WebDAV check you can run at any time- Ubuntu 24.04 LTS, fully patched at build time with unattended security updates enabled
Key facts
Item Value
Platform Microsoft Azure
Default SSH user azureuser
WebDAV and web view URL http://<vm-ip>/
Served directory /srv/webdav
WebDAV account webdav
Credentials file /root/wsgidav-credentials.txt (0600 root:root)
Health probe http://<vm-ip>/healthz (unauthenticated, returns ok)
Recommended VM size Standard_B2s (2 vCPU, 4 GB)
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair for the
azureuseraccount - A network security group that allows inbound TCP 22 for your management address, and TCP 80 (and 443 once you add TLS) for the addresses that need the share
- Recommended VM size:
Standard_B2s. Increase the disk size at deployment time if you intend to store a lot of data in the share
Step 1 - Deploy from the Azure Marketplace
- In the Azure portal, choose Create a resource and search for WsgiDAV WebDAV Server on Ubuntu 24.04 LTS.
- Choose the SecureAnyCloud offer and select Create.
- Pick your subscription, resource group and region.
- Set the VM size to
Standard_B2sor larger. - Set the authentication type to SSH public key and the username to
azureuser. - On the Disks tab, increase the OS disk size if the share will hold a lot of data.
- On the Networking tab, allow inbound SSH (22) and HTTP (80), restricted to the addresses that need them.
- Select Review + create, then Create.
Step 2 - Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:wsgidav-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name my-wsgidav \
--image sac1647283583153:wsgidav-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardOpen the ports you need. Replace the source prefix with your own address range, so the share is not open to the whole internet:
az network nsg rule create \
--resource-group my-resource-group \
--nsg-name my-wsgidavNSG \
--name allow-webdav \
--priority 1010 \
--destination-port-ranges 80 \
--source-address-prefixes <your-mgmt-cidr> \
--access Allow --protocol TcpStep 3 - Connect to your VM
Find the public address:
az vm show -d -g my-resource-group -n my-wsgidav --query publicIps -o tsvConnect over SSH:
ssh azureuser@<vm-ip>Step 4 - Confirm the services are running
systemctl status wsgidav.service nginx.service --no-pager --lines=0Both units should report active (running).
Next, check the load balancer health probe. It is the one route that does not require a password:
curl -s http://127.0.0.1/healthzokStep 5 - Retrieve your per-VM WebDAV password
Every VM generates its own password on first boot and writes it to a root-only file. Read the file with:
sudo cat /root/wsgidav-credentials.txtThe file gives you the URL, the account name webdav and the password that is unique to this VM.
If the file still says placeholder, rotated at first boot, first boot has not finished yet. Wait a moment and read the file again. journalctl -u wsgidav-firstboot shows what first boot is doing.
Now create a curl credentials file, so that none of the commands below puts your password into the process list or your shell history. The command reads the password out of the credentials file, so the password is never typed and never becomes an argument:
sudo bash -c 'umask 077; printf "user = \"webdav:%s\"\n" "$(grep "^WEBDAV_PASSWORD=" /root/wsgidav-credentials.txt | cut -d= -f2-)" > /root/.wsgidav-curl'
sudo ls -l /root/.wsgidav-curl-rw------- 1 root root 42 Aug 7 20:15 /root/.wsgidav-curlStep 6 - Confirm the share is closed to anonymous callers
Before using the share, prove that it is shut. An anonymous request must be refused:
curl -s -o /dev/null -w 'anonymous GET -> HTTP %{http_code}\n' http://127.0.0.1/
curl -s -o /dev/null -w 'anonymous PROPFIND -> HTTP %{http_code}\n' -X PROPFIND http://127.0.0.1/
curl -s -o /dev/null -w 'guessed password -> HTTP %{http_code}\n' -u webdav:webdav http://127.0.0.1/anonymous GET -> HTTP 401 anonymous PROPFIND -> HTTP 401 guessed password -> HTTP 401WsgiDAV itself listens only on loopback, and nginx is the only routable listener:
sudo ss -tlnp | grep -E ':8080|:80 ' ls -ld /srv/webdavStep 7 - Open the browsable web view
In a web browser, go to http://<vm-ip>/. Because authentication is required on every route, an unauthenticated visitor is refused.
Enter the account webdav and the password from Step 5. The directory listing appears and shows the authenticated user and whether the share is read-write.
A newly launched VM starts with an empty share. The folders and files in these screenshots are SecureAnyCloud sample content that illustrates the view, and your share begins empty.
Open a folder to see its contents. Select a file, and WsgiDAV serves it straight out of the share.
Step 8 - Upload a file and read it back
This round trip is the test that matters. Write a file over WebDAV, read it back and confirm that the bytes are identical.
echo "sac WebDAV round trip $(date -u +%Y-%m-%dT%H:%M:%SZ)" | sudo tee /tmp/webdav-demo.txt >/dev/null
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'PUT -> HTTP %{http_code}\n' -T /tmp/webdav-demo.txt http://127.0.0.1/webdav-demo.txt
sudo curl -s -K /root/.wsgidav-curl -o /tmp/webdav-demo.back -w 'GET -> HTTP %{http_code}\n' http://127.0.0.1/webdav-demo.txt
sudo cmp /tmp/webdav-demo.txt /tmp/webdav-demo.back && echo "byte compare -> identical"PUT -> HTTP 201 GET -> HTTP 200 byte compare -> identicalCreate a folder, copy the file into it and list it with a real WebDAV PROPFIND:
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'MKCOL -> HTTP %{http_code}\n' -X MKCOL http://127.0.0.1/demo-folder/
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'COPY -> HTTP %{http_code}\n' -X COPY -H "Destination: http://127.0.0.1/demo-folder/webdav-demo.txt" http://127.0.0.1/webdav-demo.txt
sudo curl -s -K /root/.wsgidav-curl -X PROPFIND -H 'Depth: 1' http://127.0.0.1/demo-folder/ | grep -oE '<[a-z0-9]+:href>[^<]*</[a-z0-9]+:href>' | head -5Then clean up, and confirm that the deletion took effect:
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'DELETE folder -> HTTP %{http_code}\n' -X DELETE http://127.0.0.1/demo-folder/
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'DELETE file -> HTTP %{http_code}\n' -X DELETE http://127.0.0.1/webdav-demo.txt
sudo curl -s -K /root/.wsgidav-curl -o /dev/null -w 'GET deleted -> HTTP %{http_code}\n' http://127.0.0.1/webdav-demo.txt
sudo rm -f /tmp/webdav-demo.txt /tmp/webdav-demo.backDELETE folder -> HTTP 204 DELETE file -> HTTP 204 GET deleted -> HTTP 404Step 9 - Run the built-in WebDAV verification
The image ships an end-to-end check that you can run at any time. It reads the credentials file itself, so you type no password:
sudo bash /usr/local/sbin/wsgidav-verify-webdav.shIt refuses anonymous access on every verb, rejects a list of guessed passwords, then performs a full authenticated round trip: PROPFIND, PUT of a 256 KiB payload, GET with an SHA256 byte comparison, MKCOL, COPY, MOVE, LOCK and UNLOCK, PROPPATCH, and DELETE followed by a confirmation that the resource is really gone. It leaves the share exactly as it found it.
Step 10 - Mount the share from your desktop
Run these commands on your own computer, not on the VM. Use the account webdav and the password from Step 5.
Windows File Explorer
In File Explorer choose This PC, then Map network drive. Pick a drive letter and enter the address: http://<vm-ip>/ Tick "Connect using different credentials" and sign in as webdav.Windows maps a WebDAV drive over plain HTTP only if you allow it, and doing so is a poor idea on an untrusted network. Add TLS first (Step 12) and map https://<your-domain>/ instead.
macOS Finder
In Finder choose Go, then Connect to Server (Command K). Enter: http://<vm-ip>/ Sign in as webdav with the password from Step 5.Linux (GNOME Files)
In Files choose Other Locations, then enter in "Connect to Server": dav://<vm-ip>/Linux (command line, davfs2)
sudo apt-get install -y davfs2
sudo mkdir -p /mnt/webdav
sudo mount -t davfs http://<vm-ip>/ /mnt/webdavrclone
rclone config create myshare webdav url=http://<vm-ip>/ vendor=other user=webdav rclone ls myshare:Step 11 - Change the WebDAV password
The image ships a tool that changes the password without it ever reaching your shell history, the process list or the system logs. It prompts for the new password, writes a fresh bcrypt hash and restarts the server so that the change takes effect:
sudo wsgidav-set-password webdavThe same tool creates additional accounts. Give it a name that does not exist yet, and it adds the account to the password database:
sudo wsgidav-set-password aliceAll accounts see the same share. If you need separate shares with separate permissions, run one instance per share. Alternatively, edit provider_mapping in /etc/wsgidav/wsgidav.yaml to publish more than one directory, then restart the service.
After any change to /etc/wsgidav/htpasswd, restart the server, because it reads the password database once at startup. wsgidav-set-password does that for you.
Step 12 - Put TLS in front before you expose the share
This matters. WebDAV Basic authentication sends your password with every request. The password is base64 encoded, which is not encryption. Over plain HTTP, anyone who can observe the traffic can read it. Do not use this share over an untrusted network until TLS terminates in front of it.
There are two supported options.
Option A: terminate TLS on an Azure Application Gateway or Azure Front Door. Point the backend pool at this VM on port 80, attach your certificate to the listener, and restrict the network security group so that only the gateway can reach port 80. This is the better option if you already run a gateway, because it keeps certificate renewal off the VM.
Option B: terminate TLS on the VM with a certificate from Let's Encrypt. You need a DNS name that points at the VM and inbound TCP 443 open. Run:
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain>certbot edits the nginx vhost at /etc/nginx/sites-available/sac-wsgidav, adds the port 443 listener and sets up automatic renewal. When TLS works, redirect plain HTTP by adding return 301 https://$host$request_uri; to the port 80 server block, and mount the share as https://<your-domain>/ everywhere.
Step 13 - Restrict the network to the addresses that need it
Do not leave port 80 open to 0.0.0.0/0. Limit it to your office range, your VPN, or your Application Gateway subnet:
az network nsg rule create \
--resource-group my-resource-group \
--nsg-name my-wsgidavNSG \
--name restrict-webdav \
--priority 1005 \
--destination-port-ranges 80 443 \
--source-address-prefixes <your-mgmt-cidr> \
--access Allow --protocol TcpServer components
Component Version Purpose
WsgiDAV 4.3.5 The WebDAV server and browsable directory view
Cheroot 11.1.2 Production WSGI server hosting WsgiDAV
nginx 1.24.0 Reverse proxy on port 80, the only routable listener
Python 3.12.3 Runtime, in a dedicated virtual environment
Ubuntu 24.04 LTS Operating system
Filesystem layout
Path Size Purpose
/ 29 GB Root filesystem
/boot 881 MB Operating system kernel files
/boot/efi 105 MB UEFI boot partition (Gen2 Hyper V)
/mnt 8 GB Azure temporary resource disk (not persistent)
Key directories
Path Purpose
/srv/webdav The served share, owned 0750 wsgidav:wsgidav. Ships empty
/opt/wsgidav/venv Python virtual environment holding WsgiDAV and its dependencies
/etc/wsgidav/wsgidav.yaml Server configuration, 0640 root:wsgidav
/etc/wsgidav/htpasswd bcrypt password database, 0640 root:wsgidav
/etc/nginx/sites-available/sac-wsgidav The nginx vhost
/root/wsgidav-credentials.txt Per-VM credentials, 0600 root:root
Managing the services
systemctl status wsgidav --no-pager --lines=0sudo systemctl restart wsgidav
sudo systemctl restart nginxFollow the server log:
sudo journalctl -u wsgidav -fThe server logs warnings and errors only, so customer file names are not copied into the persistent system journal on every request. The nginx access log at /var/log/nginx/access.log is the record of who fetched what. To get per-request detail from WsgiDAV itself when you diagnose a client problem, raise verbose from 2 to 3 in /etc/wsgidav/wsgidav.yaml and restart the service. Then set it back.
Scripts and log files
Path Purpose
/usr/local/sbin/wsgidav-firstboot.sh First boot: generates the per-VM password, opens the public listener
/usr/local/sbin/wsgidav-set-password Changes or adds an account password, without it reaching logs or history
/usr/local/sbin/wsgidav-verify-webdav.sh End-to-end WebDAV check (Step 9)
/usr/local/sbin/wsgidav-secret-leak-check.sh Proves no per-VM password reached the journal, auth log, backups or history
/var/lib/sac/data/wsgidav-firstboot.done First-boot sentinel; its presence stops first boot re-running
journalctl -u wsgidav Server log
journalctl -u wsgidav-firstboot First-boot log
/var/log/nginx/access.log, /var/log/nginx/error.log nginx logs
On startup
wsgidav-firstboot.service runs once, on the first boot of each VM, gated on the sentinel above. It resolves the public address of the VM, generates a unique WebDAV password, writes it into the bcrypt password database, starts WsgiDAV on loopback, and only then enables the public nginx vhost. It finishes by proving that a full WebDAV round trip works and that no password leaked into any log, and then writes the sentinel.
Until that unit has completed, nothing is listening on port 80 and there is no password database at all, so the wsgidav service cannot start either. This is deliberate: a newly launched VM is never reachable with a credential that anybody else could know. On later boots, the unit is skipped and the normal service units start the server.
Backups
Back up the share. Send the archive to a root-only directory. Never copy /etc/wsgidav or the credentials file into a backup location, because they hold the password database and the plain-text password:
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 -l /var/backups/wsgidav/Confirm that the backup carried no secret into /var/backups:
sudo bash /usr/local/sbin/wsgidav-secret-leak-check.shLEAKCHECK_OK no per-VM secret found in the journal, auth.log, /var/backups, shell history, MOTD, any system log, /etc or the share; the password database holds a bcrypt hash onlyCopy the archive off the VM to your own backup storage. To restore, extract it back into /srv/webdav and reset ownership with sudo chown -R wsgidav:wsgidav /srv/webdav.
Troubleshooting
Every request returns 401, including the correct password. The server reads the password database once when it starts. If you edited /etc/wsgidav/htpasswd by hand, restart the service with sudo systemctl restart wsgidav. Use sudo wsgidav-set-password webdav instead, which restarts the service for you.
Every request returns 502. nginx is running but WsgiDAV is not. Check systemctl status wsgidav. If it reports condition failed, the password database at /etc/wsgidav/htpasswd is missing, which is the state the image ships in. This means first boot did not complete. Look at journalctl -u wsgidav-firstboot.
The credentials file still shows a temporary password. First boot has not finished. It resolves the VM address, generates the password and proves a full WebDAV round trip before it writes the file. It therefore takes a few seconds longer than the time the VM needs to accept SSH connections.
Windows will not map the drive. Windows restricts Basic authentication over plain HTTP. Add TLS (Step 12) and map the https:// address. Windows also expects a trailing slash on the address.
Uploads of large files stop partway. nginx is configured with no request size cap and streams uploads straight through, so the usual cause is that the client times out or the disk fills. Check free space with df -h /srv/webdav.
A symlink in the share returns 403. This is deliberate. WsgiDAV does not follow symlinks, so a link inside the share cannot be used to read a file outside it.
Locks or custom properties disappear after a restart. The lock and property managers are held in memory on purpose, so no lock or property database can be captured into the image or left behind on disk. Locks are released and dead properties are lost when the service restarts. File contents are unaffected.
Security recommendations
- Terminate TLS in front of this VM before using it over any untrusted network. WebDAV Basic authentication sends the password with every request, base64 encoded but not encrypted. See Step 12.
- Restrict the network security group to the addresses that need the share. Do not leave port 80 open to the internet.
- Change the shipped per-VM password with
sudo wsgidav-set-password webdavand store it in your password manager. - Give each person their own account with
sudo wsgidav-set-password <name>rather than sharing one password, so that access can be withdrawn individually. - Treat the share as untrusted content. Anyone who can write to it can place a file that another user will open. nginx sends
X-Content-Type-Options: nosniffso that browsers do not reinterpret an uploaded file as something executable. If you accept content from people you do not trust, serve it from a separate hostname to keep it out of the origin of the browse view. - Keep the credentials file where it is.
/root/wsgidav-credentials.txtis0600 root:root. Do not copy it into a backup, a home directory or a configuration management repository. - Leave unattended security updates enabled so the operating system continues to receive patches.
- Back up the share and copy the archive off the VM (see Backups).
Support
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.
WsgiDAV is open-source software distributed under the MIT licence. WsgiDAV is a trademark of its respective owners. SecureAnyCloud is not affiliated with, endorsed by or sponsored by the WsgiDAV project or its maintainers. This is a repackaged open-source software product with additional charges for SecureAnyCloud support services.
Deploy on Azure
Launch WsgiDAV WebDAV Server on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com