Files
57_Wolve e23557b4fb feat(firewall): add deny-by-default host firewall (harden-firewall.sh)
Add a reusable iptables baseline that hardens hosts with ICMP + SSH
defaults and lets deployments register the ports they need. INPUT is
deny-by-default (loopback, established, ICMP, SSH on the configured port,
plus registered ports); OUTPUT stays open and FORWARD is left untouched so
Docker container networking is unaffected.
Persistence is native -- no boot hook. Rules are saved and restored by the
distro's own package (iptables/ip6tables on Alpine, iptables-persistent on
Debian, iptables-services on Alma) via the new oslib helpers
install_iptables / fw_save_cmd / fw_enable_restore. The saved ruleset
carries the INPUT->sshguard jump, so brute-force protection survives reboot
without the old sshguard-iptables hook.
A self-contained /usr/local/sbin/firewall-apply rebuilds INPUT from
declarative drop-ins under /etc/firewall/ports.d and runs the native save,
so deployments add a port without needing the repo present:
  printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/mystack.rule
  /usr/local/sbin/firewall-apply
- SSH port read live from sshd_config (custom bastion ports just work);
  FW_SSH_SOURCE restricts the source CIDR; FW_ALLOW_PING gates echo
- harden-ssh.sh / harden-jumphost.sh install it when ENABLE_FIREWALL=1
  (default) and skip the sshguard-only hook; ENABLE_FIREWALL=0 keeps it
- cloud-init base.yml / jumphost.yml forward the toggle
- the four stack deploy.sh open_web_ports() register 80/443 via the
  firewall (ufw/firewalld kept as fallback); Docker-published ports bypass
  INPUT, so this is belt-and-braces and self-documenting
- README + cloud-init/README document the mechanism, Docker caveat, and the
  `disable` recovery path
2026-06-12 17:06:25 -05:00

508 lines
23 KiB
Bash

#!/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, policy.hujson
# and .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:=}"
: "${HEADPLANE_OIDC_CLIENT_ID:=}" # optional: headplane UI OIDC login
: "${HEADPLANE_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() {
# Register 80/443 for this stack. Prefer the host firewall (harden-firewall.sh)
# when present: drop in a rule file and re-apply. Else fall back to ufw/
# firewalld if active (no-op when neither is).
#
# NOTE: Caddy publishes 80/443 via Docker, which reaches the host through the
# nat/FORWARD chains and BYPASSES the INPUT firewall -- so this is harmless
# belt-and-braces for any host-bound bind and self-documents the stack ports.
if [[ -d /etc/firewall/ports.d && -x /usr/local/sbin/firewall-apply ]]; then
log "Registering 80,443/tcp with host firewall..."
printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/headscale.rule
/usr/local/sbin/firewall-apply
elif 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
}
# Write headplane's config to $STACK_DIR/headplane.yaml from the current .env
# values. OIDC is enabled only when a client id + secret are present; otherwise
# headplane falls back to API-key login. Mode is API-only (no Docker socket).
render_headplane_config() {
local oidc_enabled=false
[[ -n "${HEADPLANE_OIDC_CLIENT_ID:-}" && -n "${HEADPLANE_OIDC_CLIENT_SECRET:-}" ]] && oidc_enabled=true
( umask 077
cat > "$STACK_DIR/headplane.yaml" <<EOF
# Generated by deploy.sh -- do not edit by hand (regenerated each run from .env).
server:
host: "0.0.0.0"
port: 3000
base_url: "https://${HEADSCALE_DOMAIN}"
cookie_secret: "${HEADPLANE_COOKIE_SECRET}"
cookie_secure: true
headscale:
url: "http://headscale:8080"
api_key: "${HEADPLANE_HS_API_KEY:-}"
oidc:
enabled: ${oidc_enabled}
issuer: "https://${POCKETID_DOMAIN}"
client_id: "${HEADPLANE_OIDC_CLIENT_ID:-}"
client_secret: "${HEADPLANE_OIDC_CLIENT_SECRET:-}"
use_pkce: true
EOF
)
chmod 600 "$STACK_DIR/headplane.yaml"
}
# ----------------------------------------------------------------------------
# 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 policy.hujson .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}|" \
-e "s|^HEADPLANE_COOKIE_SECRET=.*|HEADPLANE_COOKIE_SECRET=$(openssl rand -hex 16)|" \
-e "s|^HEADPLANE_OIDC_CLIENT_ID=.*|HEADPLANE_OIDC_CLIENT_ID=${HEADPLANE_OIDC_CLIENT_ID}|" \
-e "s|^HEADPLANE_OIDC_CLIENT_SECRET=.*|HEADPLANE_OIDC_CLIENT_SECRET=${HEADPLANE_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"
# ACL policy: install the default on first run only, so your edits survive
# re-deploys. headscale reads it on start (policy mode = file); after editing,
# validate with `headscale policy check` and `docker compose restart headscale`.
if [[ ! -f "$STACK_DIR/policy.hujson" ]]; then
log "Installing default ACL policy -> $STACK_DIR/policy.hujson"
install -m 0640 "$SCRIPT_DIR/policy.hujson" "$STACK_DIR/policy.hujson"
else
log "ACL policy exists; leaving $STACK_DIR/policy.hujson alone."
fi
# Render headplane.yaml now so the bind mount exists (api_key may still be
# empty on first run; it's re-rendered after the key is minted below).
render_headplane_config
# 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 (ignore comment lines so documentation tokens don't
# trip a false positive).
grep -v '^[[:space:]]*#' "$STACK_DIR/config.yaml" | grep -q '__[A-Za-z_]*__' \
&& 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
# Start headscale + caddy first; headplane needs a headscale API key, which we
# can only mint once headscale is up.
log "Starting headscale + caddy..."
docker compose up -d --remove-orphans headscale caddy
log "Waiting for headscale to become healthy (up to 120s)..."
deadline=$(( $(date +%s) + 120 ))
while (( $(date +%s) < deadline )); do
h=$(docker compose ps --format '{{.Service}} {{.Health}}' 2>/dev/null | awk '$1=="headscale"{print $2}')
[[ "$h" == "healthy" ]] && { log "headscale healthy."; break; }
sleep 5
done
# ----------------------------------------------------------------------------
# Headplane: mint a headscale API key on first deploy (idempotent -- stored in
# .env), render its config, then bring the UI up.
# ----------------------------------------------------------------------------
if ! grep -qE '^HEADPLANE_HS_API_KEY=.+' "$ENV_FILE"; then
log "Creating a headscale API key for headplane..."
HP_KEY=$(docker compose exec -T headscale headscale apikeys create --expiration 999d 2>/dev/null \
| tr -d '\r' | tail -n1)
if [[ -n "$HP_KEY" ]]; then
sed -i "s|^HEADPLANE_HS_API_KEY=.*|HEADPLANE_HS_API_KEY=${HP_KEY}|" "$ENV_FILE"
log "Stored headplane API key in .env."
else
warn "Could not mint a headscale API key; set HEADPLANE_HS_API_KEY in $ENV_FILE and re-run."
fi
fi
# Re-read .env (now with the API key) and render headplane's config.
set -a; . "$ENV_FILE"; set +a
render_headplane_config
log "Starting headplane + remaining services..."
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
# Convenience: a `headscale` command on the host that runs the CLI inside the
# container from ANY directory (plain `docker compose` needs to be run from
# $STACK_DIR). STACK_DIR is baked in; "$@" is passed through.
cat > /usr/local/bin/headscale <<EOF
#!/bin/sh
# Auto-generated by headscale deploy.sh -- runs the headscale CLI in-container.
exec docker compose -f "$STACK_DIR/docker-compose.yml" exec headscale headscale "\$@"
EOF
chmod +x /usr/local/bin/headscale
log "Installed /usr/local/bin/headscale (runs the CLI inside the container)."
echo
log "Stack status:"
docker compose ps
echo
cat <<EOF
================================================================
DEPLOYED
Headscale: https://${HEADSCALE_DOMAIN}
Headplane: https://${HEADSCALE_DOMAIN}/admin (web UI)
Tailnet: ${TAILNET_DOMAIN} (MagicDNS suffix)
OIDC: https://${POCKETID_DOMAIN}
Stack dir: ${STACK_DIR}
OIDC redirect/callback URIs -- register THESE in pocket-id:
headscale: https://${HEADSCALE_DOMAIN}/oidc/callback
headplane: https://${HEADSCALE_DOMAIN}/admin/oidc/callback (2nd OIDC client)
ACL policy: ${STACK_DIR}/policy.hujson
headscale policy check # validate after editing
cd ${STACK_DIR} && docker compose restart headscale # apply changes
Headscale CLI -- use the installed wrapper from anywhere:
headscale users create alice
headscale users list
headscale nodes list
headscale --help
# (equivalent: cd $STACK_DIR && docker compose exec headscale headscale ...)
Tailscale client login:
tailscale up --login-server https://${HEADSCALE_DOMAIN}
# Browser opens, OIDC flow via pocket-id, node registers.
Manage (run from $STACK_DIR):
cd $STACK_DIR
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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
dVmXdVmX/0X5H8de64YAUAAA