feat(api): serve the host installer and its downloads

GET /install.sh serves a POSIX script, embedded in the API at build time
so the script and the routes that redeem its token ship together. It
checks the platform, asks where box images should live, downloads the
host agent at a pinned version, verifies it against SHA256SUMS, installs
it for the calling user and hands over with the token in the environment
rather than argv.

GET /install/:component/:version/:asset redirects to a one-minute signed
URL on a private S3-compatible bucket, so every download passes through a
route that can be logged or switched off. The SigV4 signer is written
against Web Crypto and checked against AWS's published example.
This commit is contained in:
Wanjohi
2026-09-28 09:12:23 +03:00
parent 54dbe8628a
commit 065af689b3
8 changed files with 351 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env sh
# Nestri host installer — https://api.nestri.io/install.sh
#
# This file is the source of what that URL serves, kept in the public
# repository so anyone about to pipe it into a shell can read it first.
#
# curl -fsSL https://api.nestri.io/install.sh | sh -s -- <install-token>
#
# What it does, in order: check this is 64-bit Linux, ask where box images
# should live, download the host agent for this platform, verify it against the
# published SHA256SUMS, install it to ~/.local/bin, and hand over to
# the agent's own onboarding, which checks the machine, registers it with the
# token and starts the agent as a systemd user service. It never asks for sudo: the agent
# runs as the user who ran this.
#
# The token comes from the dashboard's Installation page. It registers one
# machine, works once and lapses after an hour.
set -eu
API="${NESTRI_API:-https://api.nestri.io}"
# Pinned, not "latest", so the script and the binary it installs are a pair
# somebody chose. Bump when cutting a release; NESTRI_HOST_VERSION overrides it.
DEFAULT_VERSION="0.1.0"
VERSION="${NESTRI_HOST_VERSION:-$DEFAULT_VERSION}"
BIN_DIR="${NESTRI_BIN_DIR:-$HOME/.local/bin}"
say() { printf '%s\n' "$*" >&2; }
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
TOKEN="${1:-${NESTRI_INSTALL_TOKEN:-}}"
[ -n "$TOKEN" ] || die "no install token. Copy the full command from the dashboard's Installation page."
# --- platform ---------------------------------------------------------------
[ "$(uname -s)" = Linux ] || die "a host has to run Linux (with KVM); this is $(uname -s)."
case "$(uname -m)" in
x86_64|amd64) target=x86_64-unknown-linux-musl ;;
*) die "no host build for $(uname -m) yet." ;;
esac
[ "$(id -u)" -ne 0 ] || die "run this as the user that will run boxes, not as root."
# --- fetch ------------------------------------------------------------------
if command -v curl >/dev/null 2>&1; then
get() { curl -fsSL "$1" -o "$2"; }
elif command -v wget >/dev/null 2>&1; then
get() { wget -qO "$2" "$1"; }
else
die "need curl or wget"
fi
# --- where box images go ----------------------------------------------------
# Their own device, xfs or ext4, and never `/`: box images are large, and a
# filled root filesystem takes the whole machine down with it. The agent's
# preflight checks this again; asking here is so the default is a good guess.
tty_ok() { [ -e /dev/tty ] && (exec 3</dev/tty) 2>/dev/null; }
BOX_STORE="${NESTRI_BOX_STORE:-}"
if [ -z "$BOX_STORE" ]; then
guess="$(df -P -T -x tmpfs -x devtmpfs -x overlay 2>/dev/null \
| awk 'NR>1 && ($2=="xfs"||$2=="ext4") && $7!="/" && $7!~/^\/(boot|efi)/ {print $5, $7}' \
| sort -rn | awk 'NR==1 {print $2}')"
default="${guess:+$guess/nestri}"
if tty_ok; then
printf 'Where should box images go? (xfs or ext4, not /) [%s]: ' "${default:-none found}" >&2
read -r answer </dev/tty || answer=""
BOX_STORE="${answer:-$default}"
else
BOX_STORE="$default"
fi
[ -n "$BOX_STORE" ] || die "no xfs or ext4 filesystem besides / was found. Mount one, or set NESTRI_BOX_STORE."
fi
# --- download and verify ----------------------------------------------------
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT INT TERM
ASSET="nestri-host-$target"
BASE="$API/install/host/$VERSION"
say "Downloading the host agent $VERSION ($target)…"
get "$BASE/$ASSET" "$TMP/$ASSET" || die "download failed: $BASE/$ASSET"
# A checksum fetched from the same place as the binary is not a security
# boundary. It catches a truncated or corrupted download, which is the failure
# that actually happens; the download itself is over TLS from our API.
get "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" || die "no SHA256SUMS for $VERSION"
want="$(grep -F " $ASSET" "$TMP/SHA256SUMS" | cut -d' ' -f1 | head -n1)"
[ -n "$want" ] || die "no checksum for $ASSET in SHA256SUMS"
if command -v sha256sum >/dev/null 2>&1; then
have="$(sha256sum "$TMP/$ASSET" | cut -d' ' -f1)"
else
have="$(shasum -a 256 "$TMP/$ASSET" | cut -d' ' -f1)"
fi
[ "$have" = "$want" ] || die "checksum mismatch — not installing
expected $want
got $have"
say "Checksum OK."
mkdir -p "$BIN_DIR"
chmod +x "$TMP/$ASSET"
mv "$TMP/$ASSET" "$BIN_DIR/nestri-host"
say "Installed $BIN_DIR/nestri-host"
say ""
# --- onboard ----------------------------------------------------------------
# The token goes through the environment rather than argv, so it is not in
# `ps` for the length of the run.
export NESTRI_INSTALL_TOKEN="$TOKEN" NESTRI_BOX_STORE="$BOX_STORE" NESTRI_API="$API"
if tty_ok; then
exec "$BIN_DIR/nestri-host" onboard </dev/tty
else
exec "$BIN_DIR/nestri-host" onboard
fi