Files
57_Wolve c00ca055f2 feat(copyparty): add file-server deployment with SFTP/FTPS + security-notices updater
New deployments/copyparty/: copyparty (copyparty/ac) behind Caddy/LE for the
web UI/WebDAV, plus its own SFTP (password auth) and FTPS listeners published
directly. Ships update.sh, which drives container updates off copyparty's
security-advisories API (api.copyparty.eu/advisories) -- policies latest|security|off.

- Real client IP end-to-end: Caddy XFF/X-Real-IP + copyparty xff-src: lan.
- SFTP host key + self-signed FTPS cert generated/persisted in /cfg; admin
  password generated on first deploy; conf auto-included via the image's % /cfg.
- Firewall opens 80/443 + SFTP/FTPS + passive range (colon form for ports.d).
- Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 15:56:24 -05:00

242 lines
11 KiB
Bash

#!/usr/bin/env bash
#
# automations.sh -- one command to run or deploy anything in this repo.
#
# Run it two ways:
#
# 1. One-liner on a fresh target host (clones the repo, then launches):
# curl -fsSL https://git.anomalous.dev/57_Wolve/automations/raw/branch/main/automations.sh \
# | REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git bash
#
# 2. From a clone:
# ./automations.sh
#
# It opens a Gum wizard (auto-installed) that lets you:
# • Mode: deploy on THIS host, or build deploy.sh artifacts locally.
# • Pick any deployment (pocket-id, beszel, headscale, webfinger, squid,
# copyparty, simplex) or any generic script (harden-ssh, harden-jumphost,
# sshuser, auto-update).
# Shared defaults come from globals/ (see globals/README.md).
#
# Non-interactive: set SKIP_PROMPTS=1 plus the needed vars and pipe the menu
# choices in, or just call the underlying deployments/<name>/deploy.sh
# directly -- they all honor SKIP_PROMPTS=1.
set -euo pipefail
# ----------------------------------------------------------------------------
# Self-locate, or bootstrap by cloning the repo (one-liner / piped form).
# ----------------------------------------------------------------------------
_self="${BASH_SOURCE[0]:-}"
if [[ -n "$_self" && -f "$(cd "$(dirname "$_self")" 2>/dev/null && pwd)/scripts/lib.sh" ]]; then
ROOT="$(cd "$(dirname "$_self")" && pwd)"
else
# Piped via curl: we don't have the repo on disk. Clone it, then re-exec.
: "${REPO_URL:=}"
: "${REPO_BRANCH:=main}"
[[ -n "$REPO_URL" ]] || {
echo "[x] Running standalone (piped). Set REPO_URL=... so I can clone the repo." >&2
exit 1
}
# We need git to clone, but oslib.sh's pkg_install isn't on disk yet (that's
# what we're cloning). Install git inline across the supported package
# managers -- apk (Alpine), apt-get (Debian/Ubuntu), dnf/yum (Alma/RHEL).
if ! command -v git >/dev/null 2>&1; then
echo "[+] git not found; installing it..." >&2
if command -v apk >/dev/null 2>&1; then apk add -q git || true
elif command -v apt-get >/dev/null 2>&1; then { apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git; } || true
elif command -v dnf >/dev/null 2>&1; then dnf install -y -q git || true
elif command -v yum >/dev/null 2>&1; then yum install -y -q git || true
fi
fi
command -v git >/dev/null 2>&1 || {
echo "[x] git is required to clone the repo, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install git, then re-run." >&2
exit 1
}
_tmp="$(mktemp -d -t automations.XXXXXX)"
echo "[+] Cloning $REPO_URL ($REPO_BRANCH)..."
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" "$_tmp"
exec bash "$_tmp/automations.sh" "$@"
fi
# shellcheck source=scripts/lib.sh
. "$ROOT/scripts/lib.sh"
load_globals
DEPLOYMENTS=(pocket-id beszel headscale webfinger squid copyparty simplex)
SCRIPTS=(setup-host harden-ssh harden-jumphost sshuser auto-update)
# ----------------------------------------------------------------------------
# Prompt helpers (gum). `ask` records each answer in ENVS for passing onward.
# ----------------------------------------------------------------------------
ENVS=()
ask() { # <VAR> <label> [password|optional]
local var="$1" label="$2" mode="${3:-}"
local cur="${!var:-}" val # indirect ref needs var already declared
if [[ "$mode" == "password" ]]; then
val="$(gum input --password --header "$label")"
else
val="$(gum input --header "$label" --value "$cur" --placeholder "$cur")"
fi
[[ "$mode" == "optional" && -z "$val" ]] && return 0
printf -v "$var" '%s' "$val"
ENVS+=("$var=$val")
}
# Which values to ask for, per deployment. Generated secrets are produced by
# the deploy scripts themselves and are intentionally not listed here.
ask_deployment_vars() {
case "$1" in
pocket-id)
ask POCKETID_DOMAIN "Public hostname (e.g. id.example.com)"
ask ACME_EMAIL "Let's Encrypt email"
ask BASE_DOMAIN "WebFinger base domain (blank = none / use webfinger deployment)" optional
if [[ -n "${BASE_DOMAIN:-}" ]]; then ask REDIRECT_URL "Redirect target for the base domain"; fi ;;
beszel)
ask BESZEL_DOMAIN "Public hostname (e.g. monitoring.example.com)"
ask ACME_EMAIL "Let's Encrypt email" ;;
headscale)
ask HEADSCALE_DOMAIN "headscale hostname (e.g. hs.example.com)"
ask ACME_EMAIL "Let's Encrypt email"
ask TAILNET_DOMAIN "Tailnet MagicDNS base (e.g. tail.example.com)"
ask POCKETID_DOMAIN "OIDC issuer hostname (your pocket-id)"
ask OIDC_CLIENT_ID "OIDC client_id (from pocket-id)"
ask OIDC_CLIENT_SECRET "OIDC client_secret (from pocket-id)" password
ask HEADPLANE_OIDC_CLIENT_ID "headplane UI OIDC client_id (blank = API-key login)" optional
if [[ -n "${HEADPLANE_OIDC_CLIENT_ID:-}" ]]; then ask HEADPLANE_OIDC_CLIENT_SECRET "headplane UI OIDC client_secret" password; fi ;;
webfinger)
ask BASE_DOMAIN "Apex domain to serve (e.g. example.com)"
ask ISSUER_URL "OIDC issuer URL (e.g. https://auth.example.com)"
ask REDIRECT_URL "Redirect target for other traffic (e.g. https://example.org)"
ask ACME_EMAIL "Let's Encrypt email" ;;
squid)
ask TRUSTED_CIDR "Trusted client CIDR(s) allowed to use the proxy (e.g. 100.64.0.0/10)"
ask BIND_ADDR "Host IP to bind the proxy on (blank = 0.0.0.0)" optional
ask CACHE_SIZE_MB "On-disk cache size in MB (blank = 5000)" optional
ask CACHE_ONLY_LISTED "Cache ONLY listed domains? (1=yes, blank=boost mode)" optional ;;
copyparty)
ask COPYPARTY_DOMAIN "Public hostname for the web UI (e.g. files.example.com)"
ask ACME_EMAIL "Let's Encrypt email"
ask DATA_DIR "Host data folder shared as the root (blank = /srv/copyparty/data)" optional
ask FTP_NAT "Public IP for passive FTPS via NAT (blank = none)" optional
ask UPDATE_POLICY "Auto-update policy: latest | security | off (blank = latest)" optional ;;
simplex)
ask DOMAIN "Apex domain (creates smp.DOMAIN, xftp.DOMAIN)"
ask ACME_EMAIL "Let's Encrypt email"
ask XFTP_QUOTA "XFTP disk quota" optional
ask SSH_PORT "SSH port" optional
ask ALLOWED_IP "Your IP to whitelist in sshguard" optional ;;
esac
}
ask_script_vars() {
case "$1" in
setup-host)
ask HOST "Hostname <svc>-<n> or FQDN (e.g. sto-1)"
ask BASE_DOMAIN "Base domain" optional
ask DATACENTER "Data center label" optional ;;
harden-ssh)
ask SSH_PORT "SSH port to listen on" optional
ask ALLOWED_IP "Your IP to whitelist in sshguard" optional
ask NTFY_URL "ntfy login-notify URL (blank to skip)" optional
if [[ -n "${NTFY_URL:-}" ]]; then ask NTFY_TOKEN "ntfy bearer token (blank if unauth publish)" password; fi ;;
harden-jumphost)
ask SSH_PORT "SSH port to listen on" optional
ask ALLOWED_IP "Your IP to whitelist in sshguard" optional
ask JUMP_TARGETS "Allowed ProxyJump targets (host:port, space-separated)" optional
ask NTFY_URL "ntfy login-notify URL (blank to skip)" optional
if [[ -n "${NTFY_URL:-}" ]]; then ask NTFY_TOKEN "ntfy bearer token (blank if unauth publish)" password; fi ;;
auto-update)
ask AUTO_REBOOT "Auto-reboot when needed? (0=never, 1=always, idle=when no SSH active)" optional
ask ALLOW_RELEASE_UPGRADE "Also upgrade to a new Alpine stable release? (1/0)" optional ;;
sshuser)
: ;; # sshuser.sh has its own interactive interface
esac
}
require_root() {
[[ $EUID -eq 0 ]] || _die "Deploying on this host must run as root."
os_detect # validates the distro is supported (Alpine/Debian/Alma)
}
# ----------------------------------------------------------------------------
# Dispatch
# ----------------------------------------------------------------------------
bootstrap_deployment() {
local name="$1"
require_root
ask_deployment_vars "$name"
if [[ "$name" == "simplex" ]]; then
# install-simplex.sh re-clones REPO_URL and runs harden + deploy + backup.
[[ -n "${REPO_URL:-}" ]] || _die "REPO_URL is required for simplex (set it in globals.env)."
_log "Launching simplex installer..."
env "${ENVS[@]}" REPO_URL="$REPO_URL" REPO_BRANCH="${REPO_BRANCH:-main}" SKIP_PROMPTS=1 \
bash "$ROOT/deployments/simplex/install-simplex.sh"
else
_log "Deploying $name on this host..."
env "${ENVS[@]}" SKIP_PROMPTS=1 bash "$ROOT/deployments/$name/deploy.sh"
fi
}
bootstrap_script() {
local name="$1"
require_root
ask_script_vars "$name"
# auto-update from the menu means "schedule the daily job".
local subcmd=""
[[ "$name" == "auto-update" ]] && subcmd="install"
_log "Running $name on this host..."
env "${ENVS[@]}" FORCE=1 bash "$ROOT/scripts/$name.sh" $subcmd
}
build_deployment() {
local name="$1"
if [[ "$name" == "simplex" ]]; then
_warn "simplex has no embedded-archive build step; it deploys via install-simplex.sh."
return 0
fi
local dir="$ROOT/deployments/$name"
[[ -f "$dir/build.sh" ]] || _die "$name has no build.sh."
_log "Building $name/deploy.sh..."
bash "$dir/build.sh"
if gum confirm "scp $name/deploy.sh to a host now?"; then
local target port
target="$(gum input --header "scp target (user@host)" --placeholder "root@host")"
port="$(gum input --header "SSH port" --value "${SSH_PORT:-22}")"
[[ -n "$target" ]] || { _warn "No target given; skipping scp."; return 0; }
scp -P "${port:-22}" "$dir/deploy.sh" "$target:"
_log "Copied. On the host, run: bash deploy.sh"
fi
}
# ----------------------------------------------------------------------------
# Wizard
# ----------------------------------------------------------------------------
ensure_gum
MODE="$(gum choose --header "What do you want to do?" \
"Deploy on this host" \
"Build deploy.sh artifacts locally")"
case "$MODE" in
"Deploy on this host")
CHOICE="$(printf '%s\n' \
"${DEPLOYMENTS[@]/#/deploy: }" \
"${SCRIPTS[@]/#/script: }" \
| gum choose --header "Pick a deployment or script")"
kind="${CHOICE%%: *}"; name="${CHOICE#*: }"
case "$kind" in
deploy) bootstrap_deployment "$name" ;;
script) bootstrap_script "$name" ;;
*) _die "Nothing selected." ;;
esac ;;
"Build deploy.sh artifacts locally")
name="$(printf '%s\n' "${DEPLOYMENTS[@]}" | gum choose --header "Pick a deployment to build")"
[[ -n "$name" ]] || _die "Nothing selected."
build_deployment "$name" ;;
*)
_die "Nothing selected." ;;
esac
_log "Done."