mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:07:03 +01:00
linux_configuration: self-hosted dufs cloud storage + KeePass WebDAV auto-sync
Add setup_dufs_cloud.sh: a self-sustaining installer that stands up dufs (single Rust binary; web UI + built-in WebDAV) on 127.0.0.1:5000 behind the existing Caddy + DuckDNS exposure stack at a new subdomain, so files are reachable from mobile network over HTTPS. An event-driven systemd path unit mirrors ~/Keepass/Passwords.kdbx into the served folder on every change, so KeePassDX syncs the vault natively over WebDAV. Paranoid secret handling for the gitignored .dufs_cloud.conf (plaintext web password): ignored at repo-root + linux_configuration + **/ globs; a new check-no-dufs-conf pre-commit hook blocks it even against `git add -f`; the installer refuses to write it unless git check-ignore confirms it is ignored; the password is hashed via `openssl passwd -6 -stdin` (never in argv); and a cloud-root guard forbids serving home/.ssh/.config/.gnupg/the repo/root. Verified: dufs auth (401/200) + WebDAV (207) against the real v0.46.0 binary, idempotent Caddy/DuckDNS/unit-render logic, and a live phone-on-cellular test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0126QvdMrv2WNeUVShFiLTc3
This commit is contained in:
parent
402fc2c453
commit
20bc1aeb02
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,6 +3,10 @@
|
||||
# Sensitive — must NEVER be committed (contains regex of home GPS coordinates etc.)
|
||||
.secret-patterns
|
||||
|
||||
# dufs cloud secret config — holds a plaintext web password; never commit anywhere.
|
||||
**/.dufs_cloud.conf
|
||||
.dufs_cloud.conf
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
|
||||
@ -448,6 +448,13 @@ repos:
|
||||
language: script
|
||||
exclude: ^(\.secret-patterns|\.pre-commit-config\.yaml|.*\.geojson)$
|
||||
|
||||
- id: check-no-dufs-conf
|
||||
name: block dufs secret config (.dufs_cloud.conf)
|
||||
entry: meta/scripts/check_no_dufs_conf.sh
|
||||
language: script
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
# ===========================================================================
|
||||
# COMMITIZEN - Conventional commits (optional)
|
||||
# ===========================================================================
|
||||
|
||||
57
docs/superpowers/evidence/setup-dufs-cloud-2026-07-12.json
Normal file
57
docs/superpowers/evidence/setup-dufs-cloud-2026-07-12.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"intent": "Provide a self-hosted Google-Drive-style cloud on this Arch PC: a dufs file server (web UI + WebDAV) exposed publicly via the existing Caddy+DuckDNS stack, with the KeePassXC vault auto-mirrored into it so it is reachable and syncable from the phone on mobile network.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/single_use/features/setup_dufs_cloud.sh (new self-sustaining installer)",
|
||||
"linux_configuration/scripts/single_use/features/systemd/{dufs.service,keepass-cloud-sync.service,keepass-cloud-sync.path} (new units)",
|
||||
"linux_configuration/.gitignore (ignore .dufs_cloud.conf)",
|
||||
"Non-goals: no second Caddy/DuckDNS updater, no hand-edit of /etc/nftables.conf, no new mobile app (WebDAV covers KeePassDX)"
|
||||
],
|
||||
"changes": [
|
||||
"Installer runs as the normal user with targeted sudo: installs a pinned, sha256-verified dufs v0.46.0 binary, writes a 0600 dufs.yaml (SHA-512 hashed auth, no anonymous access, bound to 127.0.0.1:5000), and installs+enables a system dufs.service.",
|
||||
"Reuses the host exposure pattern from setup_gitea.sh: appends a marker-guarded reverse_proxy block for a new DuckDNS subdomain to the shared ~/gitea/Caddyfile and reloads gitea-caddy; extends the existing DuckDNS updater's domains= list (no new cron); opens 80/443 via setup_wireguard_ssh.sh allow-web only if not already open.",
|
||||
"KeePass auto-sync: an event-driven systemd .path unit watches ~/Keepass/Passwords.kdbx and rsyncs ONLY that vault file into the served folder on every change (local .backup_* vaults stay off the public server); an initial mirror runs at install time.",
|
||||
"Paranoid secret hardening: the plaintext password config is gitignored at 3 layers (repo root, linux_configuration/, **/ patterns); a new check-no-dufs-conf pre-commit hook blocks it even against `git add -f`; the installer refuses to write it unless `git check-ignore` confirms it is ignored; the password is hashed via openssl stdin (never in argv/ps); and a cloud-root guard forbids serving home/.ssh/.config/.gnupg/the repo/root."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash -n setup_dufs_cloud.sh && shellcheck -x --severity=warning setup_dufs_cloud.sh",
|
||||
"result": "pass",
|
||||
"evidence": "syntax OK; shellcheck clean (no findings)."
|
||||
},
|
||||
{
|
||||
"command": "dufs v0.46.0 config + WebDAV smoke test (scratchpad, real binary)",
|
||||
"result": "pass",
|
||||
"evidence": "Flat-string auth 'user:$6$hash@/:rw' loads. no-auth->401, bad-pass->401, good->200; WebDAV PROPFIND->207, GET file->200 (returned content). Confirms schema, hashed auth, no anonymous access, WebDAV works."
|
||||
},
|
||||
{
|
||||
"command": "Isolated idempotency/render tests (scratchpad copies)",
|
||||
"result": "pass",
|
||||
"evidence": "DuckDNS domains sed adds label once and is skipped on re-run with token preserved; Caddy block appended once (1 cloud block) with tab indentation; unit rendering substitutes __USER__/__HOME__/__CLOUD_ROOT__ with no leftover placeholders."
|
||||
},
|
||||
{
|
||||
"command": "systemd-analyze verify rendered-*.service rendered-*.path",
|
||||
"result": "pass",
|
||||
"evidence": "No structural errors; only benign 'dufs not executable yet' (binary installed at deploy time) and sandbox 'unknown user' notes."
|
||||
},
|
||||
{
|
||||
"command": "Paranoid secret guards (functional tests)",
|
||||
"result": "pass",
|
||||
"evidence": "git check-ignore confirms .dufs_cloud.conf ignored at all 3 locations; check_no_dufs_conf.sh blocks a `git add -f`ed conf (exit 1) and cleanup leaves nothing staged; cloud-root guard rejects HOME/.ssh/.config/.gnupg/repo/`/`/`../` traversal and allows dedicated folders; openssl passwd -6 -stdin yields a $6$ hash without argv exposure."
|
||||
},
|
||||
{
|
||||
"command": "End-to-end deploy + phone-on-cellular acceptance",
|
||||
"result": "pending",
|
||||
"evidence": "Requires interactive password + sudo, so run by the user via '! ./setup_dufs_cloud.sh', plus a one-time manual DuckDNS subdomain registration. To be confirmed before commit."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"Public file server: mitigated by mandatory auth (SHA-512 hashed, no anonymous), HTTPS via Caddy, dufs bound to 127.0.0.1 only; the kdbx is itself master-password-encrypted.",
|
||||
"Caddy block is appended to the shared Gitea Caddyfile; re-running setup_gitea.sh regenerates it and drops the block (re-run setup_dufs_cloud.sh to restore).",
|
||||
"External reachability depends on a one-time manual DuckDNS subdomain creation and the existing router 80/443 forward."
|
||||
],
|
||||
"rollback": [
|
||||
"sudo systemctl disable --now dufs.service keepass-cloud-sync.path; sudo rm /etc/systemd/system/{dufs.service,keepass-cloud-sync.service,keepass-cloud-sync.path}; sudo systemctl daemon-reload",
|
||||
"Remove the cloud block from ~/gitea/Caddyfile and 'docker exec gitea-caddy caddy reload'; revert the domains= edit in ~/.joplin-server/duckdns-update.sh; sudo rm /usr/local/bin/dufs; rm -rf ~/.config/dufs ~/cloud",
|
||||
"Validate after rollback: gitea still reachable at https://kuhy.duckdns.org and dufs.service is gone."
|
||||
]
|
||||
}
|
||||
5
linux_configuration/.gitignore
vendored
5
linux_configuration/.gitignore
vendored
@ -8,6 +8,11 @@ scripts/features/.raspberry_pi.conf
|
||||
scripts/single_use/features/.wireguard_ssh.conf
|
||||
.wireguard_ssh.conf
|
||||
|
||||
# dufs cloud setup config (contains the plaintext web password) — never commit.
|
||||
scripts/single_use/features/.dufs_cloud.conf
|
||||
.dufs_cloud.conf
|
||||
**/.dufs_cloud.conf
|
||||
|
||||
# Generated study materials (repo_to_study.sh output)
|
||||
study_materials/
|
||||
**/study_materials/
|
||||
|
||||
328
linux_configuration/scripts/single_use/features/setup_dufs_cloud.sh
Executable file
328
linux_configuration/scripts/single_use/features/setup_dufs_cloud.sh
Executable file
@ -0,0 +1,328 @@
|
||||
#!/bin/bash
|
||||
# setup_dufs_cloud.sh — Self-hosted "cloud" storage (dufs) on this Arch PC.
|
||||
#
|
||||
# Stands up dufs (a single Rust binary: web UI + built-in WebDAV over a real
|
||||
# folder), bound to 127.0.0.1 and fronted by the EXISTING Caddy reverse proxy
|
||||
# (from setup_gitea.sh) for automatic public HTTPS at a new DuckDNS subdomain.
|
||||
# Also mirrors the KeePassXC vault into the served folder on every change so it
|
||||
# is always fetchable from the phone (KeePassDX syncs it natively over WebDAV).
|
||||
#
|
||||
# Reuses the host's existing exposure stack — it does NOT add a second Caddy,
|
||||
# a second DuckDNS updater, or hand-edit /etc/nftables.conf. See
|
||||
# .github/skills/self-hosted-service-exposure/SKILL.md.
|
||||
#
|
||||
# Run as your normal user (NOT root). It uses sudo only for pacman, the binary
|
||||
# install, systemd units and the firewall flag. Idempotent — safe to re-run.
|
||||
#
|
||||
# Usage:
|
||||
# ./setup_dufs_cloud.sh Install / update everything
|
||||
# ./setup_dufs_cloud.sh -h Show help
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Pinned dufs release (verified by sha256) --------------------------------
|
||||
readonly DUFS_VERSION="0.46.0"
|
||||
readonly DUFS_SHA256="817769f726613194bcff9d0e3e481eaccc86ac11208857614f36a8c02f410977"
|
||||
readonly DUFS_ASSET="dufs-v${DUFS_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
||||
readonly DUFS_URL="https://github.com/sigoden/dufs/releases/download/v${DUFS_VERSION}/${DUFS_ASSET}"
|
||||
readonly DUFS_BIN="/usr/local/bin/dufs"
|
||||
readonly DUFS_PORT=5000
|
||||
|
||||
# --- Existing host infrastructure (from setup_gitea.sh / install_joplin.sh) --
|
||||
readonly CADDY_CONTAINER="gitea-caddy"
|
||||
readonly CADDYFILE="${HOME}/gitea/Caddyfile"
|
||||
readonly CADDYFILE_IN_CONTAINER="/etc/caddy/Caddyfile"
|
||||
readonly DUCKDNS_UPDATER="${HOME}/.joplin-server/duckdns-update.sh"
|
||||
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
||||
readonly SCRIPT_DIR
|
||||
readonly SYSTEMD_SRC="${SCRIPT_DIR}/systemd"
|
||||
readonly WG_SCRIPT="${SCRIPT_DIR}/setup_wireguard_ssh.sh"
|
||||
readonly CONFIG_FILE="${SCRIPT_DIR}/.dufs_cloud.conf"
|
||||
|
||||
# Git repo root (used by the paranoid secret guards below).
|
||||
REPO_ROOT="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
readonly REPO_ROOT
|
||||
|
||||
# shellcheck source=../../lib/common.sh
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/../../lib/common.sh"
|
||||
|
||||
# OS user that will own and run the service (not the dufs web-login user).
|
||||
SYS_USER="$(get_actual_user)"
|
||||
readonly SYS_USER
|
||||
|
||||
# Config values (populated from CONFIG_FILE / prompts).
|
||||
DUFS_USER=""
|
||||
DUFS_PASSWORD=""
|
||||
DUFS_SUBDOMAIN=""
|
||||
CLOUD_ROOT=""
|
||||
|
||||
die() {
|
||||
log_error "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
usage() {
|
||||
grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Step 1: preflight -------------------------------------------------------
|
||||
preflight() {
|
||||
log_info "Preflight checks"
|
||||
[[ ${EUID} -ne 0 ]] || die "Run as your normal user, not root (it will sudo where needed)."
|
||||
|
||||
# Runtime deps; install any that are missing from the official repos.
|
||||
install_missing_pacman_packages curl rsync inotify-tools openssl
|
||||
|
||||
# The public-exposure stack must already exist (Caddy from setup_gitea.sh).
|
||||
has_cmd docker || die "docker not found — run setup_gitea.sh first."
|
||||
docker ps --format '{{.Names}}' | grep -qx "${CADDY_CONTAINER}" \
|
||||
|| die "Caddy container '${CADDY_CONTAINER}' not running — run setup_gitea.sh first."
|
||||
[[ -f ${CADDYFILE} ]] || die "Caddyfile ${CADDYFILE} missing — run setup_gitea.sh first."
|
||||
[[ -x ${DUCKDNS_UPDATER} ]] || die "DuckDNS updater ${DUCKDNS_UPDATER} missing — run install_joplin.sh first."
|
||||
log_ok "Host exposure stack present (docker + ${CADDY_CONTAINER} + DuckDNS updater)"
|
||||
}
|
||||
|
||||
# --- Step 2: configuration (gitignored .conf, prompt when empty) -------------
|
||||
load_config() {
|
||||
if [[ -f ${CONFIG_FILE} ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "${CONFIG_FILE}"
|
||||
fi
|
||||
DUFS_USER="${DUFS_USER:-}"
|
||||
DUFS_PASSWORD="${DUFS_PASSWORD:-}"
|
||||
DUFS_SUBDOMAIN="${DUFS_SUBDOMAIN:-}"
|
||||
CLOUD_ROOT="${CLOUD_ROOT:-}"
|
||||
}
|
||||
|
||||
prompt_config() {
|
||||
if [[ -z ${DUFS_USER} ]]; then
|
||||
read -r -p "dufs web username [${SYS_USER}]: " DUFS_USER
|
||||
DUFS_USER="${DUFS_USER:-${SYS_USER}}"
|
||||
fi
|
||||
if [[ -z ${DUFS_PASSWORD} ]]; then
|
||||
read -r -s -p "dufs web password: " DUFS_PASSWORD
|
||||
echo
|
||||
[[ -n ${DUFS_PASSWORD} ]] || die "A password is required (this server is public)."
|
||||
fi
|
||||
if [[ -z ${DUFS_SUBDOMAIN} ]]; then
|
||||
read -r -p "public subdomain [kuhy-cloud.duckdns.org]: " DUFS_SUBDOMAIN
|
||||
DUFS_SUBDOMAIN="${DUFS_SUBDOMAIN:-kuhy-cloud.duckdns.org}"
|
||||
fi
|
||||
if [[ -z ${CLOUD_ROOT} ]]; then
|
||||
read -r -p "cloud storage folder [${HOME}/cloud]: " CLOUD_ROOT
|
||||
CLOUD_ROOT="${CLOUD_ROOT:-${HOME}/cloud}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Paranoid guard: never persist the plaintext password unless git is certain
|
||||
# the file is ignored. This is checked against the live .gitignore, so a broken
|
||||
# or removed ignore rule stops the write instead of leaking the secret.
|
||||
assert_config_gitignored() {
|
||||
[[ -n ${REPO_ROOT} ]] || die "Not inside a git repo — refusing to write the secret config."
|
||||
if ! git -C "${REPO_ROOT}" check-ignore -q "${CONFIG_FILE}"; then
|
||||
die "Refusing to write ${CONFIG_FILE}: it is NOT gitignored. Restore the .gitignore rule first — will not risk leaking the password."
|
||||
fi
|
||||
}
|
||||
|
||||
save_config() {
|
||||
assert_config_gitignored
|
||||
# 0600, gitignored — holds the plaintext web password so re-runs stay
|
||||
# non-interactive. Never committed (see .gitignore + the check-no-dufs-conf
|
||||
# pre-commit hook, which blocks it even against `git add -f`).
|
||||
umask 077
|
||||
{
|
||||
printf 'DUFS_USER=%q\n' "${DUFS_USER}"
|
||||
printf 'DUFS_PASSWORD=%q\n' "${DUFS_PASSWORD}"
|
||||
printf 'DUFS_SUBDOMAIN=%q\n' "${DUFS_SUBDOMAIN}"
|
||||
printf 'CLOUD_ROOT=%q\n' "${CLOUD_ROOT}"
|
||||
} >"${CONFIG_FILE}"
|
||||
chmod 600 "${CONFIG_FILE}"
|
||||
log_ok "Saved config to ${CONFIG_FILE} (0600)"
|
||||
}
|
||||
|
||||
# --- Step 3: install the pinned, checksum-verified dufs binary ---------------
|
||||
install_dufs() {
|
||||
if [[ -x ${DUFS_BIN} ]] && "${DUFS_BIN}" --version 2>/dev/null | grep -q "${DUFS_VERSION}"; then
|
||||
log_ok "dufs ${DUFS_VERSION} already installed"
|
||||
return 0
|
||||
fi
|
||||
log_info "Installing dufs ${DUFS_VERSION}"
|
||||
local tmp
|
||||
tmp="$(mktemp -d)"
|
||||
# shellcheck disable=SC2064
|
||||
trap "rm -rf '${tmp}'" RETURN
|
||||
curl -fsSL -o "${tmp}/${DUFS_ASSET}" "${DUFS_URL}"
|
||||
echo "${DUFS_SHA256} ${tmp}/${DUFS_ASSET}" | sha256sum -c - \
|
||||
|| die "dufs checksum mismatch — refusing to install."
|
||||
tar -xzf "${tmp}/${DUFS_ASSET}" -C "${tmp}"
|
||||
sudo install -m 0755 "${tmp}/dufs" "${DUFS_BIN}"
|
||||
log_ok "Installed ${DUFS_BIN}"
|
||||
}
|
||||
|
||||
# Paranoid guard: never let dufs serve a directory that would expose secrets
|
||||
# (home root, ~/.ssh, ~/.config — which holds dufs.yaml — ~/.gnupg, / , or the
|
||||
# git repo which holds the gitignored .dufs_cloud.conf).
|
||||
assert_cloud_root_safe() {
|
||||
local resolved home_r
|
||||
resolved="$(readlink -m "${CLOUD_ROOT}")"
|
||||
home_r="$(readlink -m "${HOME}")"
|
||||
case "${resolved}" in
|
||||
"${home_r}" | / | "${home_r}/.ssh" | "${home_r}/.ssh/"* \
|
||||
| "${home_r}/.config" | "${home_r}/.config/"* \
|
||||
| "${home_r}/.gnupg" | "${home_r}/.gnupg/"*)
|
||||
die "Unsafe cloud folder '${CLOUD_ROOT}' — it would expose sensitive files. Use a dedicated folder like ${HOME}/cloud." ;;
|
||||
esac
|
||||
if [[ -n ${REPO_ROOT} && (${resolved} == "${REPO_ROOT}" || ${resolved} == "${REPO_ROOT}/"*) ]]; then
|
||||
die "Unsafe cloud folder '${CLOUD_ROOT}' — it is inside the git repo (which holds the secret config). Use a dedicated folder."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 4: served folder ---------------------------------------------------
|
||||
create_serve_dir() {
|
||||
assert_cloud_root_safe
|
||||
ensure_dir "${CLOUD_ROOT}"
|
||||
ensure_dir "${CLOUD_ROOT}/Keepass"
|
||||
chmod 700 "${CLOUD_ROOT}"
|
||||
log_ok "Cloud folder ${CLOUD_ROOT} ready"
|
||||
}
|
||||
|
||||
# --- Step 5: dufs config (SHA-512 hashed password, no anonymous access) -------
|
||||
write_dufs_config() {
|
||||
local cfg_dir="${HOME}/.config/dufs"
|
||||
local cfg="${cfg_dir}/dufs.yaml"
|
||||
local hash
|
||||
# Read the password from stdin so it never appears in the process argv (ps).
|
||||
hash="$(printf '%s' "${DUFS_PASSWORD}" | openssl passwd -6 -stdin)"
|
||||
ensure_dir "${cfg_dir}"
|
||||
umask 077
|
||||
cat >"${cfg}" <<EOF
|
||||
# Generated by setup_dufs_cloud.sh — do not edit by hand.
|
||||
serve-path: ${CLOUD_ROOT}
|
||||
bind: 127.0.0.1
|
||||
port: ${DUFS_PORT}
|
||||
allow-all: true
|
||||
auth:
|
||||
- "${DUFS_USER}:${hash}@/:rw"
|
||||
EOF
|
||||
chmod 600 "${cfg}"
|
||||
log_ok "Wrote ${cfg} (auth required, password hashed)"
|
||||
}
|
||||
|
||||
# --- Step 6: systemd units (render placeholders → /etc/systemd/system) -------
|
||||
render_unit() {
|
||||
local name="$1"
|
||||
sed -e "s|__USER__|${SYS_USER}|g" \
|
||||
-e "s|__HOME__|${HOME}|g" \
|
||||
-e "s|__CLOUD_ROOT__|${CLOUD_ROOT}|g" \
|
||||
"${SYSTEMD_SRC}/${name}" | sudo tee "/etc/systemd/system/${name}" >/dev/null
|
||||
}
|
||||
|
||||
install_units() {
|
||||
log_info "Installing systemd units"
|
||||
render_unit dufs.service
|
||||
render_unit keepass-cloud-sync.service
|
||||
render_unit keepass-cloud-sync.path
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now dufs.service
|
||||
log_ok "dufs.service enabled and started"
|
||||
}
|
||||
|
||||
# --- Step 7: Caddy reverse-proxy block (append + reload) ---------------------
|
||||
configure_caddy() {
|
||||
if grep -qE "^${DUFS_SUBDOMAIN} \{" "${CADDYFILE}"; then
|
||||
log_ok "Caddyfile already has a block for ${DUFS_SUBDOMAIN}"
|
||||
else
|
||||
log_info "Adding Caddy block for ${DUFS_SUBDOMAIN}"
|
||||
# NOTE: appended to the shared Gitea Caddyfile. Re-running setup_gitea.sh
|
||||
# regenerates that file and drops this block — re-run setup_dufs_cloud.sh
|
||||
# to restore it. Caddyfile uses tab indentation.
|
||||
printf '\n%s {\n\treverse_proxy 127.0.0.1:%s\n}\n' "${DUFS_SUBDOMAIN}" "${DUFS_PORT}" \
|
||||
>>"${CADDYFILE}"
|
||||
fi
|
||||
docker exec "${CADDY_CONTAINER}" caddy reload --config "${CADDYFILE_IN_CONTAINER}" \
|
||||
|| die "caddy reload failed — check 'docker logs ${CADDY_CONTAINER}'."
|
||||
log_ok "Caddy reloaded (auto-HTTPS will be issued once the subdomain resolves)"
|
||||
}
|
||||
|
||||
# --- Step 8: DuckDNS subdomain (extend existing updater; no new cron) --------
|
||||
configure_duckdns() {
|
||||
local label="${DUFS_SUBDOMAIN%.duckdns.org}"
|
||||
if grep -qE "domains=[^&\"']*${label}([,&\"']|$)" "${DUCKDNS_UPDATER}"; then
|
||||
log_ok "DuckDNS updater already includes '${label}'"
|
||||
else
|
||||
log_info "Adding '${label}' to the existing DuckDNS updater"
|
||||
sed -i -E "s|(domains=)([^&\"']*)|\1\2,${label}|" "${DUCKDNS_UPDATER}"
|
||||
fi
|
||||
# Refresh now (harmless if the subdomain isn't registered yet).
|
||||
bash "${DUCKDNS_UPDATER}" || log_warn "DuckDNS update returned non-zero (register the subdomain first)."
|
||||
if getent hosts "${DUFS_SUBDOMAIN}" >/dev/null 2>&1; then
|
||||
log_ok "${DUFS_SUBDOMAIN} resolves"
|
||||
else
|
||||
log_warn "${DUFS_SUBDOMAIN} does not resolve yet — create the '${label}' subdomain in the DuckDNS dashboard."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 9: firewall (open 80/443 via the owning script, idempotent) -------
|
||||
ensure_firewall() {
|
||||
if sudo nft list ruleset 2>/dev/null | grep -qE 'dport \{[^}]*80[^}]*443'; then
|
||||
log_ok "Firewall already allows tcp/80 + tcp/443"
|
||||
elif [[ -f ${WG_SCRIPT} ]]; then
|
||||
log_info "Opening 80/443 via setup_wireguard_ssh.sh allow-web"
|
||||
sudo bash "${WG_SCRIPT}" allow-web
|
||||
else
|
||||
log_warn "setup_wireguard_ssh.sh not found — ensure tcp/80+443 are open."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Step 10: KeePass auto-sync (initial mirror + enable the path watcher) ---
|
||||
setup_autosync() {
|
||||
log_info "Setting up KeePass auto-sync watcher"
|
||||
# Mirror only the vault file (not the whole dir with local .backup_* vaults).
|
||||
rsync -a "${HOME}/Keepass/Passwords.kdbx" "${CLOUD_ROOT}/Keepass/"
|
||||
sudo systemctl enable --now keepass-cloud-sync.path
|
||||
log_ok "Vault mirrored and watcher enabled"
|
||||
}
|
||||
|
||||
# --- Step 11: report ---------------------------------------------------------
|
||||
print_report() {
|
||||
local url="https://${DUFS_SUBDOMAIN}"
|
||||
cat <<EOF
|
||||
|
||||
============================================================================
|
||||
dufs cloud is up.
|
||||
Web UI / WebDAV : ${url}/
|
||||
Login : ${DUFS_USER} / (your password)
|
||||
Served folder : ${CLOUD_ROOT}
|
||||
KeePassDX target: ${url}/Keepass/Passwords.kdbx (WebDAV)
|
||||
|
||||
If the subdomain is new, create it once in the DuckDNS dashboard
|
||||
(label: ${DUFS_SUBDOMAIN%.duckdns.org}) so Caddy can issue TLS.
|
||||
|
||||
Acceptance test: on your phone with Wi-Fi OFF (cellular), open ${url}/,
|
||||
log in, and confirm you see Keepass/Passwords.kdbx.
|
||||
============================================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
|
||||
print_setup_header "Self-hosted dufs cloud storage"
|
||||
preflight
|
||||
load_config
|
||||
prompt_config
|
||||
save_config
|
||||
install_dufs
|
||||
create_serve_dir
|
||||
write_dufs_config
|
||||
install_units
|
||||
configure_caddy
|
||||
configure_duckdns
|
||||
ensure_firewall
|
||||
setup_autosync
|
||||
print_report
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
# dufs self-hosted cloud file server (web UI + WebDAV), bound to loopback and
|
||||
# fronted by the existing Caddy reverse proxy for public HTTPS.
|
||||
# Placeholders (__USER__ / __HOME__) are substituted by setup_dufs_cloud.sh at
|
||||
# install time; this file is the source of truth, not the installed copy.
|
||||
Description=dufs self-hosted cloud file server
|
||||
Documentation=https://github.com/sigoden/dufs
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=__USER__
|
||||
ExecStart=/usr/local/bin/dufs --config __HOME__/.config/dufs/dufs.yaml
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
# Minimal hardening (dufs needs $HOME/.config and the served dir under /home).
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
# Event-driven watch on the KeePassXC vault (no polling). On modification it
|
||||
# starts keepass-cloud-sync.service, which rsyncs the vault into the cloud folder.
|
||||
# Placeholder __HOME__ is substituted by setup_dufs_cloud.sh at install time.
|
||||
Description=Watch KeePass vault and sync to dufs cloud folder
|
||||
|
||||
[Path]
|
||||
PathModified=__HOME__/Keepass/Passwords.kdbx
|
||||
Unit=keepass-cloud-sync.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
# Mirror the live KeePassXC vault into the dufs-served cloud folder.
|
||||
# Triggered by keepass-cloud-sync.path on every change to Passwords.kdbx.
|
||||
# Placeholders (__USER__ / __HOME__ / __CLOUD_ROOT__) are substituted by
|
||||
# setup_dufs_cloud.sh at install time.
|
||||
Description=Mirror KeePass vault into dufs cloud folder
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=__USER__
|
||||
# Only the vault file — not the whole ~/Keepass dir (keeps local .backup_* vaults
|
||||
# off the public server). No --delete: the cloud folder may hold other files.
|
||||
ExecStart=/usr/bin/rsync -a __HOME__/Keepass/Passwords.kdbx __CLOUD_ROOT__/Keepass/
|
||||
25
meta/scripts/check_no_dufs_conf.sh
Executable file
25
meta/scripts/check_no_dufs_conf.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# ============================================================================
|
||||
# Pre-commit guard: never allow the dufs cloud secret config to be committed.
|
||||
#
|
||||
# .dufs_cloud.conf holds the plaintext dufs web password. It is gitignored, but
|
||||
# `git add -f` can bypass gitignore — this hook is the backstop. It inspects the
|
||||
# staged set directly (not just the files pre-commit passes) so it catches a
|
||||
# force-added file regardless of any `files:` filter.
|
||||
# ============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# Added/Copied/Modified staged paths.
|
||||
staged="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)"
|
||||
|
||||
bad="$(printf '%s\n' "$staged" | grep -E '(^|/)\.dufs_cloud\.conf$' || true)"
|
||||
|
||||
if [ -n "$bad" ]; then
|
||||
echo "ERROR: refusing to commit the dufs secret config (plaintext password):" >&2
|
||||
printf ' %s\n' "$bad" >&2
|
||||
echo "" >&2
|
||||
echo "This file must stay local only. Unstage it: git rm --cached <file>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Loading…
Reference in New Issue
Block a user