Files
automations/automations.sh
T
57_Wolve e7ba68790b Done — the Squid deployment is fully built, wired in, and statically verified on the feat/squid-proxy branch. ## What landed deployments/squid/ (13 files) plus wiring into [automations.sh](automations.sh:51), [README.md](README.md), and [.gitignore](.gitignore). All six approved design decisions are implemented: - **Self-built minimal Alpine image** — apk add squid (ssl-bump compiled in), openssl/tini. [entrypoint.sh](deployments/squid/entrypoint.sh) renders squid.conf and generates the cache policy from the lists at start. - **SSL-bump** peek→splice→bump with [splice-domains.txt](deployments/squid/splice-domains.txt) passthrough. - **Wildcard hostname caching** — leading-dot [cache-domains.txt](deployments/squid/cache-domains.txt) + optional regex; CACHE_ONLY_LISTED boost/allowlist toggle. - **HTML/dynamic storage gate**, with query strings exempt on boosted domains. - **CA lifecycle** — generated on first deploy via the built image, key 0600 on the host, staged to tmpfs for the container, never embedded, git-ignored. - **Deny-by-default http_access + BIND_ADDR pinning** as the real access gate (Docker bypasses INPUT). ## One bug caught & fixed The test of the entrypoint's policy generation showed it emitting ubuntu.com with an **unescaped dot** — the s/\./\\./g double-backslash is sed-implementation-dependent and silently failed. Switched to a [.] character class (ubuntu[.]com), portable across busybox/git-bash, re-embedded, re-audited. ## Verified vs. pending **Verified locally:** bash -n on all scripts, LF endings, launcher parses, embedded archive byte-matches the loose files, and the cache-policy generation produces correct Squid directives in both modes. **Pending a Docker host** (this Windows box has none, as the plan anticipated): the container functional smoke test — build, CA mint, a bumped HTTPS fetch, a TCP_HIT, and a splice check. A few version-sensitive specifics get confirmed there: tls-cert=/tls-key= option names, the security_file_certgen path, busybox install -o/-g, ssl::server_name subdomain matching, and the squid -z flag. (All isolated and easy to adjust if any differ.) ## Commit (for you to GPG-sign) Staged and ready. Per your convention I haven't committed — here's the message: `` feat(squid): add SSL-bump caching forward-proxy deployment New deployments/squid/: an explicit forward proxy with SSL-bump TLS interception (local CA, generated on first deploy) and hostname-targeted static-content caching. Unlike the other stacks it is a forward proxy, not a Caddy/Let's-Encrypt inbound site. - Self-built minimal Alpine image (apk squid ships ssl-bump); entrypoint renders squid.conf and generates the cache policy from the domain lists. - Wildcard hostname caching (cache-domains.txt leading-dot + optional cache-domains.regex); boost vs strict-allowlist toggle (CACHE_ONLY_LISTED). - Storage gate never caches HTML or dynamic content; query strings exempt on boosted domains so versioned static assets still cache. - splice-domains.txt passthrough for pinned/banking domains. - Deny-by-default http_access (TRUSTED_CIDR) + BIND_ADDR pinning; CA key 0600 on host, never embedded, git-ignored. - Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ` `bash git commit -S -F- <<'EOF' <paste message above> EOF ` Want me to **push feat/squid-proxy** once you've committed (you'd open the PR in Gitea), or hold? And if you can point me at a Docker host (or run docker compose build in /srv/squid` after a test deploy), I'll drive the functional smoke test and fix anything the live Squid version flags.
New deployments/squid/: an explicit forward proxy with SSL-bump TLS
interception (local CA, generated on first deploy) and hostname-targeted
static-content caching. Unlike the other stacks it is a forward proxy, not
a Caddy/Let's-Encrypt inbound site.
- Self-built minimal Alpine image (apk squid ships ssl-bump); entrypoint
  renders squid.conf and generates the cache policy from the domain lists.
- Wildcard hostname caching (cache-domains.txt leading-dot + optional
  cache-domains.regex); boost vs strict-allowlist toggle (CACHE_ONLY_LISTED).
- Storage gate never caches HTML or dynamic content; query strings exempt on
  boosted domains so versioned static assets still cache.
- splice-domains.txt passthrough for pinned/banking domains.
- Deny-by-default http_access (TRUSTED_CIDR) + BIND_ADDR pinning; CA key 0600
  on host, never embedded, git-ignored.
- Wired into automations.sh, README, .gitignore; cloud-init for fresh VMs.
2026-06-22 16:32:25 -05:00

221 lines
9.7 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, simplex)
# or any generic script (harden-ssh, harden-jumphost, sshuser).
# 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
}
command -v git >/dev/null 2>&1 || { command -v apk >/dev/null 2>&1 && apk add -q git; }
_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 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 ;;
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."