feat: unified launcher, multi-OS hardening, login alerts & auto-updates
Restructure around a single entry point (automations.sh) with a Gum wizard and a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on Alpine/Debian/Alma; seed root keys from globals/. - ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec - daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and opt-in Alpine stable-branch upgrades - generic + per-deployment cloud-init; Gitea release workflow on tag - README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
This commit is contained in:
58 files changed
+6225
-284
No files matched your search
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# deploy.sh -- deploy the headscale stack (caddy + headscale) on Alpine.
|
||||
#
|
||||
# Single-node, dedicated host: runs everything as root.
|
||||
#
|
||||
# What this does:
|
||||
# 1. Installs docker + docker-cli-compose if missing.
|
||||
# 2. Lays down docker-compose.yml, Caddyfile, and a substituted
|
||||
# config.yaml in $STACK_DIR.
|
||||
# 3. Generates .env on first run; existing .env is never overwritten.
|
||||
# 4. Prompts for required values not preset (HEADSCALE_DOMAIN,
|
||||
# ACME_EMAIL, TAILNET_DOMAIN, POCKETID_DOMAIN, OIDC_CLIENT_ID,
|
||||
# OIDC_CLIENT_SECRET).
|
||||
# 5. Substitutes those values into config.yaml from the embedded
|
||||
# template.
|
||||
# 6. Opens TCP 80/443 in UFW if it's installed and active (Tailscale
|
||||
# clients connect on 443; LE HTTP-01 needs 80).
|
||||
# 7. Pulls images, brings the stack up, waits for healthchecks.
|
||||
#
|
||||
# Idempotent: re-run to apply config changes / pull new images. Re-running
|
||||
# regenerates config.yaml from the current .env so edits to .env propagate
|
||||
# (but secrets in .env are never touched once seeded).
|
||||
#
|
||||
# Self-contained: docker-compose.yml, Caddyfile, config.yaml, .env.example
|
||||
# are embedded as a base64-encoded tar.gz at the bottom of this file.
|
||||
# Rebuild with build.sh after editing the loose source files.
|
||||
#
|
||||
# Usage:
|
||||
# bash deploy.sh # interactive prompts
|
||||
# HEADSCALE_DOMAIN=hs.example.com \
|
||||
# ACME_EMAIL=admin@example.com \
|
||||
# TAILNET_DOMAIN=tail.example.com \
|
||||
# POCKETID_DOMAIN=auth.example.com \
|
||||
# OIDC_CLIENT_ID=... OIDC_CLIENT_SECRET=... FORCE=1 bash deploy.sh
|
||||
#
|
||||
# STACK_DIR=/opt/headscale bash deploy.sh
|
||||
# SKIP_DOCKER_INSTALL=1 bash deploy.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${STACK_DIR:=/srv/headscale}"
|
||||
: "${SKIP_DOCKER_INSTALL:=0}"
|
||||
: "${FORCE:=0}"
|
||||
: "${SKIP_PROMPTS:=0}" # non-interactive: require values via env, no prompts
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && FORCE=1
|
||||
: "${HEADSCALE_DOMAIN:=}"
|
||||
: "${ACME_EMAIL:=}"
|
||||
: "${TAILNET_DOMAIN:=}"
|
||||
: "${POCKETID_DOMAIN:=}"
|
||||
: "${OIDC_CLIENT_ID:=}"
|
||||
: "${OIDC_CLIENT_SECRET:=}"
|
||||
|
||||
log() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[1;31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[[ $EUID -eq 0 ]] || die "Run as root."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OS detection + Docker install (Alpine / Debian / Alma). This deploy.sh is
|
||||
# self-contained (scp'd standalone), so the OS logic is inlined here instead
|
||||
# of sourced from scripts/oslib.sh.
|
||||
# ---------------------------------------------------------------------------
|
||||
osfam() {
|
||||
local id="" like=""
|
||||
if [[ -r /etc/os-release ]]; then
|
||||
id="$(. /etc/os-release 2>/dev/null && echo "${ID:-}")"
|
||||
like="$(. /etc/os-release 2>/dev/null && echo "${ID_LIKE:-}")"
|
||||
fi
|
||||
case " $id $like " in
|
||||
*" alpine "*) echo alpine ;;
|
||||
*" debian "*|*" ubuntu "*) echo debian ;;
|
||||
*" rhel "*|*" fedora "*|*" centos "*) echo rhel ;;
|
||||
*) echo "${id:-unknown}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "Docker already installed: $(docker --version)"
|
||||
else
|
||||
log "Installing Docker (OS: $(osfam))..."
|
||||
case "$(osfam)" in
|
||||
alpine) apk add -q docker docker-cli-compose openrc ;;
|
||||
debian|rhel) command -v curl >/dev/null 2>&1 || \
|
||||
{ command -v apt-get >/dev/null 2>&1 && apt-get install -y -qq curl; } || \
|
||||
{ command -v dnf >/dev/null 2>&1 && dnf install -y -q curl; }
|
||||
curl -fsSL https://get.docker.com | sh ;;
|
||||
*) die "Unsupported OS for auto Docker install. Set SKIP_DOCKER_INSTALL=1 and install Docker yourself." ;;
|
||||
esac
|
||||
fi
|
||||
if command -v rc-update >/dev/null 2>&1; then
|
||||
rc-update add docker default >/dev/null 2>&1 || true
|
||||
rc-service docker status >/dev/null 2>&1 || rc-service docker start
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl enable --now docker >/dev/null 2>&1 || systemctl start docker || true
|
||||
fi
|
||||
}
|
||||
|
||||
open_web_ports() {
|
||||
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
|
||||
log "ufw active -- allowing 80,443/tcp..."
|
||||
ufw allow 80/tcp >/dev/null; ufw allow 443/tcp >/dev/null
|
||||
elif command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
||||
log "firewalld active -- allowing http,https..."
|
||||
firewall-cmd -q --add-service=http --permanent
|
||||
firewall-cmd -q --add-service=https --permanent
|
||||
firewall-cmd -q --reload
|
||||
fi
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Extract embedded archive
|
||||
# ----------------------------------------------------------------------------
|
||||
SCRIPT_DIR=$(mktemp -d -t headscale-deploy.XXXXXX)
|
||||
trap 'rm -rf "$SCRIPT_DIR"' EXIT
|
||||
|
||||
extract_archive() {
|
||||
grep -a -A 9999999 '^__ARCHIVE_BELOW__$' "$0" \
|
||||
| tail -n +2 \
|
||||
| base64 -d \
|
||||
| tar -xz -C "$SCRIPT_DIR"
|
||||
}
|
||||
|
||||
if grep -q -a '^__ARCHIVE_BELOW__$' "$0"; then
|
||||
log "Extracting embedded deployment files..."
|
||||
extract_archive
|
||||
else
|
||||
die "No embedded archive found. Run build.sh to embed deployment files."
|
||||
fi
|
||||
|
||||
for f in docker-compose.yml Caddyfile config.yaml .env.example; do
|
||||
[[ -f "$SCRIPT_DIR/$f" ]] || die "Embedded archive missing $f"
|
||||
done
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Prompt for required vars
|
||||
# ----------------------------------------------------------------------------
|
||||
prompt() {
|
||||
local varname="$1" message="$2"
|
||||
local -n ref="$varname"
|
||||
if [[ -z "${ref:-}" ]]; then
|
||||
[[ "$SKIP_PROMPTS" == "1" ]] && die "$varname required (set it in the environment; running with SKIP_PROMPTS=1)."
|
||||
read -r -p "$message: " ref
|
||||
[[ -n "$ref" ]] || die "$varname required."
|
||||
fi
|
||||
}
|
||||
|
||||
prompt HEADSCALE_DOMAIN "Public hostname for headscale (e.g. hs.example.com)"
|
||||
prompt ACME_EMAIL "Let's Encrypt email"
|
||||
prompt TAILNET_DOMAIN "Tailnet base domain for MagicDNS (e.g. tail.example.com)"
|
||||
prompt POCKETID_DOMAIN "OIDC issuer hostname (your pocket-id, e.g. auth.example.com)"
|
||||
prompt OIDC_CLIENT_ID "OIDC client_id (from pocket-id)"
|
||||
prompt OIDC_CLIENT_SECRET "OIDC client_secret (from pocket-id)"
|
||||
|
||||
[[ "$HEADSCALE_DOMAIN" != "$TAILNET_DOMAIN" ]] \
|
||||
|| die "HEADSCALE_DOMAIN and TAILNET_DOMAIN must differ."
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Docker
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ "$SKIP_DOCKER_INSTALL" != "1" ]]; then
|
||||
install_docker
|
||||
fi
|
||||
|
||||
# Open 80/443 on whichever host firewall is active. (Tailscale clients connect
|
||||
# on 443; LE HTTP-01 needs 80. Note the IPv6-to-docker-proxy default-deny
|
||||
# gotcha on ufw -- opening the ports explicitly covers it.)
|
||||
open_web_ports
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Stack directory + files
|
||||
# ----------------------------------------------------------------------------
|
||||
log "Setting up $STACK_DIR..."
|
||||
install -d -m 0750 "$STACK_DIR"
|
||||
install -m 0640 "$SCRIPT_DIR/docker-compose.yml" "$STACK_DIR/docker-compose.yml"
|
||||
install -m 0640 "$SCRIPT_DIR/Caddyfile" "$STACK_DIR/Caddyfile"
|
||||
|
||||
# .env
|
||||
ENV_FILE="$STACK_DIR/.env"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
log "Seeding $ENV_FILE..."
|
||||
install -m 0600 "$SCRIPT_DIR/.env.example" "$ENV_FILE"
|
||||
sed -i \
|
||||
-e "s|^HEADSCALE_DOMAIN=.*|HEADSCALE_DOMAIN=${HEADSCALE_DOMAIN}|" \
|
||||
-e "s|^ACME_EMAIL=.*|ACME_EMAIL=${ACME_EMAIL}|" \
|
||||
-e "s|^TAILNET_DOMAIN=.*|TAILNET_DOMAIN=${TAILNET_DOMAIN}|" \
|
||||
-e "s|^POCKETID_DOMAIN=.*|POCKETID_DOMAIN=${POCKETID_DOMAIN}|" \
|
||||
-e "s|^OIDC_CLIENT_ID=.*|OIDC_CLIENT_ID=${OIDC_CLIENT_ID}|" \
|
||||
-e "s|^OIDC_CLIENT_SECRET=.*|OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}|" \
|
||||
"$ENV_FILE"
|
||||
else
|
||||
log ".env exists; leaving it alone."
|
||||
fi
|
||||
|
||||
# Validate
|
||||
missing=()
|
||||
for var in HEADSCALE_DOMAIN ACME_EMAIL TAILNET_DOMAIN POCKETID_DOMAIN \
|
||||
OIDC_CLIENT_ID OIDC_CLIENT_SECRET; do
|
||||
grep -E "^${var}=.+$" "$ENV_FILE" >/dev/null || missing+=("$var")
|
||||
done
|
||||
(( ${#missing[@]} == 0 )) || die "Missing values in $ENV_FILE: ${missing[*]}"
|
||||
|
||||
# Re-read final values from .env so config.yaml substitution sees what's
|
||||
# actually deployed (not whatever was passed in the environment).
|
||||
# shellcheck disable=SC1090
|
||||
set -a; . "$ENV_FILE"; set +a
|
||||
|
||||
# config.yaml: regenerate every run from the template so changes to .env
|
||||
# propagate. Secrets in config.yaml are derived from .env, not stored
|
||||
# independently, so this is safe.
|
||||
log "Generating config.yaml from template..."
|
||||
install -m 0600 "$SCRIPT_DIR/config.yaml" "$STACK_DIR/config.yaml"
|
||||
|
||||
# Escape replacement values for sed (handle slashes/&). Headscale URLs and
|
||||
# OIDC secrets can contain those.
|
||||
sed_escape() { printf '%s' "$1" | sed -e 's/[\/&]/\\&/g'; }
|
||||
|
||||
sed -i \
|
||||
-e "s/__HEADSCALE_DOMAIN__/$(sed_escape "$HEADSCALE_DOMAIN")/g" \
|
||||
-e "s/__TAILNET_DOMAIN__/$(sed_escape "$TAILNET_DOMAIN")/g" \
|
||||
-e "s/__POCKETID_DOMAIN__/$(sed_escape "$POCKETID_DOMAIN")/g" \
|
||||
-e "s/__OIDC_CLIENT_ID__/$(sed_escape "$OIDC_CLIENT_ID")/g" \
|
||||
-e "s/__OIDC_CLIENT_SECRET__/$(sed_escape "$OIDC_CLIENT_SECRET")/g" \
|
||||
"$STACK_DIR/config.yaml"
|
||||
|
||||
# Final sanity check
|
||||
grep -q '__.*__' "$STACK_DIR/config.yaml" \
|
||||
&& warn "config.yaml still has un-substituted __PLACEHOLDER__ markers!" \
|
||||
|| true
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Bring up the stack
|
||||
# ----------------------------------------------------------------------------
|
||||
if [[ "$FORCE" != "1" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
About to pull images and start the stack from $STACK_DIR.
|
||||
|
||||
Caddy will request a Let's Encrypt cert for ${HEADSCALE_DOMAIN}. DNS for
|
||||
that name must already point at this host, and ports 80/443 must be
|
||||
reachable from the internet.
|
||||
|
||||
In pocket-id, the OIDC client must have this redirect URI registered:
|
||||
https://${HEADSCALE_DOMAIN}/oidc/callback
|
||||
|
||||
Continue? [y/N]
|
||||
EOF
|
||||
read -r ans
|
||||
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || { warn "Aborted."; exit 0; }
|
||||
fi
|
||||
|
||||
cd "$STACK_DIR"
|
||||
log "Pulling images..."
|
||||
docker compose pull
|
||||
|
||||
log "Starting stack..."
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Wait for health
|
||||
# ----------------------------------------------------------------------------
|
||||
log "Waiting for services to become healthy (up to 120s)..."
|
||||
deadline=$(( $(date +%s) + 120 ))
|
||||
while (( $(date +%s) < deadline )); do
|
||||
status=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null || true)
|
||||
unhealthy=$(echo "$status" | awk '$2 != "healthy" && $2 != "" {print $1}')
|
||||
if [[ -z "$unhealthy" && -n "$status" ]]; then
|
||||
log "All services healthy."
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo
|
||||
log "Stack status:"
|
||||
docker compose ps
|
||||
echo
|
||||
cat <<EOF
|
||||
================================================================
|
||||
DEPLOYED
|
||||
|
||||
Headscale: https://${HEADSCALE_DOMAIN}
|
||||
Tailnet: ${TAILNET_DOMAIN} (MagicDNS suffix)
|
||||
OIDC: https://${POCKETID_DOMAIN}
|
||||
Stack dir: ${STACK_DIR}
|
||||
|
||||
Bootstrap a user (run from $STACK_DIR):
|
||||
docker compose exec headscale headscale users create alice
|
||||
docker compose exec headscale headscale users list
|
||||
|
||||
Tailscale client login:
|
||||
tailscale up --login-server https://${HEADSCALE_DOMAIN}
|
||||
# Browser opens, OIDC flow via pocket-id, node registers.
|
||||
|
||||
CLI inside the container:
|
||||
docker compose exec headscale headscale --help
|
||||
docker compose exec headscale headscale nodes list
|
||||
|
||||
Manage:
|
||||
docker compose logs -f
|
||||
docker compose pull && docker compose up -d # update
|
||||
docker compose down # stop, keep volumes
|
||||
docker compose down -v # stop, WIPE data
|
||||
|
||||
Or just re-run this script -- it's idempotent.
|
||||
================================================================
|
||||
EOF
|
||||
|
||||
# IMPORTANT: do not put any code below this exit. Everything after the
|
||||
# __ARCHIVE_BELOW__ marker is the embedded tar.gz payload (base64).
|
||||
exit 0
|
||||
__ARCHIVE_BELOW__
|
||||
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
|
||||
Reference in new issue
Block a user