From a23d5d6fc7ae372dc7225d06cdb27b3d833f5d9f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 23:06:13 +0000 Subject: [PATCH] Add optional client-side encryption layer for vpn-data-mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VPS side of a plain SMB mount necessarily sees plaintext while it's mounted and in use — that's unavoidable for data a VPS service actually needs to read. What's avoidable is everything else: a disk image, backup, or provider-side look at the VPS while the mount isn't actively in use showing your actual files instead of ciphertext. tools/gocryptfs-setup-home.sh (new): standalone tool for the home box. Creates a gocryptfs-encrypted directory and passphrase file; the user points their existing Samba share's `path =` at the cipherdir (manual step — same read-only stance on remote Samba config vpn-data-mount.sh already takes, this tool doesn't touch smb.conf either). services/vpn-data-mount.sh: after mounting a share over CIFS as before, optionally offers a gocryptfs decrypt layer on top. Fetches the passphrase fresh over the same SSH trust already used for share discovery, pipes it straight into gocryptfs, and never writes it to the VPS's own disk. A generated systemd unit (via a wrapper script, not one long quoted ExecStart= one-liner — avoids stacking systemd's own word-splitting on top of bash -c's) keeps the decrypted view coming back on boot, re-fetching the passphrase each time rather than caching it. Fully opt-in and per-share — a plain unencrypted mount works exactly as before if declined. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01H4k6J1qXXyYxhGEgnJaMvn --- README.md | 34 +++++++- services/vpn-data-mount.sh | 146 +++++++++++++++++++++++++++++++ tools/gocryptfs-setup-home.sh | 156 ++++++++++++++++++++++++++++++++++ 3 files changed, 335 insertions(+), 1 deletion(-) create mode 100755 tools/gocryptfs-setup-home.sh diff --git a/README.md b/README.md index b8a39f2..2d4af54 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ a ready-to-copy Caddy config snippet to `~/docker/caddy-snippets/`. | Group | Services | |-------|---------| | `base` | `net-tools`, `ncdu`, `git`, `curl`, `wget`, `htop`, `tree`, `zip`/`unzip`, `ca-certificates`, `gnupg`, `jq`, `rsync`; `glow` (terminal markdown reader, Charm apt repo); Docker CE + Compose plugin; `openssh-server` with GitHub/Launchpad SSH key import, optional password-auth lockdown, and SSH Host aliases; optional NetBird overlay network | -| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (mount existing SMB shares from a NetBird-connected home box — SSH trust bootstrap, then read-only discovery of shares already configured there; never writes to the home box's Samba config; repeatable, pick from any number of a home box's shares in one pass) | +| `homelab` | `caddy`, `crowdsec`, `authelia`, `coturn` (shared TURN/STUN relay — Asterisk, Mattermost Calls, and future WebRTC-capable services all register a dedicated credential against one instance instead of each running its own), `homeassistant`, `asterisk`, `pstn-trunk`, `sms-inbound`, `security-dashboard`, `sunshine`, `vpn-data-mount` (mount existing SMB shares from a NetBird-connected home box — SSH trust bootstrap, then read-only discovery of shares already configured there; never writes to the home box's Samba config; repeatable, pick from any number of a home box's shares in one pass; optional per-share [gocryptfs decrypt layer](#client-side-encryption-for-vpn-data-mount) so the VPS only ever handles ciphertext) | | `utilities` | `actualbudget`, `ai-gpu`, `ai-stack`, `archivebox`, `changedetection`, `ddclient`, `filebrowser`, `fmd`, `gatus`, `homebox`, `iopaint`, `joplin`, `koha`, `magicmirror`, `mail-archiver`, `mattermost`, `mealie`, `meshcentral`, `n8n`, `nextcloud`, `ntfy`, `onlyoffice`, `paintplus`, `portainer`, `rustdesk`, `stirling-pdf`, `syncthing`, `traccar`, `unifi`, `uptimekuma`, `vaultwarden`, `watchyourlan`, `watchtower`, `wg-easy`, `wordpress` (multi-site, dedicated MariaDB per site — blogs, business sites, e-commerce via WooCommerce) | | `media` | `arm`, `audiobookshelf`, `calibre-web`, `emby`, `immich`, `jellyfin`, `lyrion` | | `cameras` | `frigate`, `frigate-audio`, `frigate-notify`, `sky-cam` | @@ -327,6 +327,38 @@ Password authentication is only offered to be disabled if at least one key import actually succeeded in that run — never blindly, so you can't get locked out by declining every import prompt. +## Client-side encryption for vpn-data-mount + +A plain SMB mount over the VPN protects the data in transit, but the VPS +itself — its disk, page cache, and anyone with access to the machine (the +host, an attacker who compromises it, a compelled disclosure) — sees +plaintext while it's mounted. `vpn-data-mount`'s optional decrypt layer +closes most of that gap by encrypting on the home box, before anything +ever crosses the network: + +1. On the home box, run `sudo bash tools/gocryptfs-setup-home.sh`. It + creates an encrypted directory (a "cipherdir") and a passphrase file, + and prints the exact next step. Point your existing Samba share's + `path =` at the cipherdir itself — this tool never touches smb.conf, + same read-only stance `vpn-data-mount` itself takes on the VPS side. +2. On the VPS, `sudo ./setup.sh vpn-data-mount` as usual. After it mounts + the share over CIFS, it asks whether to layer gocryptfs decryption on + top — say yes and give it the passphrase file's path. It fetches that + file fresh over the same SSH trust already set up for share discovery, + pipes it straight into `gocryptfs`, and never writes it to the VPS's + own disk. A systemd unit keeps the decrypted view coming back on boot, + fetching the passphrase again each time rather than caching it. + +What this changes: a disk image, backup, or provider-side look at the VPS +while the passphrase isn't actively loaded shows only ciphertext. What it +doesn't change: anything actually reading through the decrypted mount +while it's live still sees plaintext, same as any data in active use +anywhere — that part isn't a software problem this (or any) tool can +solve out from under the machine actually using the data. + +Skip this entirely if it's not worth the added moving part — `vpn-data-mount` +works exactly the same without it, plain CIFS, nothing to opt into. + ## SSH Host aliases `~/.ssh/config` lets you `ssh ` instead of typing `ssh user@1.2.3.4` diff --git a/services/vpn-data-mount.sh b/services/vpn-data-mount.sh index a2a9ba8..e9df47c 100644 --- a/services/vpn-data-mount.sh +++ b/services/vpn-data-mount.sh @@ -71,6 +71,22 @@ # case for a home-data share; non-ASCII filenames may not round-trip # perfectly on a kernel missing this module, which is a kernel # limitation this script can't paper over further). +# +# Optional: a gocryptfs decrypt layer on top of the plain CIFS mount, for +# when "available to the VPS" and "opaque to the VPS's operator/anyone with +# disk access to it" both matter. Set up the encrypted store on the home +# box first with tools/gocryptfs-setup-home.sh (that tool's header explains +# the actual threat model this buys you and, just as importantly, doesn't). +# The short version: the home box encrypts before anything ever crosses +# SMB, so the VPS's CIFS mount only ever holds ciphertext; this script's +# decrypt layer then fetches the passphrase fresh over the same SSH trust +# already used for share discovery — piped straight into gocryptfs, never +# written to this VPS's own disk — and mounts a decrypted view alongside +# the raw ciphertext mount. Whatever's actively reading through that +# decrypted view still sees plaintext live, same as any mount anywhere; +# nothing changes that. What changes is everything else: a snapshot, +# backup, or disk-level look at this VPS while the passphrase isn't +# actively loaded shows only ciphertext. # ── Standalone bootstrap ────────────────────────────────────────────────────── if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then @@ -130,6 +146,14 @@ _vdm_list_existing() { echo "$entries" | sed "s|^${_VDM_TAG_PREFIX}| •|" echo "" fi + + local units + units="$(systemctl list-unit-files 'vpn-data-mount-decrypt-*.service' --no-legend 2>/dev/null | awk '{print $1}')" + if [ -n "$units" ]; then + log_info "Decrypt layers configured:" + echo "$units" | sed 's/^/ • /' + echo "" + fi } # ── Name a raw IP so it can be used everywhere instead of typing it again ── @@ -400,6 +424,124 @@ CREDS log_success "Added to /etc/fstab (backup: $(basename "$bk"))" } +# ── Optional gocryptfs decrypt layer on top of an already-mounted share ──── +# See the file header for the threat model. Fully opt-in, per-share, off by +# default — a plain unencrypted share works exactly as before. +_vdm_ensure_gocryptfs() { + command -v gocryptfs >/dev/null 2>&1 || apt-get install -y gocryptfs -qq + + # -allow_other needs this uncommented for a non-root mount; harmless + # (and unnecessary, since we always mount as root below) otherwise — + # cheap to guarantee rather than leave as a silent gotcha if that ever + # changes. + local conf="/etc/fuse.conf" + [ -f "$conf" ] || touch "$conf" + if grep -q '^#user_allow_other' "$conf"; then + sed -i 's/^#user_allow_other/user_allow_other/' "$conf" + elif ! grep -q '^user_allow_other' "$conf"; then + echo "user_allow_other" >> "$conf" + fi +} + +# Generates /etc/systemd/system/vpn-data-mount-decrypt-