◐ theme

A self-hosted “cloud drive” in one Rust binary

How commit 20bc1ae stands up dufs behind the existing Caddy/DuckDNS stack, auto-syncs the KeePassXC vault over WebDAV, and wraps the one secret it needs in six layers of paranoia.

Contents
  1. Background — the pieces already on the machine
  2. Intuition — what changed and why
  3. Code — a walkthrough by concern
  4. Playground — the serve-dir safety guard
  5. Quiz — check your understanding

1 · Background

This PC already runs a small fleet of self-hosted services, and they all share one exposure pattern. Understanding that pattern is 80% of understanding this change.

The exposure stack (already in place)

The two things this change adds a home for

dufs is the chosen tool: a single ~2.5 MB Rust binary that serves a real filesystem folder over HTTP with a web UI and built-in WebDAV. Because it serves the folder directly, an rsync (or any local write) into that folder shows up instantly — no database, no re-scan step. Contrast Seafile, which stores files in an internal block database that rsync cannot write into.

2 · Intuition

Nothing here reinvents exposure. dufs slots into the existing Caddy pattern exactly like Gitea did — a new loopback app, a new Caddy block, a new DuckDNS label:

📱 Phoneon cellular
→
Caddy:443 HTTPS
kuhy-cloud.duckdns.org
→
dufs127.0.0.1:5000
auth required
→
~/cloudreal folder

Only Caddy is public. dufs is unreachable except through it, and it refuses every request without the right login.

The vault, kept fresh automatically

KeePassXC keeps writing Passwords.kdbx where it always has. A systemd path unit watches that one file; the instant it changes, a oneshot service rsyncs it into the served folder. No polling loop, no cron — the kernel wakes the unit only on a real write.

KeePassXCsaves the vault
→
.path unitPathModified fires
→
rsynconly the .kdbx
→
~/data/cloud/Keepassphone pulls via WebDAV

On the phone, KeePassDX opens the database straight from the WebDAV URL — so the “mobile app” requirement is met with zero custom code.

The paranoid part: one secret, six fences around it

To keep re-runs non-interactive, the installer caches the web password in .dufs_cloud.conf. That plaintext file must never reach the repo or the served folder. The change surrounds it with defense-in-depth so that a single mistake can’t leak it:

1
Gitignored three waysrepo root, linux_configuration/, and a **/ glob
2
Pre-commit backstopa hook inspects the staged set and blocks the file even after git add -f
3
Runtime refusalthe installer runs git check-ignore first and aborts rather than write the secret to a non-ignored path
4
No argv leakthe password is hashed via openssl passwd -6 -stdin, never passed as a command argument visible in ps
5
Serve-dir guarddufs refuses to serve $HOME, .ssh, .config, .gnupg, the repo, or /
6
Minimal exposureonly Passwords.kdbx is mirrored — the local timestamped .backup_* vaults stay off the public server

3 · Code

Nine files, grouped by what they’re for.

installer The idempotent flow

setup_dufs_cloud.sh runs as the normal user and uses sudo only where it must (binary install, systemd, firewall). Its main reads like the plan:

# pinned + checksum-verified — no "curl | sh" trust
readonly DUFS_VERSION="0.46.0"
readonly DUFS_SHA256="817769f7…410977"
…
main() {
  preflight            # deps + require the Caddy/DuckDNS stack to already exist
  load_config; prompt_config; save_config
  install_dufs         # download, verify sha256, install -m0755
  create_serve_dir
  write_dufs_config    # hashed auth, bind 127.0.0.1, no anonymous
  install_units        # render placeholders → /etc/systemd/system
  configure_caddy      # append a block, reload gitea-caddy
  configure_duckdns    # extend the EXISTING updater, no new cron
  ensure_firewall      # allow-web only if 80/443 not already open
  setup_autosync       # initial mirror + enable the .path watcher
}

reuse Slotting into the existing stack

The change deliberately edits shared infrastructure in place rather than duplicating it. The Caddy block is appended with an idempotency guard and the proxy is reloaded live:

if grep -qE "^${DUFS_SUBDOMAIN} \{" "$CADDYFILE"; then … # already present
else printf '\n%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "$DUFS_SUBDOMAIN" "$DUFS_PORT" >> "$CADDYFILE"; fi
docker exec gitea-caddy caddy reload --config /etc/caddy/Caddyfile

DuckDNS gets the new label spliced into the one running updater’s domains= list (token untouched) — never a second cron:

sed -i -E "s|(domains=)([^&\"']*)|\1\2,${label}|" "$DUCKDNS_UPDATER"

systemd Event-driven sync, not polling

Three tiny unit templates carry placeholders (__USER__, __HOME__, __CLOUD_ROOT__) that the installer substitutes. The watcher is the interesting one:

# keepass-cloud-sync.path
[Path]
PathModified=__HOME__/Keepass/Passwords.kdbx
Unit=keepass-cloud-sync.service

# keepass-cloud-sync.service (oneshot) — only the vault file
ExecStart=/usr/bin/rsync -a __HOME__/Keepass/Passwords.kdbx __CLOUD_ROOT__/Keepass/

hardening The runtime secret guards

Two functions make leaking the password structurally hard. First, refuse to persist it unless git is certain it’s ignored:

assert_config_gitignored() {
  [[ -n $REPO_ROOT ]] || die "Not in a git repo — refusing to write the secret."
  if ! git -C "$REPO_ROOT" check-ignore -q "$CONFIG_FILE"; then
    die "Refusing to write $CONFIG_FILE: it is NOT gitignored."
  fi
}

Second, never let dufs serve a directory that would expose secrets (this is the logic you can play with below):

case "$resolved" in
  "$home_r" | / | "$home_r/.ssh"* | "$home_r/.config"* | "$home_r/.gnupg"*)
    die "Unsafe cloud folder — it would expose sensitive files." ;;
esac
if [[ $resolved == "$REPO_ROOT"* ]]; then die "…inside the git repo"; fi

hook Blocking the file even against git add -f

check_no_dufs_conf.sh reads the staged set directly, so a force-added conf is caught regardless of any file filter:

staged="$(git diff --cached --name-only --diff-filter=ACM)"
bad="$(printf '%s\n' "$staged" | grep -E '(^|/)\.dufs_cloud\.conf$' || true)"
[ -n "$bad" ] && { echo "ERROR: refusing to commit the dufs secret config" >&2; exit 1; }

4 · Playground — the serve-dir guard

Type a candidate CLOUD_ROOT and see the exact verdict assert_cloud_root_safe would give. (Assumes $HOME=/home/kuhy, repo /home/kuhy/testsAndMisc. Paths are resolved like readlink -m, so .. collapses.)

~/cloud ~ (home) ~/.ssh ~/.config/dufs inside the repo traversal → .gnupg /srv/cloud

5 · Quiz

Score: 0 / 0