Add curl|bash installer + wire upload into publish script

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) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-29 14:06:34 -03:00
parent d1c4619a01
commit 3c29af55a2
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 296 additions and 0 deletions

View file

@ -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"

256
scripts/install.sh Executable file
View file

@ -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 <<EOF >&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 <<EOF
About to install banger $TARGET_VERSION (requires sudo):
/usr/local/bin/banger
/usr/local/bin/bangerd
/usr/local/lib/banger/banger-vsock-agent
/etc/systemd/system/bangerd.service (background daemon)
/etc/systemd/system/bangerd-root.service (privileged helper)
Why sudo: banger needs permission to automatically manage network
access for the VMs you launch. The privileged work runs in a small
helper service; the rest runs as you.
For details, see: $TRUST_DOC_URL
EOF
)
printf '%s\n' "$SUMMARY"
if [[ "$ASSUME_YES" -ne 1 ]]; then
if [[ ! -t 0 ]] && [[ ! -r /dev/tty ]]; then
die "no terminal available to confirm; re-run with --yes"
fi
REPLY=""
if [[ -t 0 ]]; then
read -r -p "Continue? [y/N] " REPLY
else
# curl|bash path: stdin is the pipe; reach for the user's tty.
read -r -p "Continue? [y/N] " REPLY < /dev/tty
fi
case "$REPLY" in
y|Y|yes|YES) ;;
*) die "aborted by user" ;;
esac
fi
log "elevating to sudo for the install step"
SUDO=""
if [[ "$EUID" -ne 0 ]]; then
command -v sudo >/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 <<EOF >&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

View file

@ -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"