#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-or-later # # headscale-server-setup.sh -- OPTIONAL self-hosted coordination server. # # Use instead of Tailscale's control plane when you want no account limits and # full control over who may join. Run on a PUBLIC Debian 12+/Ubuntu 22.04+ VPS # with a DNS name pointing at it. It: # 1. installs the official Headscale .deb (latest release, or --version) # 2. points server_url at https:// and enables built-in Let's Encrypt # TLS (unless --no-tls, for running behind your own reverse proxy) # 3. starts the systemd service # 4. creates a user and mints a reusable pre-auth key # # The Plex host and every client then join with: # sudo tailscale up --login-server https:// --authkey set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib/common.sh source "${SCRIPT_DIR}/lib/common.sh" || { echo "missing ${SCRIPT_DIR}/lib/common.sh" >&2; exit 1; } enable_error_trap readonly CFG="/etc/headscale/config.yaml" DOMAIN="" USER_NAME="plex" VERSION="" # auto-detect latest if empty EXPIRY="720h" # preauth key lifetime (30 days) USE_TLS=1 readonly LISTEN_PLAIN="127.0.0.1:8080" PREAUTH_KEY="" usage() { cat </dev/null | head -n1))" return fi local ver arch url tmp ver="$(detect_version)" arch="$(dpkg --print-architecture)" url="https://github.com/juanfont/headscale/releases/download/v${ver}/headscale_${ver}_linux_${arch}.deb" tmp="$(mktemp --suffix=.deb)" log "downloading Headscale v${ver} (${arch})" run curl -fsSL -o "$tmp" "$url" log "installing package" run apt-get install -y "$tmp" run rm -f "$tmp" } # set or append a top-level scalar key in the YAML config (other keys untouched) set_yaml() { local key="$1" val="$2" if grep -qE "^[[:space:]]*${key}:" "$CFG"; then run sed -i -E "s|^([[:space:]]*)${key}:.*|\1${key}: ${val}|" "$CFG" elif [[ $DRY_RUN -eq 1 ]]; then echo " + append ${key}: ${val} >> $CFG" else printf '%s: %s\n' "$key" "$val" >> "$CFG" fi } configure_headscale() { [[ -f "$CFG" ]] || die "expected config at $CFG (did the package install correctly?)" run cp -a "$CFG" "${CFG}.bak.$(date +%Y%m%d%H%M%S)" set_yaml server_url "https://${DOMAIN}" if [[ $USE_TLS -eq 1 ]]; then set_yaml listen_addr "0.0.0.0:443" set_yaml tls_letsencrypt_hostname "${DOMAIN}" set_yaml tls_letsencrypt_challenge_type "HTTP-01" set_yaml tls_letsencrypt_listen ":http" warn "built-in TLS: ports 80 (ACME challenge) and 443 must be reachable." else set_yaml listen_addr "$LISTEN_PLAIN" warn "--no-tls: terminate TLS at a reverse proxy in front of $LISTEN_PLAIN." fi ok "configured $CFG (server_url=https://${DOMAIN})" } start_headscale() { run systemctl enable --now headscale if [[ $DRY_RUN -eq 0 ]]; then sleep 2 systemctl is-active --quiet headscale \ && ok "headscale is running" \ || warn "headscale not active; check 'journalctl -u headscale -e'" fi } provision_user() { if [[ $DRY_RUN -eq 1 ]]; then echo " + headscale users create $USER_NAME" echo " + headscale preauthkeys create --user $USER_NAME --reusable --expiration $EXPIRY" return fi if ! headscale users list 2>/dev/null | grep -qw "$USER_NAME"; then log "creating user '$USER_NAME'" headscale users create "$USER_NAME" || warn "users create failed (may already exist)" else ok "user '$USER_NAME' already exists" fi log "minting reusable pre-auth key (valid $EXPIRY)" # Newer headscale wants the user id; older accepts the name. Try name, then id. PREAUTH_KEY="$(headscale preauthkeys create --user "$USER_NAME" --reusable --expiration "$EXPIRY" 2>/dev/null | tail -n1 || true)" if [[ -z "$PREAUTH_KEY" || "$PREAUTH_KEY" == *" "* ]]; then local uid uid="$(headscale users list 2>/dev/null | awk -v u="$USER_NAME" '$0 ~ u {print $1; exit}')" [[ -n "$uid" ]] && PREAUTH_KEY="$(headscale preauthkeys create --user "$uid" --reusable --expiration "$EXPIRY" 2>/dev/null | tail -n1 || true)" fi if [[ -n "$PREAUTH_KEY" ]]; then ok "pre-auth key (treat as a secret): $PREAUTH_KEY" else warn "could not auto-mint a key; run: headscale preauthkeys create --user $USER_NAME --reusable --expiration $EXPIRY" fi } main() { parse_args "$@" install_headscale configure_headscale start_headscale provision_user cat <} On the Plex host, do VPN + Plex config in one step: sudo ./plex-tailscale-setup.sh --login-server https://${DOMAIN} --authkey ${PREAUTH_KEY:-} Manage access: headscale users list headscale nodes list headscale preauthkeys create --user ${USER_NAME} --reusable --expiration ${EXPIRY} EOF } main "$@"