Merge pull request #289 from outis1one/claude/ionos-script-integration-x32ofw
Claude/ionos script integration x32ofw
This commit is contained in:
@@ -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 <alias>` instead of typing `ssh user@1.2.3.4`
|
||||
|
||||
+11
-7
@@ -335,9 +335,6 @@ install_mattermost() {
|
||||
[ -n "$DB_PASS" ] || DB_PASS=$(generate_password 32)
|
||||
|
||||
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
||||
local UID_VAL GID_VAL
|
||||
UID_VAL=$(id -u "$ACTUAL_USER")
|
||||
GID_VAL=$(id -g "$ACTUAL_USER")
|
||||
|
||||
# Compute SITE_URL — extra instances default to a distinct subdomain so
|
||||
# they don't collide with the first instance's.
|
||||
@@ -489,15 +486,22 @@ TURN_HOST=$TURN_HOST_VAL
|
||||
TURN_PORT=$TURN_PORT_VAL
|
||||
TURN_USERNAME=$TURN_USERNAME_VAL
|
||||
TURN_PASSWORD=$TURN_PASSWORD_VAL
|
||||
|
||||
# PUID/PGID for file ownership
|
||||
PUID=$UID_VAL
|
||||
PGID=$GID_VAL
|
||||
EOF
|
||||
chmod 600 .env
|
||||
|
||||
mkdir -p data logs config plugins db
|
||||
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DIR"
|
||||
# mattermost/mattermost-team-edition's image runs as a fixed UID/GID
|
||||
# 2000 baked in at build time — unlike some other images in this repo,
|
||||
# it does NOT read PUID/PGID env vars (that's a LinuxServer.io s6-overlay
|
||||
# convention this image doesn't use; a previous version of this file set
|
||||
# them anyway, which did nothing). Confirmed live: leaving ./data,
|
||||
# ./logs, ./config, ./plugins owned by ACTUAL_USER instead of 2000:2000
|
||||
# makes the container fail on its very first start with "could not
|
||||
# create config file: open /mattermost/config/config.json: permission
|
||||
# denied" and crash-loop — db (postgres:15-alpine) isn't affected, its
|
||||
# entrypoint fixes ownership itself on startup when it needs to.
|
||||
chown -R 2000:2000 data logs config plugins
|
||||
|
||||
# ── Firewall ─────────────────────────────────────────────────────────────
|
||||
if command -v ufw &>/dev/null; then
|
||||
|
||||
+219
-1
@@ -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 ──
|
||||
@@ -308,6 +332,46 @@ _vdm_find_existing_smb_password() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── Already mounted? Find its label+mount point instead of re-prompting ───
|
||||
# Prints "label|mount_point" if this exact host+share is already tagged in
|
||||
# /etc/fstab, nothing otherwise. No log_* calls — same $(...) capture
|
||||
# reason as _vdm_find_existing_smb_password above.
|
||||
_vdm_find_existing_mount() {
|
||||
local host="$1" share_name="$2"
|
||||
grep -E "^${_VDM_TAG_PREFIX} [^ ]+ — ${host}:${share_name} -> " /etc/fstab 2>/dev/null \
|
||||
| sed -E "s/^${_VDM_TAG_PREFIX} ([^ ]+) — [^ ]+ -> (.*)\$/\1|\2/" \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# ── Tear down an existing mount so it can be redone from scratch ──────────
|
||||
# Decrypt layer first (it sits on top of the CIFS mount — systemctl stop
|
||||
# runs the unit's own ExecStop, which unmounts it) if one exists for this
|
||||
# label, then the CIFS mount, its credentials file, and its /etc/fstab
|
||||
# tag+entry (backed up first, same as every other /etc/fstab write in this
|
||||
# file — a fixed ",+1d" range: the tag line plus exactly the one mount line
|
||||
# that always immediately follows it, never an open-ended range to the next
|
||||
# blank line or EOF; see the file's own history for why that distinction
|
||||
# matters).
|
||||
_vdm_remove_mount() {
|
||||
local label="$1" mount_point="$2"
|
||||
local unit="vpn-data-mount-decrypt-${label}.service"
|
||||
|
||||
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q .; then
|
||||
systemctl disable --now "$unit" >/dev/null 2>&1
|
||||
rm -f "/etc/systemd/system/${unit}" "/usr/local/sbin/vpn-data-mount-decrypt-${label}.sh"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
umount "$mount_point" 2>/dev/null || true
|
||||
rmdir "$mount_point" 2>/dev/null || true
|
||||
rm -f "/etc/samba/credentials.vpn-data-mount-${label}"
|
||||
|
||||
local bk="/etc/fstab.backup.$(date +%Y%m%d-%H%M%S)"
|
||||
cp /etc/fstab "$bk"
|
||||
sed -i "/^${_VDM_TAG_PREFIX} ${label} — /,+1d" /etc/fstab
|
||||
log_success "Removed the existing mount for '$label' (fstab backup: $(basename "$bk"))"
|
||||
}
|
||||
|
||||
# ── Prompt for a password twice, hidden, matching ──────────────────────────
|
||||
# Prints the password on success. No log_* calls — same reason as above;
|
||||
# uses plain stderr output instead so it's visible without corrupting a
|
||||
@@ -400,6 +464,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-<label>.service so the
|
||||
# decrypt layer comes back after a reboot the same way the fstab CIFS mount
|
||||
# does. RequiresMountsFor sequences it after that mount (whether it's an
|
||||
# fstab-generated mount unit or comes up some other way) instead of racing
|
||||
# it. The passphrase fetch happens fresh on every start — nothing from it
|
||||
# persists in the unit file itself or anywhere else on this disk.
|
||||
_vdm_write_decrypt_unit() {
|
||||
local host="$1" ssh_user="$2" label="$3" passfile="$4" cifs_mount_point="$5" decrypted_point="$6"
|
||||
local wrapper="/usr/local/sbin/vpn-data-mount-decrypt-${label}.sh"
|
||||
local unit="/etc/systemd/system/vpn-data-mount-decrypt-${label}.service"
|
||||
|
||||
# A wrapper script, not one long quoted ExecStart= one-liner — a single
|
||||
# ExecStart string embedding an SSH-piped-to-gocryptfs pipeline needs
|
||||
# two independent layers of quoting (systemd's own ExecStart= word
|
||||
# splitting, then bash -c's), which is exactly the kind of thing that
|
||||
# looks right and silently breaks on the one path/host with a space or
|
||||
# an unusual character in it. A plain script just uses normal bash
|
||||
# quoting once.
|
||||
cat > "$wrapper" << WRAPPER
|
||||
#!/bin/bash
|
||||
set -e
|
||||
sudo -u "${ACTUAL_USER}" ssh -o BatchMode=yes -o ConnectTimeout=10 "${ssh_user}@${host}" "cat '${passfile}'" \\
|
||||
| gocryptfs -passfile /dev/stdin -allow_other "${cifs_mount_point}" "${decrypted_point}"
|
||||
WRAPPER
|
||||
chmod 700 "$wrapper"
|
||||
chown root:root "$wrapper"
|
||||
|
||||
cat > "$unit" << UNIT
|
||||
[Unit]
|
||||
Description=vpn-data-mount gocryptfs decrypt layer: ${label}
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
RequiresMountsFor=${cifs_mount_point}
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=${wrapper}
|
||||
ExecStop=/bin/fusermount -u ${decrypted_point}
|
||||
Restart=on-failure
|
||||
RestartSec=15
|
||||
User=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
if systemctl enable --now "vpn-data-mount-decrypt-${label}.service" >/dev/null 2>&1; then
|
||||
log_success "Decrypt layer will remount automatically on boot (systemd unit: vpn-data-mount-decrypt-${label})"
|
||||
else
|
||||
log_warning "Couldn't enable the boot-time systemd unit — you'll need to re-run the decrypt step manually after a reboot."
|
||||
fi
|
||||
}
|
||||
|
||||
_vdm_setup_decrypt_layer() {
|
||||
local host="$1" ssh_user="$2" label="$3" cifs_mount_point="$4"
|
||||
|
||||
echo ""
|
||||
local WANT=""
|
||||
prompt_yn " Is [$label] a gocryptfs-encrypted share (set up on the home box with tools/gocryptfs-setup-home.sh)? Layer decryption on top? (y/n):" "n" WANT
|
||||
[[ "$WANT" =~ ^[Yy]$ ]] || return 0
|
||||
|
||||
_vdm_ensure_gocryptfs
|
||||
|
||||
local passfile=""
|
||||
prompt_text " Path to the passphrase file on the home box (printed at the end of gocryptfs-setup-home.sh):" "" passfile
|
||||
if [ -z "$passfile" ]; then
|
||||
log_warning "No path given — skipping the decrypt layer for [$label]."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes -o ConnectTimeout=5 "${ssh_user}@${host}" "test -f '$passfile'" 2>/dev/null; then
|
||||
log_warning "Can't see $passfile on ${ssh_user}@${host} — skipping the decrypt layer for [$label]. Run tools/gocryptfs-setup-home.sh there first, or check the path."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local decrypted_point=""
|
||||
prompt_text " Local mount point for the decrypted view:" "${cifs_mount_point}-decrypted" decrypted_point
|
||||
mkdir -p "$decrypted_point"
|
||||
|
||||
if mountpoint -q "$decrypted_point" 2>/dev/null; then
|
||||
log_info "$decrypted_point is already mounted — leaving it as-is, just (re)writing the boot-time unit."
|
||||
else
|
||||
if ! sudo -u "$ACTUAL_USER" ssh -o BatchMode=yes "${ssh_user}@${host}" "cat '$passfile'" \
|
||||
| gocryptfs -passfile /dev/stdin -allow_other "$cifs_mount_point" "$decrypted_point"; then
|
||||
log_error "gocryptfs mount failed for [$label] — check the passphrase file on the home box and that $cifs_mount_point actually holds a gocryptfs store (gocryptfs.conf present at its root)."
|
||||
rmdir "$decrypted_point" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
log_success "Decrypted view mounted at $decrypted_point"
|
||||
fi
|
||||
|
||||
_vdm_write_decrypt_unit "$host" "$ssh_user" "$label" "$passfile" "$cifs_mount_point" "$decrypted_point"
|
||||
|
||||
# Point chained callers (filebrowser.sh etc.) at the decrypted view
|
||||
# instead of the raw ciphertext mount, same out-param as elsewhere.
|
||||
VDM_LAST_MOUNT_POINT="$decrypted_point"
|
||||
}
|
||||
|
||||
# ── Parse a selection like "1", "1,3", "1-3", "1 3 5" into 1-based indices ──
|
||||
# Prints one index per line. Silently drops anything that doesn't look like
|
||||
# a number or a range — the caller validates indices against the actual
|
||||
@@ -506,9 +688,41 @@ _vdm_add_mount() {
|
||||
echo ""
|
||||
log_info "Setting up: [$this_share] -> $this_path"
|
||||
|
||||
# Already mounted from an earlier run? Offer to reconfigure in
|
||||
# place or fully redo it, instead of forcing a new label on a share
|
||||
# that already has one.
|
||||
local existing existing_label existing_point default_label="$this_share"
|
||||
existing="$(_vdm_find_existing_mount "$HOST" "$this_share")"
|
||||
if [ -n "$existing" ]; then
|
||||
existing_label="${existing%%|*}"
|
||||
existing_point="${existing#*|}"
|
||||
echo ""
|
||||
log_info "[$this_share] is already mounted as '$existing_label' at $existing_point."
|
||||
echo " 1) Set up/redo the gocryptfs decrypt layer for it (leaves the mount itself alone)"
|
||||
echo " 2) Fully redo this mount (new mount point and/or re-enter credentials)"
|
||||
echo " 3) Skip"
|
||||
local EXIST_CHOICE=""
|
||||
prompt_text " Choice [1/2/3]:" "1" EXIST_CHOICE
|
||||
case "$EXIST_CHOICE" in
|
||||
2)
|
||||
_vdm_remove_mount "$existing_label" "$existing_point"
|
||||
default_label="$existing_label"
|
||||
;;
|
||||
3)
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
VDM_LAST_MOUNT_POINT="$existing_point"
|
||||
picked_any=true
|
||||
_vdm_setup_decrypt_layer "$HOST" "$SSH_USER" "$existing_label" "$existing_point"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
LABEL=""
|
||||
while true; do
|
||||
prompt_text " Local label for this mount:" "$this_share" LABEL
|
||||
prompt_text " Local label for this mount:" "$default_label" LABEL
|
||||
LABEL="$(echo "$LABEL" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
|
||||
if [ -z "$LABEL" ]; then
|
||||
log_warning "Label can't be empty."; continue
|
||||
@@ -527,8 +741,11 @@ _vdm_add_mount() {
|
||||
# emby.sh that chain into this service and want to default
|
||||
# their own "which directory" prompt to whatever was just
|
||||
# mounted. Last one wins if several were picked in this run.
|
||||
# _vdm_setup_decrypt_layer overwrites this with the decrypted
|
||||
# view's path instead, if one gets set up for this share.
|
||||
VDM_LAST_MOUNT_POINT="$MOUNT_POINT"
|
||||
picked_any=true
|
||||
_vdm_setup_decrypt_layer "$HOST" "$SSH_USER" "$LABEL" "$MOUNT_POINT"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -553,6 +770,7 @@ install_vpn-data-mount() {
|
||||
echo "[DRY-RUN] Would read-only list the home box's existing Samba shares (never writes there)"
|
||||
echo "[DRY-RUN] Would let you pick one or more by number and mount them locally over CIFS"
|
||||
echo "[DRY-RUN] Would add each to /etc/fstab with a root-only credentials file (not guest)"
|
||||
echo "[DRY-RUN] Would offer a gocryptfs decrypt layer per share (opt-in, requires the home box already set up via tools/gocryptfs-setup-home.sh)"
|
||||
echo "[DRY-RUN] Repeatable — can be run again for additional home boxes"
|
||||
return 0
|
||||
fi
|
||||
|
||||
Executable
+156
@@ -0,0 +1,156 @@
|
||||
#!/usr/bin/env bash
|
||||
# gocryptfs-setup-home.sh — Set up a gocryptfs-encrypted directory on this
|
||||
# box (the "home box" in vpn-data-mount.sh's terms) so a remote VPS can
|
||||
# mount it over SMB and only ever handle ciphertext.
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash gocryptfs-setup-home.sh
|
||||
#
|
||||
# What this buys you: files placed in the encrypted store below are
|
||||
# encrypted on THIS box, before they ever cross the network. Your existing
|
||||
# Samba share, if its `path =` points at the encrypted store (a manual
|
||||
# smb.conf edit — see "Next steps" at the end of this script; this tool
|
||||
# doesn't touch smb.conf, matching vpn-data-mount.sh's own read-only stance
|
||||
# on remote Samba config), only ever transmits ciphertext over SMB. The
|
||||
# VPS's CIFS mount, and its own disk/page cache, only ever hold ciphertext
|
||||
# too. services/vpn-data-mount.sh's optional decrypt-layer step then fetches
|
||||
# the passphrase file this script creates below, fresh, over the same SSH
|
||||
# trust already used for share discovery — never storing it on the VPS's
|
||||
# disk — and decrypts on the fly into a second mountpoint there.
|
||||
#
|
||||
# What this does NOT buy you: privacy from whatever's actively reading
|
||||
# through that decrypted view on the VPS while it's mounted there — if a
|
||||
# service on the VPS needs to read the plaintext to do its job (serve a
|
||||
# file, transcode video, whatever), that service (and root on that VPS)
|
||||
# sees plaintext for as long as it's mounted, same as any live-in-use data
|
||||
# anywhere. What changes is everything else: a disk image, backup, or
|
||||
# provider-side look at the VPS while the passphrase isn't actively loaded
|
||||
# shows only ciphertext, instead of showing your actual files.
|
||||
#
|
||||
# This also does NOT migrate any existing plainly-shared data into the new
|
||||
# encrypted store automatically — that's a real "am I about to overwrite/
|
||||
# lose something" decision this script shouldn't make for you. If you have
|
||||
# an existing share you want to convert, mount the plaintext view this
|
||||
# script offers to create, move your files into it yourself (they land
|
||||
# encrypted in the cipherdir as you do), then repoint your Samba share's
|
||||
# `path =` at the cipherdir once you're done.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
info() { printf '\033[0;34m[INFO]\033[0m %s\n' "$*"; }
|
||||
ok() { printf '\033[0;32m[OK]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
|
||||
err() { printf '\033[0;31m[ERROR]\033[0m %s\n' "$*" >&2; }
|
||||
|
||||
[[ "$(id -u)" == "0" ]] || { err "Run with sudo: sudo bash $0"; exit 1; }
|
||||
|
||||
ACTUAL_USER="${SUDO_USER:-$USER}"
|
||||
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" | cut -d: -f6)"
|
||||
|
||||
# Prompts twice, hidden, matching — prints the password on success. IFS=
|
||||
# matters, not just -s/-r: a plain `read -r pw1` silently strips leading/
|
||||
# trailing whitespace even into a single variable, which would make the
|
||||
# passphrase saved to the passfile different from the one actually set on
|
||||
# the store — see services/vpn-data-mount.sh's own note on this, same bug.
|
||||
prompt_password_confirm() {
|
||||
local prompt="$1" pw1="" pw2=""
|
||||
while true; do
|
||||
echo -n " ${prompt}: " >&2
|
||||
IFS= read -r -s pw1; echo "" >&2
|
||||
echo -n " Confirm: " >&2
|
||||
IFS= read -r -s pw2; echo "" >&2
|
||||
if [ -n "$pw1" ] && [ "$pw1" = "$pw2" ]; then
|
||||
echo " Captured (${#pw1} characters)." >&2
|
||||
printf '%s' "$pw1"
|
||||
return 0
|
||||
fi
|
||||
echo " Passwords didn't match or were empty — try again." >&2
|
||||
done
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " gocryptfs setup — client-side encryption for a vpn-data-mount share"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo " This creates an encrypted directory (a 'cipherdir') on this box."
|
||||
echo " Point your Samba share's 'path =' at the cipherdir itself, not the"
|
||||
echo " plaintext view — the VPS should only ever receive ciphertext."
|
||||
echo ""
|
||||
|
||||
command -v gocryptfs >/dev/null 2>&1 || {
|
||||
info "Installing gocryptfs..."
|
||||
apt-get update -qq
|
||||
apt-get install -y gocryptfs -qq
|
||||
}
|
||||
|
||||
CIPHERDIR=""
|
||||
read -r -p " Directory to create as the encrypted store [${ACTUAL_HOME}/encrypted-share]: " CIPHERDIR
|
||||
CIPHERDIR="${CIPHERDIR:-${ACTUAL_HOME}/encrypted-share}"
|
||||
|
||||
DEFAULT_PASSFILE="${ACTUAL_HOME}/.gocryptfs-$(basename "$CIPHERDIR").pass"
|
||||
PASSFILE=""
|
||||
read -r -p " Where to save the passphrase file for the VPS to fetch [${DEFAULT_PASSFILE}]: " PASSFILE
|
||||
PASSFILE="${PASSFILE:-$DEFAULT_PASSFILE}"
|
||||
|
||||
if [[ -f "${CIPHERDIR}/gocryptfs.conf" ]]; then
|
||||
warn "${CIPHERDIR} is already a gocryptfs store — leaving it as-is."
|
||||
[[ -f "$PASSFILE" ]] || warn "But $PASSFILE doesn't exist — the VPS-side decrypt step needs it. Re-run and use the same passphrase you set this store up with, or create $PASSFILE by hand with 'username=...' replaced by just the passphrase on one line."
|
||||
else
|
||||
if [[ -f "$PASSFILE" ]]; then
|
||||
err "$PASSFILE already exists but $CIPHERDIR isn't an initialized store — refusing to guess whether they're supposed to match. Remove one or point at a different path and re-run."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Set a passphrase for this store now. This is the ONE thing that"
|
||||
echo " decrypts it — write it down somewhere safe. There is no recovery"
|
||||
echo " if it's lost; everything inside becomes permanently unreadable."
|
||||
echo ""
|
||||
PASSPHRASE="$(prompt_password_confirm "Passphrase for the new encrypted store")"
|
||||
|
||||
mkdir -p "$(dirname "$PASSFILE")"
|
||||
printf '%s\n' "$PASSPHRASE" > "$PASSFILE"
|
||||
chmod 600 "$PASSFILE"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$PASSFILE"
|
||||
|
||||
mkdir -p "$CIPHERDIR"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$CIPHERDIR"
|
||||
sudo -u "$ACTUAL_USER" gocryptfs -passfile "$PASSFILE" -init "$CIPHERDIR"
|
||||
ok "Encrypted store created at $CIPHERDIR"
|
||||
ok "Passphrase saved to $PASSFILE (owner-only, 600)"
|
||||
warn "Anyone who can read $PASSFILE can decrypt everything in $CIPHERDIR — it's exactly as sensitive as the data itself. It only needs to be readable by $ACTUAL_USER and root; treat SSH access to this account accordingly, since that's how the VPS fetches it."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
MOUNTPOINT=""
|
||||
read -r -p " Also mount the plaintext view locally now, so you can use it here? Enter a mount point, or leave blank to skip: " MOUNTPOINT
|
||||
if [[ -n "$MOUNTPOINT" ]]; then
|
||||
mkdir -p "$MOUNTPOINT"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$MOUNTPOINT"
|
||||
if sudo -u "$ACTUAL_USER" gocryptfs -passfile "$PASSFILE" "$CIPHERDIR" "$MOUNTPOINT"; then
|
||||
ok "Mounted plaintext view at $MOUNTPOINT (unmount later with: fusermount -u $MOUNTPOINT)"
|
||||
else
|
||||
warn "Mount failed — the store and passfile are still set up correctly, this just means you'll need to mount it yourself: gocryptfs -passfile $PASSFILE $CIPHERDIR $MOUNTPOINT"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Next steps"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo " 1. Point your Samba share's 'path =' at the cipherdir itself:"
|
||||
echo " $CIPHERDIR"
|
||||
echo " (not the plaintext mount point above, if you made one) — this"
|
||||
echo " tool doesn't edit smb.conf for you, same as vpn-data-mount.sh's"
|
||||
echo " own read-only stance on the VPS side. If this share doesn't"
|
||||
echo " exist yet, tools/mount-network-drive.sh has no share-creation"
|
||||
echo " flow either — use Samba's own tools (smb.conf + smbpasswd)."
|
||||
echo ""
|
||||
echo " 2. On the VPS, run (or re-run) 'sudo ./setup.sh vpn-data-mount'."
|
||||
echo " After it mounts this share over CIFS as usual, say yes when it"
|
||||
echo " asks whether to layer gocryptfs decryption on top, and give it"
|
||||
echo " this path when asked for the passphrase file:"
|
||||
echo " $PASSFILE"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user