#!/bin/sh # One-shot setup for a VPS. Run as root. # # sh setup.sh [--domain hh.example.com] [--version v0.1.0] [--no-auto-update] Hammerhead (dashboard and API) # sh setup.sh edge --hammerhead URL --token TOKEN [--ports 25565,30000-30100] [--version V] # sh setup.sh agent --hammerhead URL --token TOKEN [--version V] # # Hammerhead: installs it as a systemd service (with a generated master key, kept in # /etc/hammerhead/hammerhead.env), and with --domain also Caddy, which gets and renews # the HTTPS certificate for that name. DNS for the name must already point here. # edge: installs Blacktip, with its bundled Traefik, on this machine (or any other VPS), # opens the firewall and lists every IP address it can open ports on. # agent: installs Bamboo, the site agent, on the machine that hosts your services. # # Edge and agent need a token, which only exists once Hammerhead is up: create the edge under # Server -> Edges (or the site under Sites) in the dashboard and copy the values from "Show command". # Safe to run again: it upgrades and keeps your settings and data. set -eu BASE=${SHARKS_BASE:-https://git.veillax.com/Veillax} MODE=hammerhead case "${1:-}" in edge | agent) MODE=$1; shift ;; esac DOMAIN= VERSION= HH= TOKEN= PORTS= AUTOUP=1 while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN=$2; shift 2 ;; --version) VERSION=$2; shift 2 ;; --hammerhead) HH=$2; shift 2 ;; --token) TOKEN=$2; shift 2 ;; --ports) PORTS=$2; shift 2 ;; --no-auto-update) AUTOUP=0; shift ;; -h | --help) sed -n '2,16p' "$0"; exit 0 ;; *) echo "unknown option $1 (see --help)" >&2; exit 2 ;; esac done # auto_update : lets the program update itself unless --no-auto-update was given auto_update() { [ "$AUTOUP" = 1 ] && set_env "$1" "$2_AUTO_UPDATE" 1 return 0 } say() { printf '\n== %s\n' "$*"; } die() { echo "error: $*" >&2; exit 1; } [ "$(id -u)" = 0 ] || die "run as root: sudo sh $0 $*" command -v systemctl > /dev/null 2>&1 || die "this needs a systemd-based Linux (Debian, Ubuntu, Fedora, RHEL, Arch...)" case "$(uname -m)" in x86_64 | amd64) ARCH=amd64 ;; aarch64 | arm64) ARCH=arm64 ;; armv7* | armv6* | arm) ARCH=arm ;; *) die "unsupported CPU: $(uname -m)" ;; esac pkg_install() { # installs packages with whichever package manager there is if command -v apt-get > /dev/null 2>&1; then apt-get update -qq || true DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" elif command -v dnf > /dev/null 2>&1; then dnf install -y "$@" elif command -v yum > /dev/null 2>&1; then yum install -y "$@" elif command -v pacman > /dev/null 2>&1; then pacman -S --noconfirm --needed "$@" else die "install these yourself and run again: $*" fi } need() { command -v "$1" > /dev/null 2>&1 || { say "installing $1"; pkg_install "$1"; }; } need curl need tar need git latest() { # newest release tag of repo $1 (read from git, so no API token is needed) git ls-remote --tags --refs "$BASE/$1" 2> /dev/null | sed 's|.*refs/tags/||' | grep -E '^v[0-9]' | sort -V | tail -n 1 } # fetch_install : downloads that app's release for this CPU, verifies it and runs its install.sh fetch_install() { app=$1 ver=${VERSION:-$(latest "$app")} [ -n "$ver" ] || die "couldn't find a release of $app at $BASE/$app (use --version, or SHARKS_BASE for a mirror)" dir=$(mktemp -d) say "downloading $app $ver ($ARCH)" name="${app}_${ver}_linux_${ARCH}" curl -fsSL -o "$dir/$name.tar.gz" "$BASE/$app/releases/download/$ver/$name.tar.gz" || die "no $app $ver release for linux/$ARCH" curl -fsSL -o "$dir/SHA256SUMS" "$BASE/$app/releases/download/$ver/SHA256SUMS" || die "couldn't download the checksums" (cd "$dir" && grep " $name.tar.gz\$" SHA256SUMS | sha256sum -c -) || die "checksum mismatch: not installing" tar -xzf "$dir/$name.tar.gz" -C "$dir" (cd "$dir/$name" && sh ./install.sh) rm -rf "$dir" } # set_env : sets KEY=value, replacing the example line or an older value set_env() { f=$1 k=$2 v=$3 if grep -q "^#\?$k=" "$f"; then tmp=$(mktemp) awk -v k="$k" -v v="$v" 'BEGIN{done=0} { if (!done && ($0 ~ "^#?"k"=")) { print k"="v; done=1 } else print }' "$f" > "$tmp" cat "$tmp" > "$f" rm -f "$tmp" else echo "$k=$v" >> "$f" fi } firewall_open() { # firewall_open 80/tcp 443/tcp ... if command -v ufw > /dev/null 2>&1 && ufw status 2> /dev/null | grep -q "Status: active"; then for p in "$@"; do ufw allow "$p" > /dev/null; done echo "opened in ufw: $*" elif command -v firewall-cmd > /dev/null 2>&1 && firewall-cmd --state > /dev/null 2>&1; then for p in "$@"; do firewall-cmd --permanent --add-port="$p" > /dev/null; done firewall-cmd --reload > /dev/null echo "opened in firewalld: $*" else echo "No active ufw or firewalld found. If this machine has a firewall, or your host has a cloud firewall / security group, open: $*" fi } public_ips() { # global addresses actually configured on this machine ip -o addr show scope global 2> /dev/null | awk '{print $4}' | cut -d/ -f1 || true } wait_active() { # wait_active sleep 2 systemctl is-active --quiet "$1" || { journalctl -u "$1" -n 20 --no-pager; die "$1 didn't start"; } } case $MODE in hammerhead) fetch_install hammerhead ENVF=/etc/hammerhead/hammerhead.env auto_update "$ENVF" HAMMERHEAD if ! grep -q '^HAMMERHEAD_MASTER_KEY=.' "$ENVF"; then say "generating the master key" set_env "$ENVF" HAMMERHEAD_MASTER_KEY "$(/usr/local/bin/hammerhead -genkey)" echo "Saved in $ENVF. Back that file up: without the key the encrypted secrets in the database can't be read." fi if [ -n "$DOMAIN" ]; then set_env "$ENVF" HAMMERHEAD_URL "https://$DOMAIN" # only Caddy needs to reach it sed -i 's|-listen :8080|-listen 127.0.0.1:8080|' /etc/systemd/system/hammerhead.service systemctl daemon-reload if ! command -v caddy > /dev/null 2>&1; then say "installing Caddy (HTTPS certificates for $DOMAIN)" pkg_install caddy || die "couldn't install Caddy from this distribution; see https://caddyserver.com/docs/install and proxy $DOMAIN to 127.0.0.1:8080" fi mkdir -p /etc/caddy printf '%s {\n\treverse_proxy 127.0.0.1:8080\n}\n' "$DOMAIN" > /etc/caddy/Caddyfile systemctl enable caddy > /dev/null 2>&1 || true systemctl restart caddy firewall_open 80/tcp 443/tcp URL="https://$DOMAIN" else firewall_open 8080/tcp ip=$(public_ips | head -n 1) URL="http://${ip:-this-server}:8080" echo "No --domain given, so the dashboard is plain HTTP. Put it behind HTTPS before you create real logins (run this again with --domain)." fi systemctl restart hammerhead wait_active hammerhead say "Hammerhead is running" echo "Open $URL to create the first administrator." echo echo "Next, in the dashboard: Server -> Edges -> add an edge, then on the machine that has your public IPs run" echo " sh setup.sh edge --hammerhead $URL --token " echo "and on each machine that hosts your services (home server, game server...) run" echo " sh setup.sh agent --hammerhead $URL --token " echo "Either can be this same machine." ;; edge) [ -n "$HH" ] && [ -n "$TOKEN" ] || die "edge needs --hammerhead URL and --token TOKEN (Server -> Edges -> Show command)" fetch_install blacktip ENVF=/etc/blacktip/blacktip.env set_env "$ENVF" BLACKTIP_HAMMERHEAD "$HH" set_env "$ENVF" BLACKTIP_TOKEN "$TOKEN" auto_update "$ENVF" BLACKTIP chmod 600 "$ENVF" say "network" # WireGuard moves a lot of UDP; the stock socket buffers are small printf 'net.core.rmem_max=7500000\nnet.core.wmem_max=7500000\n' > /etc/sysctl.d/90-blacktip.conf sysctl -q -p /etc/sysctl.d/90-blacktip.conf > /dev/null 2>&1 || true list="80/tcp 443/tcp 51820/udp" if [ -n "$PORTS" ]; then # 25565,30000-30100: each is opened for TCP and UDP (the dashboard decides which one is used) for p in $(echo "$PORTS" | tr ',' ' '); do list="$list $p/tcp $p/udp" done fi # ufw wants a range as 30000:30100, firewalld as 30000-30100 if command -v ufw > /dev/null 2>&1; then list=$(echo "$list" | tr '-' ':'); fi # shellcheck disable=SC2086 firewall_open $list ips=$(public_ips) echo echo "Blacktip can open ports on these addresses of this machine:" echo "$ips" | sed 's/^/ /' if [ "$(echo "$ips" | grep -c .)" -lt 2 ]; then echo "Only one address found. Extra IPs must be added to this machine's network configuration first." echo "A provider's floating or 1:1 NAT address that isn't listed above can't be bound by Blacktip." fi echo "In the dashboard each port picks which of these addresses it listens on." echo "Ports you open later in the dashboard must also be allowed in the firewall: run this again with --ports." systemctl restart blacktip wait_active blacktip say "Blacktip is running" ;; agent) [ -n "$HH" ] && [ -n "$TOKEN" ] || die "agent needs --hammerhead URL and --token TOKEN (Sites -> Show command)" fetch_install bamboo ENVF=/etc/bamboo/bamboo.env set_env "$ENVF" BAMBOO_HAMMERHEAD "$HH" set_env "$ENVF" BAMBOO_TOKEN "$TOKEN" auto_update "$ENVF" BAMBOO chmod 600 "$ENVF" if command -v docker > /dev/null 2>&1 && getent group docker > /dev/null 2>&1; then usermod -aG docker bamboo set_env "$ENVF" BAMBOO_DOCKER 1 echo "Docker found: Bamboo will list your containers as targets." fi systemctl restart bamboo wait_active bamboo say "Bamboo is running (it only connects outward, so there is nothing to open)" ;; esac