Files
ServerKit/scripts/install.sh
T
Juan DenisandClaude Fable 5 11066bb130 fix(agent-install): paged version discovery, checksum verify, busybox support (A1-A3)
- version discovery pages agent-v tags (?per_page=100, 3 pages) so
  panel releases can no longer push agent tags off page 1; panel
  injects its resolved agent version into the served script and its
  own releases fetch is paginated too (A1)
- downloaded agent binary is sha256-verified against the release's
  checksums.txt: halt on mismatch, warn-and-continue when absent (A2)
- value-taking options error loudly when the value is missing; busybox
  adduser fallback; no GNU-only grep -oP; pipe-safe source guard (A3)
- test-routing.sh fixed (PASS++ suicide under set -e, guarded probes,
  curl 000000 false-pass) rather than deleted - still live tooling
- new scripts/test/test_agent_install.sh (18 passed); backend server
  tests 110 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:00:24 -04:00

460 lines
13 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# ServerKit Agent Installation Script
#
# Usage:
# curl -fsSL https://your-serverkit.com/install.sh | sudo bash -s -- --token "TOKEN" --server "URL"
#
# Options:
# --token, -t Registration token (required)
# --server, -s ServerKit server URL (required)
# --name, -n Display name for this server (optional)
# --version, -v Specific agent version to install (optional, defaults to latest)
# --help, -h Show this help message
#
set -e
# Palette (truecolor violet gradient)
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ]; then
NC=$'\033[0m'; BOLD=$'\033[1m'
fg() { printf '\033[38;2;%s;%s;%sm' "$1" "$2" "$3"; }
else
NC=''; BOLD=''
fg() { :; }
fi
V1="$(fg 196 181 253)"; V2="$(fg 167 139 250)"; V3="$(fg 139 92 246)"
V4="$(fg 124 58 237)"; V5="$(fg 109 40 217)"
WHITE="$(fg 237 233 254)"; MUTED="$(fg 165 160 190)"; FAINT="$(fg 113 108 140)"
GREEN="$(fg 52 211 153)"; RED="$(fg 248 113 113)"; YELLOW="$(fg 250 204 21)"
CYAN="$(fg 103 232 249)"
# Configuration
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc/serverkit-agent"
LOG_DIR="/var/log/serverkit-agent"
SERVICE_USER="serverkit-agent"
GITHUB_REPO="jhd3197/ServerKit"
AGENT_BINARY="serverkit-agent"
# Arguments
TOKEN=""
SERVER_URL=""
SERVER_NAME=""
VERSION="latest"
# Version injected by the panel when it serves this script (the panel already
# resolves the latest agent release for its update endpoints). Empty means
# discover the version via the GitHub API in get_latest_version().
SERVERKIT_AGENT_VERSION=""
print_banner() {
echo
printf " ${V1}${BOLD}╔═╗┌─┐┌─┐┌┬┐┌─┐┬┌─┐${NC}\n"
printf " ${V2}${BOLD}╚═╗│ ││ │ ├┤ │├┤ ${NC} ${WHITE}${BOLD}ServerKit Agent${NC}\n"
printf " ${V3}${BOLD}╚═╝└─┘└─┘ ┴ └─┘┴└──┘${NC} ${FAINT}v%s${NC}\n" "$VERSION"
echo
}
log_info() {
echo -e " ${CYAN}›${NC} $1"
}
log_success() {
echo -e " ${GREEN}●${NC} $1"
}
log_warn() {
echo -e " ${YELLOW}▲${NC} $1"
}
log_error() {
echo -e " ${RED}■${NC} $1"
exit 1
}
show_help() {
echo "ServerKit Agent Installation Script"
echo ""
echo "Usage: install.sh [OPTIONS]"
echo ""
echo "Options:"
echo " --token, -t Registration token (required)"
echo " --server, -s ServerKit server URL (required)"
echo " --name, -n Display name for this server (optional)"
echo " --version, -v Specific agent version (optional, defaults to latest)"
echo " --help, -h Show this help message"
echo ""
echo "Example:"
echo " curl -fsSL https://your-serverkit.com/install.sh | sudo bash -s -- \\"
echo " --token 'sk_reg_xxx' \\"
echo " --server 'https://your-serverkit.com'"
exit 0
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--token|-t)
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
TOKEN="$2"
shift 2
;;
--server|-s)
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
SERVER_URL="$2"
shift 2
;;
--name|-n)
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
SERVER_NAME="$2"
shift 2
;;
--version|-v)
[[ $# -ge 2 ]] || log_error "Option $1 requires a value"
VERSION="$2"
shift 2
;;
--help|-h)
show_help
;;
*)
log_error "Unknown option: $1"
;;
esac
done
# Validate required arguments
if [[ -z "$TOKEN" ]]; then
log_error "Registration token is required. Use --token or -t"
fi
if [[ -z "$SERVER_URL" ]]; then
log_error "Server URL is required. Use --server or -s"
fi
}
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root (use sudo)"
fi
}
detect_platform() {
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64|amd64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
log_error "Unsupported architecture: $ARCH"
;;
esac
if [[ "$OS" != "linux" ]]; then
log_error "This script only supports Linux. For Windows, use install.ps1"
fi
log_info "Detected platform: ${OS}-${ARCH}"
}
check_dependencies() {
log_info "Checking dependencies..."
# Check for required tools
for cmd in curl tar; do
if ! command -v "$cmd" &> /dev/null; then
log_error "$cmd is required but not installed"
fi
done
# Check for systemd
if ! command -v systemctl &> /dev/null; then
log_warn "systemd not found. You will need to start the agent manually."
fi
}
get_latest_version() {
if [[ "$VERSION" != "latest" ]]; then
return 0
fi
# The panel injects the version it already resolved when serving this
# script — use it and keep GitHub out of the enrollment path entirely.
if [[ -n "$SERVERKIT_AGENT_VERSION" ]]; then
VERSION="$SERVERKIT_AGENT_VERSION"
log_info "Using panel-resolved version: v${VERSION}"
return 0
fi
log_info "Fetching latest version..."
# Agent tags share this repo with panel releases, which can push every
# agent-v* tag off the first page — so page through instead of trusting
# the default (30-entry) first page. Capped at 3 pages of 100.
local page releases
VERSION=""
for page in 1 2 3; do
releases=$(curl -fsSL "https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=100&page=${page}") || break
VERSION=$(printf '%s\n' "$releases" | grep -o '"tag_name": *"agent-v[^"]*"' | head -1 | sed -e 's/.*agent-v//' -e 's/"$//') || true
[[ -n "$VERSION" ]] && break
# A page with no tags at all means the release list is exhausted.
printf '%s' "$releases" | grep -q '"tag_name"' || break
done
if [[ -z "$VERSION" ]]; then
log_error "Failed to fetch latest version"
fi
log_info "Latest version: v${VERSION}"
}
# Verify the downloaded archive against the release's checksums.txt.
# Best-effort when checksums.txt is unavailable (older releases); a present-
# but-mismatching checksum is a hard failure.
verify_checksum() {
local tmp_dir="$1" asset_name="$2"
local checksums_url="https://github.com/${GITHUB_REPO}/releases/download/agent-v${VERSION}/checksums.txt"
if ! command -v sha256sum &> /dev/null; then
log_warn "sha256sum not available — skipping checksum verification"
return 0
fi
if ! curl -fsSL "$checksums_url" -o "${tmp_dir}/checksums.txt"; then
log_warn "Could not download checksums.txt — skipping verification"
return 0
fi
if ! grep -q "$asset_name" "${tmp_dir}/checksums.txt"; then
log_warn "checksums.txt has no entry for ${asset_name} — skipping verification"
return 0
fi
if ! (cd "$tmp_dir" && sha256sum -c <(grep "$asset_name" checksums.txt) >/dev/null 2>&1); then
rm -rf "$tmp_dir"
log_error "Checksum verification FAILED for ${asset_name} — aborting install"
fi
log_success "Checksum verified"
}
download_agent() {
log_info "Downloading ServerKit Agent v${VERSION}..."
ASSET_NAME="serverkit-agent-${VERSION}-linux-${ARCH}.tar.gz"
DOWNLOAD_URL="https://github.com/${GITHUB_REPO}/releases/download/agent-v${VERSION}/${ASSET_NAME}"
TMP_DIR=$(mktemp -d)
ARCHIVE="${TMP_DIR}/${ASSET_NAME}"
if ! curl -fsSL "$DOWNLOAD_URL" -o "$ARCHIVE"; then
rm -rf "$TMP_DIR"
log_error "Failed to download agent from: $DOWNLOAD_URL"
fi
verify_checksum "$TMP_DIR" "$ASSET_NAME"
# Extract binary
log_info "Extracting agent..."
tar -xzf "$ARCHIVE" -C "$TMP_DIR"
# Install binary
mv "${TMP_DIR}/serverkit-agent-linux-${ARCH}" "${INSTALL_DIR}/${AGENT_BINARY}"
chmod +x "${INSTALL_DIR}/${AGENT_BINARY}"
# Cleanup
rm -rf "$TMP_DIR"
log_success "Agent installed to ${INSTALL_DIR}/${AGENT_BINARY}"
}
create_user() {
if id "$SERVICE_USER" &>/dev/null; then
log_info "User $SERVICE_USER already exists"
else
log_info "Creating service user: $SERVICE_USER"
if command -v useradd &> /dev/null; then
useradd -r -s /bin/false -d /nonexistent "$SERVICE_USER"
elif command -v adduser &> /dev/null; then
# Alpine/busybox ships adduser instead of useradd
adduser -S -D -H -s /bin/false "$SERVICE_USER"
else
log_error "Cannot create user: neither useradd nor adduser found"
fi
fi
# Add to docker group if it exists
if getent group docker > /dev/null 2>&1; then
if command -v usermod &> /dev/null; then
usermod -aG docker "$SERVICE_USER"
else
# busybox equivalent of usermod -aG
addgroup "$SERVICE_USER" docker
fi
log_info "Added $SERVICE_USER to docker group"
fi
}
create_config_dir() {
log_info "Creating configuration and log directories..."
mkdir -p "$CONFIG_DIR" "$LOG_DIR"
chown "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR"
chmod 750 "$CONFIG_DIR" "$LOG_DIR"
}
register_agent() {
log_info "Registering agent with ServerKit..."
REGISTER_CMD=(
"${INSTALL_DIR}/${AGENT_BINARY}"
--config "${CONFIG_DIR}/config.yaml"
register
--token "${TOKEN}"
--server "${SERVER_URL}"
)
if [[ -n "$SERVER_NAME" ]]; then
REGISTER_CMD+=(--name "${SERVER_NAME}")
fi
if ! "${REGISTER_CMD[@]}"; then
log_error "Agent registration failed"
fi
# Fix permissions on config files
chown -R "$SERVICE_USER:$SERVICE_USER" "$CONFIG_DIR" "$LOG_DIR"
[[ -f "${CONFIG_DIR}/config.yaml" ]] && chmod 600 "${CONFIG_DIR}/config.yaml"
[[ -f "${CONFIG_DIR}/agent.key" ]] && chmod 600 "${CONFIG_DIR}/agent.key"
log_success "Agent registered successfully"
}
install_systemd_service() {
if ! command -v systemctl &> /dev/null; then
log_warn "Skipping systemd service installation (systemd not found)"
return
fi
log_info "Installing systemd service..."
cat > /etc/systemd/system/serverkit-agent.service << EOF
[Unit]
Description=ServerKit Agent
Documentation=https://github.com/${GITHUB_REPO}
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=${SERVICE_USER}
Group=${SERVICE_USER}
ExecStart=${INSTALL_DIR}/${AGENT_BINARY} --config ${CONFIG_DIR}/config.yaml start
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=serverkit-agent
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=${CONFIG_DIR} ${LOG_DIR}
PrivateTmp=true
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryMax=512M
LimitNOFILE=65535
# Environment
Environment=HOME=/nonexistent
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable serverkit-agent
log_success "Systemd service installed"
}
start_service() {
if ! command -v systemctl &> /dev/null; then
log_warn "Cannot auto-start without systemd"
echo ""
echo "To start the agent manually:"
echo " ${INSTALL_DIR}/${AGENT_BINARY} --config ${CONFIG_DIR}/config.yaml start"
return
fi
log_info "Starting ServerKit Agent..."
systemctl start serverkit-agent
sleep 2
if systemctl is-active --quiet serverkit-agent; then
log_success "Agent is running"
else
log_error "Failed to start agent. Check logs with: journalctl -u serverkit-agent"
fi
}
print_success() {
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Installation completed successfully! ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
echo "Agent Status:"
echo " Binary: ${INSTALL_DIR}/${AGENT_BINARY}"
echo " Config: ${CONFIG_DIR}/config.yaml"
echo " Service: serverkit-agent"
echo ""
echo "Useful commands:"
echo " Check status: systemctl status serverkit-agent"
echo " View logs: journalctl -u serverkit-agent -f"
echo " Restart agent: systemctl restart serverkit-agent"
echo " Stop agent: systemctl stop serverkit-agent"
echo ""
}
# Main execution
main() {
print_banner
parse_args "$@"
check_root
detect_platform
check_dependencies
get_latest_version
download_agent
create_user
create_config_dir
register_agent
install_systemd_service
start_service
print_success
}
# Sourcing this file (e.g. from scripts/test/test_agent_install.sh) defines
# every function above for unit testing without running an install. Piped
# execution (curl | bash) has no BASH_SOURCE[0], so only a real `source`
# takes the early return.
if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then
return 0
fi
main "$@"