c00ca055f2
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>
242 lines
11 KiB
Bash
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."
|