#!/bin/sh # GameServersHub agent installer + auto-updater for Linux (systemd). # install: curl -fsSL https://gameserverspanel.com/install/linux | sh -s -- --token --broker --data-dir # update: the installer writes a LOCAL /opt/gsh/gsh-update.sh and an hourly # systemd timer runs THAT, so updates never execute remote shell. # Every binary install/update is verified against the RSA-signed manifest # (same key the Windows fleet pins) and version monotonicity (anti-rollback). set -eu DOWNLOAD_BASE='https://get.gsh-servers.com' BROKER_DEFAULT='https://broker.gsh-servers.com' GSH_HOME=/opt/gsh CONFIG_DIR=/etc/gsh BIN="$GSH_HOME/gsh-agent" PUBKEY_PEM='-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoBQ7by4pVSEyIBBxSJlm Hirx7LUkixhIpWtRGTiWV/9KuPjtF7IWqgmSy8hWzfqHSeTc9mURymzQmMlOlLrl U4ZNu6dzv6KBFaQtghHFCcbtj2pacm/5ceYmCzpa77ObRTKbaBdreLYVereRJXxU k6ictfiU5GXZORZI51aRjdg3FZkHSeZpux+s55ez/xkZ9mGug/KpEG2oARhpxmz8 9PQN8JkY74pGM6Xmh3LSG6JOz8VnJ+KoaQoSqgn33am2CCKqhlQJ8/G9JZeegqSw xg3vsbJBZH5Gxz4lfaW9Pw28ZVfpvB4ZoXQwz1n8+Lo6/mhEWOWaAbVai6tyQGkD zjzqs73+Q9g64qajirwwYostrhB6SFFYrJp5B0Nr8ACyBTPOqntKr7uvKlV/bams OvPHJ4AZYd8s94MWU0/KmOyyU9jhmZEQoJosxj07/hQbZHm+Yhyf2AH9UFbjUf0j dw01T0eCUTs0Qf90oJE9qd8gyud5tTRu44eV120XxEebzOFa0WS9OxFGVh0wzOXn VEQN1b3nqdk0T2F7EVqN736g99+tYKDWiT6T0FjTqtaAf4a3poPzLE8iJX0f5p5T FT4TRSj9H6R2V2/+yLo0IHVugtjkuiGE24j6LWWhp/GwEImZm6I+QZC6dFBdfWfn qPGflTiUddCW5Ku26Qc0xysCAwEAAQ== -----END PUBLIC KEY-----' log() { printf '%s\n' "[gsh] $*"; } fail() { printf '%s\n' "[gsh] ERROR: $*" >&2; exit 1; } TOKEN='' BROKER="$BROKER_DEFAULT" DATA_DIR='' while [ $# -gt 0 ]; do case "$1" in --token) TOKEN="$2"; shift 2 ;; --broker) BROKER="$2"; shift 2 ;; --data-dir) DATA_DIR="$2"; shift 2 ;; *) fail "unknown argument: $1" ;; esac done [ "$(id -u)" = "0" ] || fail "run as root (sudo)." [ "$(uname -s)" = "Linux" ] || fail "this installer is for Linux." case "$(uname -m)" in x86_64|amd64) : ;; *) fail "unsupported architecture $(uname -m) (amd64 only for now)" ;; esac command -v systemctl >/dev/null 2>&1 || fail "systemd is required." for tool in curl sha256sum openssl base64 install; do command -v "$tool" >/dev/null 2>&1 || fail "$tool is required." done fetch() { curl -fsSL --proto '=https' --tlsv1.2 -o "$2" "$1"; } # verify_and_download : fetch manifest.json(.sig), verify the RSA # signature with the baked public key (fail closed), enforce version # monotonicity against /opt/gsh/version, then download the binary and require # its sha256 to match the manifest's sha256Linux. Sets MF_VERSION. verify_and_download() { vd_tmp=$(mktemp -d) fetch "$DOWNLOAD_BASE/manifest.json" "$vd_tmp/manifest.json" || fail "could not download manifest.json from $DOWNLOAD_BASE" fetch "$DOWNLOAD_BASE/manifest.json.sig" "$vd_tmp/manifest.json.sig" || fail "could not download manifest.json.sig from $DOWNLOAD_BASE" printf '%s\n' "$PUBKEY_PEM" > "$vd_tmp/pub.pem" base64 -d "$vd_tmp/manifest.json.sig" > "$vd_tmp/manifest.sig.bin" 2>/dev/null || fail "manifest.json.sig is not valid base64" openssl dgst -sha256 -verify "$vd_tmp/pub.pem" -signature "$vd_tmp/manifest.sig.bin" "$vd_tmp/manifest.json" >/dev/null 2>&1 \ || fail "manifest signature verification FAILED - refusing to install an unverified build" MF_VERSION=$(sed -n 's/.*"version":"\([0-9][0-9.]*\)".*/\1/p' "$vd_tmp/manifest.json") MF_SHA=$(sed -n 's/.*"sha256Linux":"\([0-9a-fA-F]\{64\}\)".*/\1/p' "$vd_tmp/manifest.json") [ -n "$MF_VERSION" ] || fail "manifest.json carries no version" [ -n "$MF_SHA" ] || fail "the published build predates Linux support (manifest.json has no sha256Linux); publish an agent build >= 0.21.0 first" if [ -f "$GSH_HOME/version" ]; then CUR=$(cat "$GSH_HOME/version") [ "$(printf '%s\n%s\n' "$CUR" "$MF_VERSION" | sort -V | tail -n 1)" = "$MF_VERSION" ] \ || fail "downgrade refused: this machine runs $CUR, the published manifest says $MF_VERSION" fi fetch "$DOWNLOAD_BASE/gsh-agent-linux-amd64" "$vd_tmp/gsh-agent" || fail "could not download gsh-agent-linux-amd64" echo "$MF_SHA $vd_tmp/gsh-agent" | sha256sum -c - >/dev/null 2>&1 \ || fail "binary hash does not match the signed manifest - refusing to install" install -m 0755 "$vd_tmp/gsh-agent" "$1" rm -rf "$vd_tmp" } log "GameServersHub - Agent Installer (Linux)" mkdir -p "$GSH_HOME" "$CONFIG_DIR" chmod 0755 "$GSH_HOME" chmod 0700 "$CONFIG_DIR" # SteamCMD on Linux needs 32-bit runtime libraries; install them where we know # how, warn (not fail) elsewhere - Java games need none of this. if command -v apt-get >/dev/null 2>&1; then log "installing SteamCMD 32-bit dependencies (lib32gcc-s1 lib32stdc++6)" DEBIAN_FRONTEND=noninteractive apt-get install -y -qq lib32gcc-s1 lib32stdc++6 >/dev/null 2>&1 \ || log "warning: could not install 32-bit libs; Steam game installs may fail until you add them" elif command -v dnf >/dev/null 2>&1; then dnf install -y -q glibc.i686 libstdc++.i686 >/dev/null 2>&1 \ || log "warning: could not install 32-bit libs; Steam game installs may fail until you add them" else log "note: unknown package manager; if Steam game installs fail, add your distro's 32-bit gcc/stdc++ libraries" fi log "downloading + verifying the agent" # Stage to .new then rename: writing in place would hit ETXTBSY when a # previous install's agent is still running (rename over it is always safe). verify_and_download "$BIN.new" mv -f "$BIN.new" "$BIN" printf '%s' "$MF_VERSION" > "$GSH_HOME/version" log "installed gsh-agent $MF_VERSION" # Enroll only when this machine has no identity yet (a re-run keeps it). The # token goes through a 0600 file + -enroll-token-file, never argv, so it cannot # appear in the process list or shell history on the box. if [ ! -f "$CONFIG_DIR/agent.json" ]; then [ -n "$TOKEN" ] || fail "this machine is not enrolled yet: pass --token from the panel's Add Machine dialog" [ -n "$DATA_DIR" ] || fail "pass --data-dir (where game servers will be installed, e.g. /srv/gsh)" mkdir -p "$DATA_DIR" umask 077 TOKEN_FILE=$(mktemp) printf '%s' "$TOKEN" > "$TOKEN_FILE" log "enrolling with the panel" "$BIN" -enroll-only -enroll-token-file "$TOKEN_FILE" -broker-url "$BROKER" -data-dir "$DATA_DIR" -config "$CONFIG_DIR/agent.json" \ || { rm -f "$TOKEN_FILE"; fail "enrollment failed (is the token still valid? They expire quickly)"; } rm -f "$TOKEN_FILE" umask 022 else log "existing enrollment found - keeping this machine's identity" fi # Local updater: same verification pipeline, run hourly by systemd. Written # from this script so served installer and local updater can never drift. cat > "$GSH_HOME/gsh-update.sh" <&2; exit 1; } fetch() { curl -fsSL --proto '=https' --tlsv1.2 -o "\$2" "\$1"; } # verify_and_download : fetch manifest.json(.sig), verify the RSA # signature with the baked public key (fail closed), enforce version # monotonicity against /opt/gsh/version, then download the binary and require # its sha256 to match the manifest's sha256Linux. Sets MF_VERSION. verify_and_download() { vd_tmp=\$(mktemp -d) fetch "\$DOWNLOAD_BASE/manifest.json" "\$vd_tmp/manifest.json" || fail "could not download manifest.json from \$DOWNLOAD_BASE" fetch "\$DOWNLOAD_BASE/manifest.json.sig" "\$vd_tmp/manifest.json.sig" || fail "could not download manifest.json.sig from \$DOWNLOAD_BASE" printf '%s\n' "\$PUBKEY_PEM" > "\$vd_tmp/pub.pem" base64 -d "\$vd_tmp/manifest.json.sig" > "\$vd_tmp/manifest.sig.bin" 2>/dev/null || fail "manifest.json.sig is not valid base64" openssl dgst -sha256 -verify "\$vd_tmp/pub.pem" -signature "\$vd_tmp/manifest.sig.bin" "\$vd_tmp/manifest.json" >/dev/null 2>&1 \ || fail "manifest signature verification FAILED - refusing to install an unverified build" MF_VERSION=\$(sed -n 's/.*"version":"\([0-9][0-9.]*\)".*/\1/p' "\$vd_tmp/manifest.json") MF_SHA=\$(sed -n 's/.*"sha256Linux":"\([0-9a-fA-F]\{64\}\)".*/\1/p' "\$vd_tmp/manifest.json") [ -n "\$MF_VERSION" ] || fail "manifest.json carries no version" [ -n "\$MF_SHA" ] || fail "the published build predates Linux support (manifest.json has no sha256Linux); publish an agent build >= 0.21.0 first" if [ -f "\$GSH_HOME/version" ]; then CUR=\$(cat "\$GSH_HOME/version") [ "\$(printf '%s\n%s\n' "\$CUR" "\$MF_VERSION" | sort -V | tail -n 1)" = "\$MF_VERSION" ] \ || fail "downgrade refused: this machine runs \$CUR, the published manifest says \$MF_VERSION" fi fetch "\$DOWNLOAD_BASE/gsh-agent-linux-amd64" "\$vd_tmp/gsh-agent" || fail "could not download gsh-agent-linux-amd64" echo "\$MF_SHA \$vd_tmp/gsh-agent" | sha256sum -c - >/dev/null 2>&1 \ || fail "binary hash does not match the signed manifest - refusing to install" install -m 0755 "\$vd_tmp/gsh-agent" "\$1" rm -rf "\$vd_tmp" } CUR=\$(cat "\$GSH_HOME/version" 2>/dev/null || echo 0) verify_and_download "\$BIN.new" if [ "\$MF_VERSION" = "\$CUR" ]; then log "already on \$MF_VERSION" rm -f "\$BIN.new" exit 0 fi log "updating \$CUR -> \$MF_VERSION" mv -f "\$BIN" "\$BIN.old" 2>/dev/null || true mv -f "\$BIN.new" "\$BIN" printf '%s' "\$MF_VERSION" > "\$GSH_HOME/version" # KillMode=process keeps the game servers running through this restart; the new # agent re-adopts them (zero-downtime updates, ADR-0040). systemctl restart gsh-agent sleep 10 if systemctl is-active --quiet gsh-agent; then log "update to \$MF_VERSION healthy" rm -f "\$BIN.old" else log "new build failed to start - rolling back to \$CUR" mv -f "\$BIN.old" "\$BIN" 2>/dev/null || true printf '%s' "\$CUR" > "\$GSH_HOME/version" systemctl restart gsh-agent || true fail "rolled back to \$CUR" fi UPDATER_EOF chmod 0755 "$GSH_HOME/gsh-update.sh" # systemd units. KillMode=process is LOAD-BEARING: stopping or restarting the # agent must never kill the game-server processes living in its control group # (they detach and are re-adopted - ADR-0040). The default cgroup kill would # take every running game down with each agent update. cat > /etc/systemd/system/gsh-agent.service <<'UNIT_EOF' [Unit] Description=GSH game server agent After=network-online.target Wants=network-online.target [Service] ExecStart=/opt/gsh/gsh-agent -config /etc/gsh/agent.json Restart=always RestartSec=5 KillMode=process LimitNOFILE=65536 [Install] WantedBy=multi-user.target UNIT_EOF cat > /etc/systemd/system/gsh-update.service <<'UNIT_EOF' [Unit] Description=GSH agent auto-update (one shot) [Service] Type=oneshot ExecStart=/bin/sh /opt/gsh/gsh-update.sh UNIT_EOF cat > /etc/systemd/system/gsh-update.timer <<'UNIT_EOF' [Unit] Description=Hourly GSH agent auto-update [Timer] OnCalendar=hourly RandomizedDelaySec=600 Persistent=true [Install] WantedBy=timers.target UNIT_EOF systemctl daemon-reload systemctl enable --now gsh-update.timer >/dev/null 2>&1 || log "warning: could not enable the hourly update timer" systemctl enable gsh-agent >/dev/null 2>&1 || log "warning: could not enable the agent at boot" systemctl restart gsh-agent log "waiting for the agent to come online" i=0 while [ $i -lt 15 ]; do if grep -q "connected and authenticated" "$CONFIG_DIR/agent.log" 2>/dev/null; then log "Done - the GSH agent is installed, connected, and auto-updating." log "binary: $BIN" log "config: $CONFIG_DIR/agent.json" log "log: $CONFIG_DIR/agent.log" log "Your machine should appear Online in the panel now." exit 0 fi sleep 2 i=$((i + 1)) done log "The agent service is installed and starting. If the machine does not show" log "Online in the panel within a couple of minutes, check: journalctl -u gsh-agent"