Deployment guide

WsgiDAV 4.3.5

Overview

WsgiDAV is a generic, extensible WebDAV server written in Python. It publishes a directory as a WebDAV share that you can mount 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.

This guide covers launching and using the WsgiDAV 4.3.5 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud SFTP and file server listing on AWS Marketplace, and you pick it as the software version when you subscribe and launch.

Upstream's quick start serves the current directory anonymously and writeably on every interface. The SecureAnyCloud image does the opposite. 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. Every request needs a login, including the web view and its style sheets, and 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 loopback 127.0.0.1:8080 behind 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, and 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.

The image contains 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
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, pick your Region, then choose Launch through EC2.

Choose m5.large, your key pair, and a security group as described above. WsgiDAV needs little CPU, so size the instance for your users. Size the root EBS volume for the files you plan to store, because the share lives on the root volume.

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=wsgidav}]'
2

Connect to your instance

The SSH user for WsgiDAV 4.3.5 on Ubuntu 24.04 is ubuntu. Connect with your key pair:

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

Check the services

First boot generates the password and the certificate, starts WsgiDAV and opens nginx. It then proves a full WebDAV round trip before it finishes, which takes well under a minute. Check the units and the first-boot marker:

systemctl is-active wsgidav nginx wsgidav-firstboot
sudo test -f /var/lib/sac/data/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",...))
4

Get your URL and password

Read the credentials file:

sudo cat /root/wsgidav-credentials.txt

The file is readable by root only. It 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, 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 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. Alternatively, 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 it names the signed-in user and access level. The share ships empty, and the files in the screenshots were uploaded for illustration.

Open a folder to browse into it, and open a file to read it.

The web view is for browsing and downloading. To upload, create folders, rename, move or delete, mount the share (Step 6) or use any WebDAV client.

6

Mount the share from your desktop

Run these steps 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. Use either a certificate from your own certificate authority (Step 10) or the instance's own certificate imported as trusted. To compare with what your client shows, print the certificate fingerprint:

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:
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 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 sac" > /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 sac <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. The -u webdav 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 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 with sudo chown -R wsgidav:wsgidav /srv/webdav.

8

Run the built-in WebDAV check

The image ships an end-to-end check that you can run at any time, and first boot runs it too. The check reads the credentials file itself. It 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

The check leaves the share exactly as it found it.

9

Security checks

The server stores only a bcrypt hash of the password, and the files that matter are locked down. Check the hash and the file permissions:

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 = 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

WsgiDAV runs unprivileged in a sandbox that can write only to the share, and neither WsgiDAV nor nginx can reach the instance metadata service. Confirm the sandbox settings:

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 that 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. Copy and move therefore work whichever of those names a client used, and 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/sac-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 $sac_wsgidav_destination;

Confirm that 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
10

Use 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 and updates the nginx host, copy and move rules. It also updates 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 certificate:

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

To place the instance 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 wsgidav-set-hostname, because a request for a name the instance does not serve is dropped.

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 that it does not hold an old password.

The same tool adds accounts. Give it a new name, and it adds the account 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, then restart the service.

12

Configuration

WsgiDAV reads /etc/wsgidav/wsgidav.yaml. The image sets these values:

  • host / port (bind address): 127.0.0.1 / 8080, with nginx in front
  • provider_mapping (directory served): / maps to /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/sac-wsgidav). To set a cap, set a value there and reload nginx.

13

Server components

The installed components are:

  • WsgiDAV 4.3.5, in /opt/wsgidav/venv (wsgidav --version)
  • Cheroot 11.1.2, in /opt/wsgidav/venv
  • Python 3.12.3, at /opt/wsgidav/venv/bin/python
  • nginx 1.24.0, configured in /etc/nginx/sites-available/sac-wsgidav
  • Ubuntu 24.04 LTS

The important paths are:

  • /srv/webdav: the share (0750 wsgidav:wsgidav), which 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

Check the installed version and the first-boot 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. To troubleshoot a client, raise verbose to 3 in the configuration, then set it back.

14

Back up the share

Archive the share into a root-only directory. 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/htpasswd by hand, run sudo systemctl restart wsgidav, or use wsgidav-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 or the instance's IP addresses, or set your own name with wsgidav-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 that 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-firstboot and journalctl -u wsgidav-firstboot. The share is deliberately closed until first boot completes.

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 that your clients trust (Step 10).
  • Treat the share as shared content, because anyone with an account can place files that 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 upgrade applies everything else.
  • Back up the share (Step 14).

Support and licensing

WsgiDAV is open source under the MIT licence, and there is no per-user or per-CPU licence fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.

WsgiDAV is a trademark of its respective owners. SecureAnyCloud is not affiliated with or endorsed by the WsgiDAV project.

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

Deploy on AWS

Launch WsgiDAV 4.3.5 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