#!/bin/bash # Install and configure headscale locally # Copy this directory to a VM and run: sudo ./install.sh # Example: sudo ./install.sh 37.27.166.244 # docs: # this script assumes the ip addresses is pointed to the current machine, and this script runs on 0.0.0.0 # configuration is kinda manual cuz the official packaging is for .deb and i want rhel based system # fallback (Designated Encrypted Relay for Packets) is disabled. if NAT traversal fails, there will be no connection set -e die() { echo "Error: $1" >&2; exit 1; } info() { echo " $1"; } [[ $EUID -eq 0 ]] || die "Must run as root" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" PUBLIC_IP="${1:?Usage: $0 }" HEADSCALE_VERSION="0.28.0" ARCH="amd64" echo "=== Installing Headscale ===" echo "Public IP: $PUBLIC_IP" echo "" # 1. Download and install binary from GitHub releases (no RPM exists, only deb and bare binary) info "Downloading headscale v${HEADSCALE_VERSION}..." curl -fsSL -o /var/tmp/headscale "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${ARCH}" install -m 755 /var/tmp/headscale /usr/local/bin/headscale rm -f /var/tmp/headscale info "Installed: $(headscale version 2>/dev/null | head -1)" # 2. Create headscale user/group (same as what the deb postinst does) info "Creating headscale user/group..." groupadd --force --system headscale useradd --system --shell /usr/sbin/nologin --gid headscale --home-dir /var/lib/headscale --comment headscale headscale 2>/dev/null || true # 3. Create directories: /etc/headscale (config), /var/lib/headscale (data/db), /var/run/headscale (socket) info "Creating directories..." mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale chown headscale:headscale /var/lib/headscale /var/run/headscale # 4. Install systemd service (from official packaging/systemd/headscale.service, # only change: ExecStart points to /usr/local/bin/headscale instead of /usr/bin/headscale) info "Installing systemd service..." cat > /etc/systemd/system/headscale.service <<'EOF' [Unit] After=network.target Description=headscale coordination server for Tailscale X-Restart-Triggers=/etc/headscale/config.yaml [Service] Type=simple User=headscale Group=headscale ExecStart=/usr/local/bin/headscale serve ExecReload=/usr/bin/kill -HUP $MAINPID Restart=always RestartSec=5 WorkingDirectory=/var/lib/headscale ReadWritePaths=/var/lib/headscale AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN LockPersonality=true NoNewPrivileges=true PrivateDevices=true PrivateMounts=true PrivateTmp=true ProcSubset=pid ProtectClock=true ProtectControlGroups=true ProtectHome=true ProtectHostname=true ProtectKernelLogs=true ProtectKernelModules=true ProtectKernelTunables=true ProtectProc=invisible ProtectSystem=strict RemoveIPC=true RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX RestrictNamespaces=true RestrictRealtime=true RestrictSUIDSGID=true RuntimeDirectory=headscale RuntimeDirectoryMode=0750 StateDirectory=headscale StateDirectoryMode=0750 SystemCallArchitectures=native SystemCallFilter=@chown SystemCallFilter=@system-service SystemCallFilter=~@privileged UMask=0077 [Install] WantedBy=multi-user.target EOF # 5. Install config — example config with two changes: # - server_url: http://:8080 (so clients know the public address) # - listen_addr: 0.0.0.0:8080 (listen on all interfaces, not just localhost) info "Downloading example config..." curl -fsSL -o /var/tmp/config-example.yaml "https://raw.githubusercontent.com/juanfont/headscale/v${HEADSCALE_VERSION}/config-example.yaml" cp /var/tmp/config-example.yaml /etc/headscale/config.yaml rm -f /var/tmp/config-example.yaml sed -i "s|server_url: http://127.0.0.1:8080|server_url: http://${PUBLIC_IP}:8080|" /etc/headscale/config.yaml sed -i "s|listen_addr: 127.0.0.1:8080|listen_addr: 0.0.0.0:8080|" /etc/headscale/config.yaml # Disable DERP relays — all nodes have public IPs, force direct WireGuard connections only sed -i 's| - https://controlplane.tailscale.com/derpmap/default| # - https://controlplane.tailscale.com/derpmap/default|' /etc/headscale/config.yaml sed -i 's| auto_update_enabled: true| auto_update_enabled: false|' /etc/headscale/config.yaml chown -R headscale:headscale /etc/headscale # 6. Start headscale info "Enabling and starting headscale..." systemctl daemon-reload systemctl enable --now headscale # 7. Create default user and reusable auth key info "Creating default user..." headscale users create default 2>/dev/null || true USER_ID=$(headscale users list -o json 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])") info "Creating auth key (reusable, 90 days)..." AUTH_KEY=$(headscale preauthkeys create --user "$USER_ID" --expiration 2160h --reusable) # 8. Register this machine as a tailscale client info "Installing tailscale client on this machine..." dnf install -y tailscale systemctl enable --now tailscaled tailscale up --login-server "http://${PUBLIC_IP}:8080" --authkey "$AUTH_KEY" echo "" echo "=== Headscale installed ===" echo "" echo "Health check:" echo " curl http://${PUBLIC_IP}:8080/health" echo "" echo "Auth key (reusable, 90 days):" echo " $AUTH_KEY" echo "" echo "Connect a client:" echo " sudo ./client.sh ${PUBLIC_IP} ${AUTH_KEY}" echo "" echo "Create a new user:" echo " headscale users create " echo "" echo "Create an auth key:" echo " headscale preauthkeys create --user --expiration 2160h --reusable" echo "" echo "List nodes:" echo " headscale node list"