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-