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.
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.
caddy:2.8 container (gitea-caddy) is the only thing bound to public ports 80/443. It terminates TLS and gets Let’s Encrypt certificates automatically.kuhy.duckdns.org pointed at the home IP. One account/token, and you can add more subdomain labels to it.setup_wireguard_ssh.sh allow-web.forward chain is default-drop, which silently kills Docker bridge networking. Every public container therefore runs with network_mode: host and binds its app to 127.0.0.1. Gitea is the reference implementation.~/data/Keepass/Passwords.kdbx. KeePass has no built-in cloud sync by design — you choose where the file lives.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.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:
Only Caddy is public. dufs is unreachable except through it, and it refuses every request without the right login.
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.
On the phone, KeePassDX opens the database straight from the WebDAV URL — so the “mobile app” requirement is met with zero custom code.
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:
linux_configuration/, and a **/ globgit add -fgit check-ignore first and aborts rather than write the secret to a non-ignored pathopenssl passwd -6 -stdin, never passed as a command argument visible in ps$HOME, .ssh, .config, .gnupg, the repo, or /Passwords.kdbx is mirrored — the local timestamped .backup_* vaults stay off the public serverNine files, grouped by what they’re for.
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 }
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"
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/
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
git add -fcheck_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; }
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.)