diff --git a/docs/explanations/2026-07-12-explanation-dufs-cloud.html b/docs/explanations/2026-07-12-explanation-dufs-cloud.html new file mode 100644 index 00000000..bf609924 --- /dev/null +++ b/docs/explanations/2026-07-12-explanation-dufs-cloud.html @@ -0,0 +1,402 @@ + + +Self-hosted dufs cloud + KeePass WebDAV sync — explained + + +
+ ◐ 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. +
  3. Intuition — what changed and why
  4. +
  5. Code — a walkthrough by concern
  6. +
  7. Playground — the serve-dir safety guard
  8. +
  9. Quiz — check your understanding
  10. +
+
+ +

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
+
→
+
~/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
+
+ + +
+ + diff --git a/docs/explanations/index.html b/docs/explanations/index.html index cbd3fc68..e4f4895f 100644 --- a/docs/explanations/index.html +++ b/docs/explanations/index.html @@ -30,8 +30,12 @@
explanations

Change explanations

-

1 explanation · interactive walkthroughs of notable changes, newest first.

+

2 explanations · interactive walkthroughs of notable changes, newest first.

+ + 2026-07-12 + Self-hosted dufs cloud + KeePass WebDAV sync — explained + 2026-07-11 Explaining: whitelist run-with-music apps + FOSS replacements