From 3c29af55a2e9005664b9ab826a71fe12700ae4cd Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Wed, 29 Apr 2026 14:06:34 -0300 Subject: [PATCH] Add curl|bash installer + wire upload into publish script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/install.sh is the one-command installer end users run as curl -fsSL https://releases.thaloco.com/banger/install.sh | bash Design choices: * Runs as the invoking user. All network work + signature verification happens unprivileged; sudo is only re-execed for the actual install step that writes to /usr/local and creates systemd units. * Right before the sudo prompt, the script prints a plain-language summary of exactly what's about to happen — the file paths it will create and a one-line "why sudo" — so the user authorises a known scope rather than the whole pipeline. Detail link in the docs. * Uses openssl (universally available) for signature verification, not cosign. cosign is needed only by the *signer*, never the verifier. * No jq dependency. The latest_stable field is extracted from the manifest with grep+sed, since the manifest shape is well-defined and we control it. * /dev/tty fallback for the confirmation prompt so it works through the curl|bash pipe. * --yes for non-interactive CI use, --user for installing into ~/.local/bin without touching system paths, --version vX.Y.Z to pin. publish-banger-release.sh now uploads install.sh to the bucket root on every publish, so the curl URL is stable but the script logic matches the latest verified release. It also runs a key-drift check: if scripts/install.sh's embedded cosign public key differs from the one in internal/updater/verify_signature.go, publishing aborts. The two copies must stay in sync or one of them ends up rejecting every release. README's Quick start now leads with the installer one-liner and documents the audit-first variant alongside it; building from source moves below. Smoke-tested end to end against the live bucket with --user mode: manifest fetch → tarball download → cosign signature verify → hash verify → extract → install. The installed binary reports v0.1.0 at commit 6fdebd9, matching the published artifact. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 18 +++ scripts/install.sh | 256 ++++++++++++++++++++++++++++++ scripts/publish-banger-release.sh | 22 +++ 3 files changed, 296 insertions(+) create mode 100755 scripts/install.sh diff --git a/README.md b/README.md index 0e656b6..57c6eb6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,24 @@ One-command development sandboxes on Firecracker microVMs. ## Quick start +```bash +curl -fsSL https://releases.thaloco.com/banger/install.sh | bash +banger vm run --name sandbox +``` + +The installer runs as you, downloads + verifies the latest signed +release, then prompts before re-execing `sudo` for the system-install +step (writing `/usr/local/bin` + creating systemd units). If you'd +rather audit the script first: + +```bash +curl -fsSL https://releases.thaloco.com/banger/install.sh -o install.sh +less install.sh +bash install.sh +``` + +Or build from source: + ```bash make build sudo ./build/bin/banger system install --owner "$USER" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..76292bd --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,256 @@ +#!/usr/bin/env bash +# install.sh — one-command installer for banger. +# +# Designed to be invoked as: +# +# curl -fsSL https://releases.thaloco.com/banger/install.sh | bash +# +# The script runs as the invoking user, downloads + verifies the +# release tarball unprivileged, and only re-execs sudo for the actual +# install step (writing to /usr/local/* and creating systemd units). +# Right before the sudo prompt the user gets a plain-language summary +# of exactly what's about to happen, so they're authorising a known +# scope rather than the whole pipeline. +# +# Flags: +# --yes skip the interactive confirmation (CI / scripted use) +# --user install binaries to ~/.local/bin and stop; the user +# runs `sudo banger system install` later when ready +# --version v install a specific version instead of latest_stable +# +# Trust model: +# * The cosign public key below is pinned at script-write time and +# matches internal/updater/verify_signature.go in the source repo. +# * The script verifies the cosign signature on SHA256SUMS, then +# verifies the tarball's hash against SHA256SUMS, before extracting. +# * Verification uses openssl (every Linux distro ships it). cosign +# is needed only for *signing* a release, never for verifying one. +# * Manifest URL is hardcoded so a DNS-redirect cannot point us at a +# different bucket. + +set -euo pipefail + +MANIFEST_URL="https://releases.thaloco.com/banger/manifest.json" +BUCKET_BASE="https://releases.thaloco.com/banger" +TRUST_DOC_URL="https://git.thaloco.com/thaloco/banger/src/branch/main/docs/privileges.md" + +# This must stay byte-identical to BangerReleasePublicKey in +# internal/updater/verify_signature.go — publish-banger-release.sh +# rejects publishing if they drift apart. +BANGER_PUBLIC_KEY='-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElWFSLKLosBrdjfuF8ZS6U01Ufky4 +zNeVPCkA6HEJ/oe634fRqwFxkXKGWg03eGFSnlwRxnUxN2+duXQSsR0pzQ== +-----END PUBLIC KEY-----' + +log() { printf '[banger-install] %s\n' "$*" >&2; } +warn() { printf '[banger-install] WARN: %s\n' "$*" >&2; } +die() { printf '[banger-install] ERROR: %s\n' "$*" >&2; exit 1; } + +# ---------------------------------------------------------------------- +# Flag parsing +# ---------------------------------------------------------------------- +ASSUME_YES=0 +USER_INSTALL=0 +TARGET_VERSION="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -y|--yes) ASSUME_YES=1 ;; + --user) USER_INSTALL=1 ;; + --version) TARGET_VERSION="${2:-}"; shift ;; + --version=*) TARGET_VERSION="${1#--version=}" ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) die "unknown argument: $1 (try --help)" ;; + esac + shift +done + +# ---------------------------------------------------------------------- +# Platform + tool prerequisites +# ---------------------------------------------------------------------- +[[ "$(uname -s)" == "Linux" ]] || die "banger only supports Linux (saw $(uname -s))" +[[ "$(uname -m)" == "x86_64" ]] || die "banger only supports amd64 (saw $(uname -m))" + +for tool in curl sha256sum tar openssl mktemp base64 grep sed; do + command -v "$tool" >/dev/null \ + || die "required tool not in PATH: $tool" +done + +# ---------------------------------------------------------------------- +# Resolve target version +# ---------------------------------------------------------------------- +if [[ -z "$TARGET_VERSION" ]]; then + log "fetching $MANIFEST_URL" + MANIFEST=$(curl -fsSL --max-time 30 "$MANIFEST_URL") \ + || die "failed to fetch manifest" + # Pull `latest_stable` out without depending on jq — manifest shape + # is well-defined and we control it. + TARGET_VERSION=$(printf '%s' "$MANIFEST" \ + | grep -oE '"latest_stable"[[:space:]]*:[[:space:]]*"v[^"]+"' \ + | head -n1 \ + | sed -E 's/.*"v([^"]+)".*/v\1/') + [[ -n "$TARGET_VERSION" ]] || die "could not parse latest_stable from manifest" +fi + +case "$TARGET_VERSION" in + v*.*.*) ;; + *) die "unexpected version shape: $TARGET_VERSION (want vX.Y.Z)" ;; +esac + +log "target version: $TARGET_VERSION" + +# ---------------------------------------------------------------------- +# Download tarball + sums + signature +# ---------------------------------------------------------------------- +WORK_DIR=$(mktemp -d -t banger-install.XXXXXX) +trap 'rm -rf "$WORK_DIR"' EXIT + +TARBALL_NAME="banger-$TARGET_VERSION-linux-amd64.tar.gz" +RELEASE_BASE="$BUCKET_BASE/$TARGET_VERSION" + +log "downloading $TARBALL_NAME" +curl -fsSL --max-time 300 "$RELEASE_BASE/$TARBALL_NAME" -o "$WORK_DIR/$TARBALL_NAME" \ + || die "failed to download tarball" +curl -fsSL --max-time 30 "$RELEASE_BASE/SHA256SUMS" -o "$WORK_DIR/SHA256SUMS" \ + || die "failed to download SHA256SUMS" +curl -fsSL --max-time 30 "$RELEASE_BASE/SHA256SUMS.sig" -o "$WORK_DIR/SHA256SUMS.sig" \ + || die "failed to download SHA256SUMS.sig" + +# ---------------------------------------------------------------------- +# Verify cosign signature on SHA256SUMS (the tarball's hash is INSIDE +# SHA256SUMS, so a valid signature on SHA256SUMS plus a hash match on +# the tarball authenticates the whole release). +# ---------------------------------------------------------------------- +log "verifying signature on SHA256SUMS" +printf '%s\n' "$BANGER_PUBLIC_KEY" > "$WORK_DIR/cosign.pub" +base64 -d "$WORK_DIR/SHA256SUMS.sig" > "$WORK_DIR/SHA256SUMS.sig.bin" \ + || die "signature is not valid base64" +openssl dgst -sha256 \ + -verify "$WORK_DIR/cosign.pub" \ + -signature "$WORK_DIR/SHA256SUMS.sig.bin" \ + "$WORK_DIR/SHA256SUMS" >/dev/null 2>&1 \ + || die "signature verification failed — refusing to install" +log " signature OK" + +# ---------------------------------------------------------------------- +# Verify tarball hash against SHA256SUMS +# ---------------------------------------------------------------------- +log "verifying $TARBALL_NAME against SHA256SUMS" +( cd "$WORK_DIR" && sha256sum -c --status SHA256SUMS ) \ + || die "tarball hash mismatch — refusing to install" +log " hash OK" + +# ---------------------------------------------------------------------- +# Extract (validation is server-side via StageTarball when banger +# update runs; the install script trusts the verified tarball). +# ---------------------------------------------------------------------- +log "extracting" +mkdir -p "$WORK_DIR/stage" +tar -xzf "$WORK_DIR/$TARBALL_NAME" -C "$WORK_DIR/stage" + +for bin in banger bangerd banger-vsock-agent; do + [[ -f "$WORK_DIR/stage/$bin" ]] \ + || die "tarball missing expected binary: $bin" +done + +# ---------------------------------------------------------------------- +# --user mode: drop binaries in ~/.local/bin and stop +# ---------------------------------------------------------------------- +if [[ "$USER_INSTALL" -eq 1 ]]; then + USER_BIN="${HOME}/.local/bin" + USER_LIB="${HOME}/.local/lib/banger" + mkdir -p "$USER_BIN" "$USER_LIB" + install -m 0755 "$WORK_DIR/stage/banger" "$USER_BIN/banger" + install -m 0755 "$WORK_DIR/stage/bangerd" "$USER_BIN/bangerd" + install -m 0755 "$WORK_DIR/stage/banger-vsock-agent" "$USER_LIB/banger-vsock-agent" + cat <&2 + +Installed banger $TARGET_VERSION to: + $USER_BIN/banger + $USER_BIN/bangerd + $USER_LIB/banger-vsock-agent + +Make sure $USER_BIN is in your PATH, then finish setup with: + sudo $USER_BIN/banger system install + $USER_BIN/banger doctor + +EOF + exit 0 +fi + +# ---------------------------------------------------------------------- +# System install: confirm scope, then re-exec sudo +# ---------------------------------------------------------------------- +SUMMARY=$(cat </dev/null \ + || die "not running as root and sudo is not in PATH" + SUDO="sudo" +fi + +# Copy binaries into place. We do the copies + chmod + system install +# from the *staged* tarball under $WORK_DIR; using `install` is the +# right tool here because it handles atomic-ish replacement and mode +# bits in one shot. +$SUDO install -m 0755 -D "$WORK_DIR/stage/banger" /usr/local/bin/banger +$SUDO install -m 0755 -D "$WORK_DIR/stage/bangerd" /usr/local/bin/bangerd +$SUDO install -m 0755 -D "$WORK_DIR/stage/banger-vsock-agent" /usr/local/lib/banger/banger-vsock-agent + +log "registering systemd units (banger system install)" +$SUDO /usr/local/bin/banger system install + +cat <&2 + +banger $TARGET_VERSION installed. + +Next steps: + banger doctor # confirm host readiness + banger image pull debian-bookworm # fetch a default image + banger vm run # boot a sandbox + +Updates land via: + banger update --check + sudo banger update + +EOF diff --git a/scripts/publish-banger-release.sh b/scripts/publish-banger-release.sh index 7e870fa..6b76501 100755 --- a/scripts/publish-banger-release.sh +++ b/scripts/publish-banger-release.sh @@ -155,6 +155,19 @@ cosign verify-blob \ --signature "$OUT_DIR/SHA256SUMS.sig" \ "$OUT_DIR/SHA256SUMS" +# install.sh embeds its own copy of the public key for end-user +# verification (curl|bash trust path). Make sure the two copies didn't +# drift; a release with mismatched keys would either reject all +# `banger update` calls or all `install.sh | bash` runs. +log "checking install.sh embedded key matches verify_signature.go" +INSTALL_PUB="$OUT_DIR/install-script-pubkey.pem" +sed -n "/-----BEGIN PUBLIC KEY-----/,/-----END PUBLIC KEY-----/p" \ + "$REPO_ROOT/scripts/install.sh" \ + | sed -E "s/.*(-----BEGIN PUBLIC KEY-----)/\\1/; s/(-----END PUBLIC KEY-----).*/\\1/" \ + > "$INSTALL_PUB" +diff -q "$EMBEDDED_PUB" "$INSTALL_PUB" >/dev/null \ + || die "scripts/install.sh embedded key differs from internal/updater/verify_signature.go; sync them before publishing" + # Build the manifest. Pull the existing manifest from the bucket so # we don't lose previous release entries, append this one, bump # latest_stable, write back. @@ -206,6 +219,15 @@ rclone copy "$OUT_DIR/SHA256SUMS.sig" "$RCLONE_DEST_BASE/$VERSION/" log "uploading manifest" rclone copy "$NEW_MANIFEST" "$RCLONE_DEST_BASE/" +# install.sh lives at the bucket root (unversioned) so the +# `curl ... install.sh | bash` URL stays stable across releases. The +# script reads manifest.json to find the current latest_stable, so as +# long as install.sh's logic doesn't break, it keeps working for older +# releases too. +log "uploading install.sh" +rclone copy "$REPO_ROOT/scripts/install.sh" "$RCLONE_DEST_BASE/" + log "done. verify with:" log " curl -fsSL $BASE_URL/$BUCKET_PATH/manifest.json | jq ." +log " curl -fsSL $BASE_URL/$BUCKET_PATH/install.sh | head -20" log " banger update --check"