fix: install bws from the private tier, not the manifest

The previous commit put the bws install on the private/bws-secrets row as
a tarball channel. That row can never install anything: selected_packages
drops every row flagged `private`, which is the invariant that makes
--unattended safe to run. The container proved it — unzip installed, bws
did not, and the row still could not deliver on its promise.

Nor can it be a safe row: the defaults preset ticks every safe package, so
that would put a Bitwarden binary and a 12 MB GitHub download on every
throwaway public VM, for a tool those machines have no credential to use.

So ensure_bws lives in dotup and is called from cmd_private immediately
after the token is written. Nothing installs bws unless something is about
to hand it a token, and the manifest keeps its invariant.

core/unzip stays a real package with the @needs edge — the release is a
.zip and 24.04 minimal has no unzip.
This commit is contained in:
bcherb2
2026-08-17 11:20:45 -04:00
parent 90bca39396
commit 400bd9b9f1
3 changed files with 70 additions and 55 deletions
+56 -53
View File
@@ -379,6 +379,61 @@ norm() { printf '%s\n' "$*" | tr ' ' '\n' | grep . | tr '\n' ' ' | sed 's/ $//';
# Used wherever a URL that may carry a credential is about to be printed.
redact_url() { printf '%s\n' "$1" | sed 's#://[^/@]*@#://<redacted>@#'; }
# The Bitwarden Secrets Manager CLI, installed by the PRIVATE tier only.
#
# It cannot be a manifest row. `selected_packages` drops every row flagged
# `private` -- "private is never a package" is the invariant that lets
# --unattended be safe -- so a private row can never install anything. And it
# must not be a `safe` row either: the defaults preset ticks every safe package,
# which would put a Bitwarden binary and a 12 MB GitHub download on every
# throwaway public VM, for a tool those machines have no credential to use.
#
# So it lives here, called from cmd_private beside the token it exists to read.
# Nothing installs bws unless something is about to hand it a token.
ensure_bws() {
have bws && { say " bws $(bws --version 2>/dev/null | awk '{print $2}') already present"; return 0; }
command -v unzip >/dev/null 2>&1 || { err "bws needs unzip; install it and re-run"; return 1; }
# musl, not gnu: the gnu build pins a glibc newer than some LTS images
# carry, and this has to work on whatever a fresh VM turns out to be.
case $(uname -s) in
Darwin) t=macos-universal ;;
*) case $(uname -m) in
x86_64|amd64) t=x86_64-unknown-linux-musl ;;
aarch64|arm64) t=aarch64-unknown-linux-musl ;;
*) err "no bws build for $(uname -m)"; return 1 ;;
esac ;;
esac
# sdk-sm is a monorepo with per-component tags, so the `latest` release is
# usually a python SDK rather than bws. Filter by tag prefix.
v=$(curl -fsSL 'https://api.github.com/repos/bitwarden/sdk-sm/releases?per_page=40' 2>/dev/null \
| sed -n 's/.*"tag_name": *"bws-v\([0-9.]*\)".*/\1/p' | head -1)
[ -n "$v" ] || { err "could not resolve the current bws version"; return 1; }
b=https://github.com/bitwarden/sdk-sm/releases/download/bws-v$v
say " fetching bws $v"
curl -fsSL "$b/bws-$t-$v.zip" -o /tmp/bws.zip 2>/dev/null \
|| { err "bws download failed"; return 1; }
# A binary about to hold the key to every other credential is worth verifying.
if have sha256sum && curl -fsSL "$b/bws-sha256-checksums-$v.txt" -o /tmp/bws.sums 2>/dev/null; then
want=$(awk -v f="bws-$t-$v.zip" '$2==f || $2=="*"f {print $1}' /tmp/bws.sums | head -1)
got=$(sha256sum /tmp/bws.zip | awk '{print $1}')
if [ -n "$want" ] && [ "$want" != "$got" ]; then
err "bws checksum mismatch — refusing to install"
rm -f /tmp/bws.zip /tmp/bws.sums; return 1
fi
[ -n "$want" ] && say " bws checksum verified"
rm -f /tmp/bws.sums
else
warn " bws checksums unavailable — installing unverified"
fi
mkdir -p "$HOME/.local/bin" /tmp/bws.d
unzip -oq /tmp/bws.zip -d /tmp/bws.d 2>/dev/null \
|| { err "bws extract failed"; rm -rf /tmp/bws.zip /tmp/bws.d; return 1; }
install -m 755 "$(find /tmp/bws.d -type f -name bws | head -1)" "$HOME/.local/bin/bws" \
|| { err "bws install failed"; rm -rf /tmp/bws.zip /tmp/bws.d; return 1; }
rm -rf /tmp/bws.zip /tmp/bws.d
say " bws $v installed to ~/.local/bin"
}
run() {
if [ "$DRYRUN" -eq 1 ]; then printf ' + %s\n' "$*"; return 0; fi
printf '%s + %s%s\n' "$DIM" "$*" "$R" >&2
@@ -574,59 +629,6 @@ install_bespoke() {
run_sh "${SUDO:+$SUDO }rm -rf /usr/local/go && ${SUDO:+$SUDO }tar -xzf /tmp/go.tgz -C /usr/local" \
|| { note_fail "$key" "tarball extract failed"; continue; }
run_sh "rm -f /tmp/go.tgz" ;;
private/bws-secrets)
# The Bitwarden Secrets Manager CLI. Without it the private tier
# fetches nothing: `dotsecrets` shells out to `bws secret get`, so
# this row promising seven API keys and not installing the binary
# was a promise it could not keep.
#
# User-level, ~/.local/bin, no sudo. It is a single static binary
# and the private tier is per-user by definition.
if have bws && [ "$DRYRUN" -eq 0 ]; then
say " bws $(bws --version 2>/dev/null | awk '{print $2}') already present — leaving it"
continue
fi
# musl, not gnu: the gnu build pins a glibc newer than some LTS
# images carry, and this has to work on whatever a fresh VM is.
case $(uname -s) in
Darwin) t=macos-universal ;;
*) case $(uname -m) in
x86_64|amd64) t=x86_64-unknown-linux-musl ;;
aarch64|arm64) t=aarch64-unknown-linux-musl ;;
*) note_fail "$key" "no bws build for $(uname -m)"; continue ;;
esac ;;
esac
if [ "$DRYRUN" -eq 1 ]; then v=2.X.Y
else
# Releases are tagged per-component in this monorepo, so the
# `latest` endpoint points at whatever shipped last -- often a
# python SDK, not bws. Filter by tag prefix instead.
v=$(curl -fsSL 'https://api.github.com/repos/bitwarden/sdk-sm/releases?per_page=40' 2>/dev/null \
| sed -n 's/.*"tag_name": *"bws-v\([0-9.]*\)".*/\1/p' | head -1)
fi
[ -n "$v" ] || { note_fail "$key" "could not resolve the current bws version"; continue; }
b=https://github.com/bitwarden/sdk-sm/releases/download/bws-v$v
run_sh "curl -fsSL '$b/bws-$t-$v.zip' -o /tmp/bws.zip" \
|| { note_fail "$key" "download failed"; continue; }
# Bitwarden publishes checksums; a binary that is about to hold the
# key to every other credential is worth verifying.
if [ "$DRYRUN" -eq 0 ] && have sha256sum; then
if curl -fsSL "$b/bws-sha256-checksums-$v.txt" -o /tmp/bws.sums 2>/dev/null; then
want=$(awk -v f="bws-$t-$v.zip" '$2==f || $2=="*"f {print $1}' /tmp/bws.sums | head -1)
got=$(sha256sum /tmp/bws.zip | awk '{print $1}')
if [ -n "$want" ] && [ "$want" != "$got" ]; then
note_fail "$key" "checksum mismatch — refusing to install"
rm -f /tmp/bws.zip /tmp/bws.sums; continue
fi
[ -n "$want" ] && say " checksum verified"
else
warn " bws: checksums unavailable — installing unverified"
fi
rm -f /tmp/bws.sums
fi
run_sh "mkdir -p \"\$HOME/.local/bin\" && unzip -oq /tmp/bws.zip -d /tmp/bws.d && install -m 755 \"\$(find /tmp/bws.d -type f -name bws | head -1)\" \"\$HOME/.local/bin/bws\"" \
|| { note_fail "$key" "extract failed"; rm -rf /tmp/bws.zip /tmp/bws.d; continue; }
run_sh "rm -rf /tmp/bws.zip /tmp/bws.d" ;;
core/chezmoi)
# Circular by nature: dotup arrives *via* chezmoi. Present already
# in every case that matters; here for the one where it is not.
@@ -787,6 +789,7 @@ cmd_private() {
( umask 077; printf '%s\n' "$p_tok" > "$BWS_TOKEN" )
chmod 600 "$BWS_TOKEN"
say " bws token written, mode 600"
ensure_bws
else err "the blob carried no BWS_ACCESS_TOKEN"; fi
fi
unset p_tok