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:
parent
d1c4619a01
commit
3c29af55a2
3 changed files with 296 additions and 0 deletions
18
README.md
18
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"
|
||||
|
|
|
|||
256
scripts/install.sh
Executable file
256
scripts/install.sh
Executable 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue