Let the additional-mirror setup read Garage credentials over SSH

Extends the "ADDITIONAL MIRROR" section (previously SFTP-only) with a
type choice: SFTP, or S3 against a Garage instance already running on
that box. For the S3 path, this script never asks the operator to retype
a bucket name or key — it SSHes to the destination, reads
~/docker/garage/.env directly (the real, currently-configured values,
generated once by services/garage.sh and never touched again on its own
Update runs), and uses those for the dry-run verification and the
persisted mirror args. If Garage isn't installed there yet, it says so
plainly with the exact install command instead of failing cryptically or
silently skipping.

Also removed the last hardcoded suggestions from services/garage.sh
itself ("kopia-backup" / "kopia" as fixed prompt defaults) — replaced
with a freshly-generated suggestion each run (timestamp-suffixed), so
nothing about the bucket/key name is a fixed string baked into this repo
at any point in the chain; it's always the operator's actual choice, read
back live wherever it's needed.

Verified end-to-end against a mocked ssh (returning realistic
~/docker/garage/.env content) covering both outcomes: Garage installed
with a real bucket/key correctly parsed, dry-run run, and persisted; and
Garage missing, correctly warning with the install command and leaving
backup.conf untouched either way.
This commit is contained in:
Claude
2026-08-15 12:35:21 +00:00
parent 01ca0a7b09
commit 74cab14f86
2 changed files with 129 additions and 48 deletions
+115 -44
View File
@@ -932,8 +932,19 @@ install_backup() {
echo ""
local _ADD_SFTP_MIRROR=""
local _sftp_default_host="${DR_SYNC_HOST:-}"
prompt_yn " Add a direct SFTP mirror to another box$( [ -n "$_sftp_default_host" ] && echo " (e.g. $_sftp_default_host, same as the DR-spare above)")? (y/n):" "n" _ADD_SFTP_MIRROR
prompt_yn " Add a direct mirror to another box$( [ -n "$_sftp_default_host" ] && echo " (e.g. $_sftp_default_host, same as the DR-spare above)")? (y/n):" "n" _ADD_SFTP_MIRROR
if [[ "$_ADD_SFTP_MIRROR" =~ ^[Yy]$ ]]; then
echo ""
echo " 1) SFTP — Kopia's own SFTP backend, syncs straight to a directory"
echo " 2) S3 (Garage) — a Garage instance already running on that box"
echo " (services/garage.sh) — reuses Kopia's S3 backend, the same one"
echo " already proven reliable for the Backblaze B2 mirror above,"
echo " instead of Kopia's less-exercised SFTP backend"
echo ""
local _MIRROR_TYPE_CHOICE=""
prompt_text " Mirror type [1]:" "1" _MIRROR_TYPE_CHOICE
_MIRROR_TYPE_CHOICE="${_MIRROR_TYPE_CHOICE:-1}"
local _SFTP_DEST=""
prompt_text " SSH destination, user@host (~/.ssh/config aliases work too):" "$_sftp_default_host" _SFTP_DEST
if [ -z "$_SFTP_DEST" ]; then
@@ -966,57 +977,117 @@ install_backup() {
# same Port line correctly.
_SFTP_PORT="${_SFTP_PORT:-22}"
log_info " Using ${_SFTP_USER}@${_SFTP_HOSTNAME}:${_SFTP_PORT} for this mirror (resolved via ~/.ssh/config)."
# Suggest a subdirectory of the DR-spare's own path (if one is
# configured) rather than an unrelated default — reusing the
# same spare location the operator already picked, but in its
# own /kopia-data subdirectory so the actual repository data
# (Kopia's own blob-store files) doesn't end up visually mixed
# in with the two plain config files the DR-spare sync writes
# directly into DR_SYNC_PATH itself.
local _SFTP_PATH_DEFAULT="~/backups/kopia-mirror"
[ -n "${DR_SYNC_PATH:-}" ] && _SFTP_PATH_DEFAULT="${DR_SYNC_PATH%/}/kopia-data"
local _SFTP_PATH="" _MIRROR_NAME=""
prompt_text " Remote path for the repo:" "$_SFTP_PATH_DEFAULT" _SFTP_PATH
_SFTP_PATH="${_SFTP_PATH:-$_SFTP_PATH_DEFAULT}"
prompt_text " Short name for this mirror (letters/numbers/underscores):" "spare" _MIRROR_NAME
_MIRROR_NAME="${_MIRROR_NAME:-spare}"
local _MIRROR_NAME=""
prompt_text " Short name for this mirror (letters/numbers/underscores):" "$( [ "$_MIRROR_TYPE_CHOICE" = "2" ] && echo garage || echo spare )" _MIRROR_NAME
_MIRROR_NAME="${_MIRROR_NAME:-$( [ "$_MIRROR_TYPE_CHOICE" = "2" ] && echo garage || echo spare )}"
_MIRROR_NAME="${_MIRROR_NAME//[^a-zA-Z0-9_]/_}"
# sync-to sftp doesn't shell out to the system ssh client, so it
# needs an explicit key/known_hosts file rather than picking up
# whatever plain `ssh` already trusts automatically.
_backup_ensure_root_ssh_key "$_SFTP_DEST"
local _SFTP_KEYFILE="$_ROOT_SSH_KEYFILE"
if [ -z "$_SFTP_KEYFILE" ]; then
log_warning " No SSH key available for root — can't add this mirror."
elif ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" true 2>/dev/null; then
# Both types need at least a working passwordless SSH connection
# to this box — SFTP because Kopia authenticates with a keyfile
# over that same connection; S3/Garage because that's how this
# script reads Garage's own credentials remotely (the actual S3
# dry-run below talks straight to Garage's S3 API port, no SSH
# involved in that part at all).
if ! ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" true 2>/dev/null; then
log_warning " Couldn't SSH to $_SFTP_DEST without a password — not adding this"
log_warning " mirror until that works: ssh-copy-id $_SFTP_DEST"
else
log_info "Verifying SFTP mirror (dry-run sync against the 'default' repo)..."
local _sftp_err
if _sftp_err="$(env KOPIA_PASSWORD="${DEST_PASSWORDS[default]}" "$KOPIA_BIN" \
--config-file="${DEST_CONFIGS[default]}" repository sync-to sftp \
--host="$_SFTP_HOSTNAME" --port="$_SFTP_PORT" --username="$_SFTP_USER" --path="$_SFTP_PATH" \
--keyfile="$_SFTP_KEYFILE" --known-hosts=/root/.ssh/known_hosts \
--dry-run 2>&1)"; then
EXTRA_MIRROR_TYPE["$_MIRROR_NAME"]="sftp"
EXTRA_MIRROR_ARGS["$_MIRROR_NAME"]="--host=$_SFTP_HOSTNAME --port=$_SFTP_PORT --username=$_SFTP_USER --path=$_SFTP_PATH --keyfile=$_SFTP_KEYFILE --known-hosts=/root/.ssh/known_hosts"
# Reusing an existing mirror name reconfigures it (the
# associative-array assignments above already do that)
# without duplicating it in the space-separated name list.
if [[ " $EXTRA_MIRROR_NAMES " != *" $_MIRROR_NAME "* ]]; then
if [ -z "$EXTRA_MIRROR_NAMES" ]; then
EXTRA_MIRROR_NAMES="$_MIRROR_NAME"
elif [ "$_MIRROR_TYPE_CHOICE" = "2" ]; then
# ── S3 (Garage) ──────────────────────────────────────────────
# Read the real bucket/key/port straight from the remote
# instance's own .env rather than asking the operator to
# retype them here — those values are generated once by
# services/garage.sh and never touched again on its own
# Update runs, so this is always reading the box's actual
# current configuration, not something baked in here.
local _garage_env
_garage_env="$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$_SFTP_DEST" "cat ~/docker/garage/.env 2>/dev/null")"
if [ -z "$_garage_env" ]; then
log_warning " Garage isn't installed on $_SFTP_DEST yet — not adding this mirror."
log_warning " Install it there first, then re-run this installer:"
log_warning " sudo ./setup.sh garage (or: sudo bash garage.sh, on a box without this repo)"
else
local _g_bucket _g_key_id _g_key_secret _g_port
_g_bucket="$(echo "$_garage_env" | sed -nE "s/^GARAGE_BUCKET='?([^']*)'?\$/\1/p")"
_g_key_id="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_ID='?([^']*)'?\$/\1/p")"
_g_key_secret="$(echo "$_garage_env" | sed -nE "s/^GARAGE_ACCESS_KEY_SECRET='?([^']*)'?\$/\1/p")"
_g_port="$(echo "$_garage_env" | sed -nE "s/^GARAGE_S3_API_PORT=([0-9]+)\$/\1/p")"
if [ -z "$_g_bucket" ] || [ -z "$_g_key_id" ] || [ -z "$_g_key_secret" ] || [ -z "$_g_port" ]; then
log_warning " Garage is installed on $_SFTP_DEST but its .env is missing something"
log_warning " expected — check ~/docker/garage/.env there. Not adding this mirror."
else
local _g_endpoint="${_SFTP_HOSTNAME}:${_g_port}"
log_info "Verifying S3 (Garage) mirror at ${_g_endpoint}, bucket '$_g_bucket' (dry-run)..."
local _s3_err
if _s3_err="$(env KOPIA_PASSWORD="${DEST_PASSWORDS[default]}" "$KOPIA_BIN" \
--config-file="${DEST_CONFIGS[default]}" repository sync-to s3 \
--bucket="$_g_bucket" --endpoint="$_g_endpoint" \
--access-key="$_g_key_id" --secret-access-key="$_g_key_secret" \
--disable-tls --dry-run 2>&1)"; then
EXTRA_MIRROR_TYPE["$_MIRROR_NAME"]="s3"
EXTRA_MIRROR_ARGS["$_MIRROR_NAME"]="--bucket=$_g_bucket --endpoint=$_g_endpoint --access-key=$_g_key_id --secret-access-key=$_g_key_secret --disable-tls"
if [[ " $EXTRA_MIRROR_NAMES " != *" $_MIRROR_NAME "* ]]; then
if [ -z "$EXTRA_MIRROR_NAMES" ]; then
EXTRA_MIRROR_NAMES="$_MIRROR_NAME"
else
EXTRA_MIRROR_NAMES="$EXTRA_MIRROR_NAMES $_MIRROR_NAME"
fi
fi
log_success " S3 (Garage) mirror '$_MIRROR_NAME' verified — will run after every backup."
else
EXTRA_MIRROR_NAMES="$EXTRA_MIRROR_NAMES $_MIRROR_NAME"
log_warning " S3 (Garage) dry-run failed — not adding this mirror:"
log_warning " $_s3_err"
fi
fi
log_success " SFTP mirror '$_MIRROR_NAME' verified — will run after every backup."
fi
else
# ── SFTP ─────────────────────────────────────────────────────
# Suggest a subdirectory of the DR-spare's own path (if one is
# configured) rather than an unrelated default — reusing the
# same spare location the operator already picked, but in its
# own /kopia-data subdirectory so the actual repository data
# (Kopia's own blob-store files) doesn't end up visually mixed
# in with the two plain config files the DR-spare sync writes
# directly into DR_SYNC_PATH itself.
local _SFTP_PATH_DEFAULT="~/backups/kopia-mirror"
[ -n "${DR_SYNC_PATH:-}" ] && _SFTP_PATH_DEFAULT="${DR_SYNC_PATH%/}/kopia-data"
local _SFTP_PATH=""
prompt_text " Remote path for the repo:" "$_SFTP_PATH_DEFAULT" _SFTP_PATH
_SFTP_PATH="${_SFTP_PATH:-$_SFTP_PATH_DEFAULT}"
# sync-to sftp doesn't shell out to the system ssh client, so
# it needs an explicit key/known_hosts file rather than
# picking up whatever plain `ssh` already trusts automatically.
_backup_ensure_root_ssh_key "$_SFTP_DEST"
local _SFTP_KEYFILE="$_ROOT_SSH_KEYFILE"
if [ -z "$_SFTP_KEYFILE" ]; then
log_warning " No SSH key available for root — can't add this mirror."
else
log_warning " SFTP dry-run failed — not adding this mirror:"
log_warning " $_sftp_err"
log_info "Verifying SFTP mirror (dry-run sync against the 'default' repo)..."
local _sftp_err
if _sftp_err="$(env KOPIA_PASSWORD="${DEST_PASSWORDS[default]}" "$KOPIA_BIN" \
--config-file="${DEST_CONFIGS[default]}" repository sync-to sftp \
--host="$_SFTP_HOSTNAME" --port="$_SFTP_PORT" --username="$_SFTP_USER" --path="$_SFTP_PATH" \
--keyfile="$_SFTP_KEYFILE" --known-hosts=/root/.ssh/known_hosts \
--dry-run 2>&1)"; then
EXTRA_MIRROR_TYPE["$_MIRROR_NAME"]="sftp"
EXTRA_MIRROR_ARGS["$_MIRROR_NAME"]="--host=$_SFTP_HOSTNAME --port=$_SFTP_PORT --username=$_SFTP_USER --path=$_SFTP_PATH --keyfile=$_SFTP_KEYFILE --known-hosts=/root/.ssh/known_hosts"
# Reusing an existing mirror name reconfigures it (the
# associative-array assignments above already do that)
# without duplicating it in the space-separated name list.
if [[ " $EXTRA_MIRROR_NAMES " != *" $_MIRROR_NAME "* ]]; then
if [ -z "$EXTRA_MIRROR_NAMES" ]; then
EXTRA_MIRROR_NAMES="$_MIRROR_NAME"
else
EXTRA_MIRROR_NAMES="$EXTRA_MIRROR_NAMES $_MIRROR_NAME"
fi
fi
log_success " SFTP mirror '$_MIRROR_NAME' verified — will run after every backup."
else
log_warning " SFTP dry-run failed — not adding this mirror:"
log_warning " $_sftp_err"
fi
fi
fi
fi
+14 -4
View File
@@ -174,11 +174,16 @@ install_garage() {
find_free_port RPC_PORT "$RPC_PORT"
find_free_port ADMIN_PORT "$ADMIN_PORT"
# Suggested defaults are generated fresh at runtime, not fixed strings
# baked into this script — same reasoning as not hardcoding what a
# remote reader (services/backup.sh) should expect the name to be:
# this is the operator's name to pick, not this repo's.
local BUCKET_NAME="" KEY_NAME=""
prompt_text " Bucket name:" "kopia-backup" BUCKET_NAME
BUCKET_NAME="${BUCKET_NAME:-kopia-backup}"
prompt_text " Access key name:" "kopia" KEY_NAME
KEY_NAME="${KEY_NAME:-kopia}"
local _default_bucket="kopia-$(date +%s)" _default_key="key-$(date +%s)"
prompt_text " Bucket name:" "$_default_bucket" BUCKET_NAME
BUCKET_NAME="${BUCKET_NAME:-$_default_bucket}"
prompt_text " Access key name:" "$_default_key" KEY_NAME
KEY_NAME="${KEY_NAME:-$_default_key}"
mkdir -p "$DIR"/{data,meta}
ensure_docker_dir_ownership "$DIR"
@@ -232,6 +237,11 @@ COMPOSE
cat > .env << ENV
TZ=${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}
# Read directly (over SSH) by another box's services/backup.sh when adding
# this instance as a Kopia sync-to s3 mirror target — keep this key name
# stable, other scripts depend on it.
GARAGE_S3_API_PORT=${S3_API_PORT}
ENV
chmod 600 .env