Ws
Storage Azure

WsgiDAV on Ubuntu 24.04 on Azure User Guide

| Product: WsgiDAV WebDAV Server on Ubuntu 24.04 LTS on Azure

Overview

WsgiDAV is a generic and extensible WebDAV server written in Python. It publishes a directory on your server as a WebDAV share, so files can be opened, edited and saved straight from the file manager your team already uses, with no client software to install and no proprietary sync agent. It implements WebDAV class 2, which means resource locking as well as the usual create, read, update, delete, copy and move operations, plus custom properties on files and folders. Alongside the protocol it serves a browsable web view, so the same share can be opened and read in an ordinary web browser.

This cloudimg image runs WsgiDAV 4.3.5 at a pinned upstream release inside a dedicated Python virtual environment, under a purpose made unprivileged system account, served by the Cheroot production WSGI server and reverse proxied behind nginx. 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 is deliberately shipped closed. Upstream's quick start is designed for local experimentation and serves a directory anonymously, 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, and 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, so 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, written to a root only file, and only then is the public listener brought up. Backed by 24/7 cloudimg support.

What is included:

  • WsgiDAV 4.3.5 installed in a dedicated Python virtual environment and running as the wsgidav systemd 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, owned 0750 by the unprivileged wsgidav system 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 logs
  • wsgidav-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

  1. An Azure subscription with permission to create virtual machines.
  2. An SSH key pair for the azureuser account.
  3. A network security group allowing inbound TCP 22 for your management address and TCP 80 (and 443 once you add TLS) for the addresses that need the share.
  4. 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

  1. In the Azure portal choose Create a resource and search for WsgiDAV WebDAV Server on Ubuntu 24.04 LTS.
  2. Choose the cloudimg offer and select Create.
  3. Pick your subscription, resource group and region.
  4. Set the VM size to Standard_B2s or larger.
  5. Set the authentication type to SSH public key and the username to azureuser.
  6. On the Disks tab, increase the OS disk size if the share will hold a lot of data.
  7. On the Networking tab allow inbound SSH (22) and HTTP (80), restricted to the addresses that need them.
  8. Select Review + create, then Create.

Step 2 - Deploy from the Azure CLI

az vm create \
  --resource-group my-resource-group \
  --name my-wsgidav \
  --image cloudimg:wsgidav-ubuntu-24-04:default:latest \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Open the ports you need. Replace the source prefix with your own address range rather than leaving the share 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 Tcp

Step 3 - Connect to your VM

Find the public address:

az vm show -d -g my-resource-group -n my-wsgidav --query publicIps -o tsv

Connect over SSH:

ssh azureuser@<vm-ip>

Step 4 - Confirm the services are running

systemctl status wsgidav.service nginx.service --no-pager --lines=0

Both units should report active (running).

Terminal showing systemctl status for the wsgidav and nginx services, both reporting active and running on the cloudimg Ubuntu 24.04 image.

Check the load balancer health probe, which is the one route that does not require a password:

curl -s http://127.0.0.1/healthz
ok

Step 5 - Retrieve your per VM WebDAV password

Every VM generates its own password on first boot and writes it to a root only file:

sudo cat /root/wsgidav-credentials.txt

The file gives you the URL, the account name webdav, and the password that is unique to this VM.

Terminal showing the per VM credentials file with the password redacted, followed by an anonymous request to the server being refused with HTTP 401 Not Authorized.

If the file still says placeholder, rotated at first boot, first boot has not finished yet. Give it a moment and look again; journalctl -u wsgidav-firstboot shows what it is doing.

Now create a curl credentials file so that none of the commands below ever put your password into the process list or your shell history. The password is read out of the credentials file inside the command, so it 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-curl

Step 6 - Confirm the share is closed to anonymous callers

Before using it, prove 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 401

WsgiDAV itself listens only on loopback, and nginx is the only routable listener:

sudo ss -tlnp | grep -E ':8080|:80 '
ls -ld /srv/webdav

Terminal showing WsgiDAV bound only to 127.0.0.1 port 8080 while nginx listens on port 80, the served share owned by the wsgidav account with mode 0750, and the configuration and bcrypt password database in /etc/wsgidav.

Step 7 - Open the browsable web view

Browse to http://<vm-ip>/ in a web browser. Because authentication is required on every route, an unauthenticated visitor is refused:

Browser showing the WsgiDAV 401 Access not authorized page returned to a visitor who supplies no credentials, demonstrating that authentication is enforced on every route including the browsable view.

Enter the account webdav and the password from Step 5 and the directory listing appears, showing the authenticated user and whether the share is read write:

Browser showing the WsgiDAV browsable directory index of the share, listing folders and a file with their type, size and last modified time, and confirming the authenticated user and read write access. The files shown are cloudimg demonstration content that is not present in the shipped image.

A newly launched VM starts with an empty share. The folders and files in these screenshots are cloudimg demonstration content used to illustrate the view; they are removed before the image is captured, so your share begins empty.

Open a folder to see its contents:

Browser showing the contents of a folder inside the WebDAV share, listing three documents with their file type, size in bytes and last modified timestamp. This is cloudimg demonstration content.

Select a file and it is served straight out of the share:

Browser displaying the full text of a document served out of the WebDAV share, showing that files are genuinely delivered to the client rather than merely listed. This is cloudimg demonstration content.

Step 8 - Upload a file and read it back

This is the round trip that matters: write a file over WebDAV, read it back and confirm the bytes are identical.

echo "cloudimg 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  -> identical

Create 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 -5

Then tidy up, and confirm the deletion really 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.back
DELETE folder -> HTTP 204
DELETE file   -> HTTP 204
GET deleted   -> HTTP 404

Step 9 - Run the built in WebDAV verification

The image ships an end to end check you can run at any time. It reads the credentials file itself, so no password is typed:

sudo bash /usr/local/sbin/wsgidav-verify-webdav.sh

It 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.

Terminal showing the built in WebDAV verification script reporting anonymous access refused, guessed credentials rejected, DAV class 1,2 advertised, and a full authenticated round trip of PROPFIND, PUT, GET with byte comparison, MKCOL, COPY, MOVE, LOCK and UNLOCK, PROPPATCH and DELETE all verified.

Step 10 - Mount the share from your desktop

These commands run 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 will only map a WebDAV drive over plain HTTP if you allow it, and it 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/webdav

rclone

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 the change takes effect:

sudo wsgidav-set-password webdav

The same tool creates additional accounts. Give it a name that does not exist yet and it is added to the password database:

sudo wsgidav-set-password alice

All accounts see the same share. If you need separate shares with separate permissions, run one instance per share, or edit provider_mapping in /etc/wsgidav/wsgidav.yaml to publish more than one directory and restart the service.

After any change to /etc/wsgidav/htpasswd the server must be restarted, because the password database is read once at start up. 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 single request. It is base64 encoded, which is not encryption. Over plain HTTP anyone able to 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 shapes.

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, and 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 pointing at the VM and inbound TCP 443 open.

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/cloudimg-wsgidav, adds the port 443 listener and sets up automatic renewal. Once 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 Tcp

Server 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/cloudimg-wsgidav The nginx vhost
/root/wsgidav-credentials.txt Per VM credentials, 0600 root:root

Managing the services

systemctl status wsgidav --no-pager --lines=0
sudo systemctl restart wsgidav
sudo systemctl restart nginx

Follow the server log:

sudo journalctl -u wsgidav -f

The server logs warnings and errors only, so that customer file names are not copied into the persistent system journal on every request. nginx's access log at /var/log/nginx/access.log is the record of who fetched what. If you are diagnosing a client problem and want per request detail from WsgiDAV itself, raise verbose from 2 to 3 in /etc/wsgidav/wsgidav.yaml and restart the service, then put 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/cloudimg/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 VM's public address, 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 a full WebDAV round trip works and that no password leaked into any log, and 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: it means 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, and never copy /etc/wsgidav or the credentials file into a backup location, because those 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 for yourself that the backup carried no secret into /var/backups:

sudo bash /usr/local/sbin/wsgidav-secret-leak-check.sh
LEAKCHECK_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 only

Copy 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 password database is read once when the server 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 it 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; that means first boot did not complete. Look at journalctl -u wsgidav-firstboot.

The credentials file still says placeholder. First boot has not finished. It resolves the VM address, generates the password and then proves a full WebDAV round trip before it writes the file, so it takes a few seconds longer than the VM taking 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 part way. nginx is configured with no request size cap and streams uploads straight through, so the usual cause is the client timing out or the disk filling. Check free space with df -h /srv/webdav.

A symlink in the share returns 403. That is deliberate. Symlinks are not followed, 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 that 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 of course unaffected.

Security Recommendations

  1. 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.
  2. Restrict the network security group to the addresses that need the share. Do not leave port 80 open to the internet.
  3. Change the shipped per VM password with sudo wsgidav-set-password webdav and store it in your password manager.
  4. Give each person their own account with sudo wsgidav-set-password <name> rather than sharing one password, so that access can be withdrawn individually.
  5. 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: nosniff so browsers will not reinterpret an uploaded file as something executable, but if you accept content from people you do not trust, serve it from a separate hostname to keep it out of the browse view's origin.
  6. Keep the credentials file where it is. /root/wsgidav-credentials.txt is 0600 root:root. Do not copy it into a backup, a home directory or a configuration management repository.
  7. Leave unattended security updates enabled so the operating system continues to receive patches.
  8. Back up the share and copy the archive off the VM (see Backups).

Support

cloudimg provides 24/7 support for this image. Contact support@cloudimg.co.uk.

WsgiDAV is open source software distributed under the MIT licence. WsgiDAV is a trademark of its respective owners; cloudimg 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 cloudimg support services.