From 400bd9b9f1ef7e71e693fb79fd2bc730aa5b20e1 Mon Sep 17 00:00:00 2001 From: bcherb2 Date: Mon, 17 Aug 2026 11:20:45 -0400 Subject: [PATCH] fix: install bws from the private tier, not the manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .tests/test.sh | 14 +++- dot_local/bin/executable_dotup | 109 +++++++++++++++-------------- dot_local/share/dotup/packages.tsv | 2 +- 3 files changed, 70 insertions(+), 55 deletions(-) diff --git a/.tests/test.sh b/.tests/test.sh index 80df20e..1aadec5 100755 --- a/.tests/test.sh +++ b/.tests/test.sh @@ -107,8 +107,20 @@ is "dropping uv drops specify-cli" "off" "$(on agents/specify-cli)" # is real rather than decorative. reset; sh $D toggle p:private/bws-secrets >/dev/null is "ticking bws-secrets pulls in unzip" "on" "$(on core/unzip)" -is "bws-secrets has an install channel" "-tarball" \ +# bws must NOT be a manifest package. selected_packages drops every private row +# ("private is never a package" is what makes --unattended safe), and a safe row +# would put a Bitwarden binary on every throwaway public VM via the defaults +# preset. So the private tier installs it itself, next to the token. +is "bws-secrets stays a non-package" "-" \ "$(awk -F'\t' '$1=="private" && $2=="bws-secrets" {print $4}' "$M")" +is "no manifest row installs bws" "0" \ + "$(awk -F'\t' '!/^[#@]/ && NF>=3 && $2=="bws" {c++} END{print c+0}' "$M")" +is "the private tier installs bws itself" "1" \ + "$(grep -c '^ ensure_bws$' $D)" +is "…only after a token exists to use" "1" \ + "$(grep -A1 'bws token written, mode 600' $D | grep -c ensure_bws)" +is "bws is checksum-verified" "1" \ + "$(grep -c 'bws checksum mismatch' $D)" printf '\n\033[1mcredentials never reach a log\033[0m\n' # `run` echoes its whole argv to stderr. The private init's argv ends in diff --git a/dot_local/bin/executable_dotup b/dot_local/bin/executable_dotup index cb53ce5..2f0d12a 100755 --- a/dot_local/bin/executable_dotup +++ b/dot_local/bin/executable_dotup @@ -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#://[^/@]*@#://@#'; } +# 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 diff --git a/dot_local/share/dotup/packages.tsv b/dot_local/share/dotup/packages.tsv index 4f41fc4..ea34030 100644 --- a/dot_local/share/dotup/packages.tsv +++ b/dot_local/share/dotup/packages.tsv @@ -118,4 +118,4 @@ virt libvirt invasive libvirt-daemon-system - daemon + group membership virt virt-manager invasive virt-manager - @private one password, typed after the install finishes private private-repo private - - ~/.local/share/dotfiles-private — agent config, ssh config -private bws-secrets private -tarball -tarball installs bws, then 7 API keys into ~/.config/zsh/secrets.zsh +private bws-secrets private - - bws + 7 API keys into ~/.config/zsh/secrets.zsh; the tier installs bws itself