diff --git a/commands/bootstrap.sh b/commands/bootstrap.sh new file mode 100755 index 0000000..5f7b22f --- /dev/null +++ b/commands/bootstrap.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# deployor bootstrap — OS plumbing for a pristine Debian box. +# Convergent: safe to re-run; a second run changes nothing. +set -euo pipefail + +log() { printf 'deployor-bootstrap: %s\n' "$*"; } +warn() { printf 'deployor-bootstrap: WARNING: %s\n' "$*" >&2; } +die() { printf 'deployor-bootstrap: ERROR: %s\n' "$*" >&2; exit "${2:-1}"; } + +usage() { + cat <<'EOF' +usage: deployor bootstrap [--hostname ] [--ts-tag ] + + --hostname tailnet hostname (default: the role name) + --ts-tag tailnet tag to advertise (default: tag:server) + +Provide the single-use tailscale pre-auth key via the TS_AUTHKEY env var, or +enter it at the interactive prompt. It is used once and never written to disk. +EOF +} + +# --- args (validated before the root check, so errors are testable) --------- +ROLE="${1:-}" +case "$ROLE" in + control-plane|workload) shift ;; + -h|--help) usage; exit 0 ;; + "") usage >&2; die "role required (control-plane|workload)" 2 ;; + *) die "unknown role: $ROLE (want control-plane|workload)" 2 ;; +esac + +TS_HOSTNAME="$ROLE" +TS_TAG="tag:server" +while [ $# -gt 0 ]; do + case "$1" in + --hostname) + [ $# -ge 2 ] || die "--hostname needs a value" 2 + TS_HOSTNAME="$2"; shift 2 ;; + --ts-tag) + [ $# -ge 2 ] || die "--ts-tag needs a value" 2 + TS_TAG="$2"; shift 2 ;; + *) die "unknown flag: $1" 2 ;; + esac +done + +# --- guards ------------------------------------------------------------------ +[ "$(id -u)" -eq 0 ] || die "must run as root" +if [ -r /etc/os-release ]; then + # shellcheck source=/dev/null + . /etc/os-release + case "${ID:-} ${ID_LIKE:-}" in + *debian*) ;; + *) warn "not a Debian-family system (ID=${ID:-unknown}); proceeding anyway" ;; + esac +else + warn "cannot read /etc/os-release; proceeding anyway" +fi + +# --- pre-auth key (env override, else prompt; never touches disk) ------------ +if [ -z "${TS_AUTHKEY:-}" ]; then + read -rsp "tailscale pre-auth key (single-use, tagged, <=1h expiry): " TS_AUTHKEY + echo +fi +[ -n "$TS_AUTHKEY" ] || die "empty pre-auth key" + +# --- packages ---------------------------------------------------------------- +export DEBIAN_FRONTEND=noninteractive +log "installing base packages" +apt-get update -qq +apt-get install -y -qq curl ca-certificates unattended-upgrades + +# enable periodic unattended upgrades (canonical file; idempotent overwrite) +cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; +EOF + +# --- sshd hardening (restart only when the drop-in actually changed) --------- +DROPIN=/etc/ssh/sshd_config.d/99-deployor.conf +TMP="$(mktemp)" +cat > "$TMP" <<'EOF' +PermitRootLogin prohibit-password +PasswordAuthentication no +EOF +if ! cmp -s "$TMP" "$DROPIN" 2>/dev/null; then + install -m 0644 "$TMP" "$DROPIN" + systemctl restart ssh + log "sshd hardening drop-in installed" +else + log "sshd hardening drop-in already in place" +fi +rm -f "$TMP" + +# --- tailscale ---------------------------------------------------------------- +if ! command -v tailscale >/dev/null 2>&1; then + log "installing tailscale" + curl -fsSL https://tailscale.com/install.sh | sh +fi +if tailscale status >/dev/null 2>&1; then + log "tailnet already joined; skipping tailscale up" +else + log "joining tailnet as ${TS_HOSTNAME} (${TS_TAG})" + tailscale up --authkey="$TS_AUTHKEY" --hostname="$TS_HOSTNAME" --advertise-tags="$TS_TAG" +fi + +log "done — role ${ROLE}, hostname ${TS_HOSTNAME}" +if [ "$ROLE" = "control-plane" ]; then + log "next: deployor coolify install --version " +fi diff --git a/test/cli.sh b/test/cli.sh index cb5d25f..94decc6 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -31,6 +31,17 @@ check "help exits 0" 0 "usage:" "$ROOT/bin/deployor" help check "unknown command exits 2" 2 "unknown command" "$ROOT/bin/deployor" frobnicate check "bare coolify shows usage, exit 2" 2 "usage:" "$ROOT/bin/deployor" coolify +check "bootstrap: role required, exit 2" 2 "role required" "$ROOT/commands/bootstrap.sh" +check "bootstrap: --help exits 0" 0 "usage:" "$ROOT/commands/bootstrap.sh" --help +check "bootstrap: unknown role exits 2" 2 "unknown role" "$ROOT/commands/bootstrap.sh" potato +check "bootstrap: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap.sh" workload --nope +check "bootstrap: hostname needs value" 2 "needs a value" "$ROOT/commands/bootstrap.sh" workload --hostname +if [ "$(id -u)" -ne 0 ]; then + check "bootstrap: refuses non-root" 1 "must run as root" env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload +else + echo "skip: bootstrap non-root refusal (running as root)" +fi + echo "---" echo "$PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ]