fix: install bws, isolate the private config, keep credentials out of logs
Three defects the private tier could not survive a fresh machine with. bws was never installed. The private/bws-secrets row promised seven API keys and shipped no way to fetch them: dotsecrets shells out to `bws secret get`, and nothing put that binary on the box. Added as a tarball channel with checksum verification, musl rather than gnu so it does not pin a glibc newer than an older LTS carries, plus core/unzip as a real @needs dependency since the release is a .zip and 24.04 minimal has no unzip. The releases API needs filtering by tag prefix: sdk-sm is a monorepo and `latest` usually points at a python SDK, not bws. Both tiers rendered their config to ~/.config/chezmoi/chezmoi.toml, so re-running the public installer overwrote the private config and took its seven promptStringOnce answers with it. Silently -- the templates degrade politely when data is missing, so the symptom was `git commit` not knowing who you are, days later. The private tier now renders to private.toml and the cmp alias carries the matching -c. `run` echoes its argv to stderr, and the private init argv ends in https://user:TOKEN@host -- into scrollback, any `dotup 2>log`, and any agent transcript. Traced through redact_url instead, in both the live and dry-run branches. The clone is also chmod -R go-rwx afterwards: it lands at the caller umask, and .git/config stores that same credential URL. Tests: 90 passing, 9 new covering all three.
This commit is contained in:
@@ -102,6 +102,38 @@ is "@spec lines are not packages" "0" \
|
|||||||
"$(sh $D render | cut -f2 | grep -c 'spec' || true)"
|
"$(sh $D render | cut -f2 | grep -c 'spec' || true)"
|
||||||
reset; sh $D toggle p:core/uv >/dev/null
|
reset; sh $D toggle p:core/uv >/dev/null
|
||||||
is "dropping uv drops specify-cli" "off" "$(on agents/specify-cli)"
|
is "dropping uv drops specify-cli" "off" "$(on agents/specify-cli)"
|
||||||
|
# bws-secrets promised seven API keys and installed nothing to fetch them with.
|
||||||
|
# The binary ships as a .zip and 24.04 minimal has no unzip, so the dependency
|
||||||
|
# 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" \
|
||||||
|
"$(awk -F'\t' '$1=="private" && $2=="bws-secrets" {print $4}' "$M")"
|
||||||
|
|
||||||
|
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
|
||||||
|
# https://user:TOKEN@host, so it must not go through `run`.
|
||||||
|
is "private init does not use run()" "0" \
|
||||||
|
"$(grep -c 'run chezmoi init' $D)"
|
||||||
|
# Two, not one: the dry-run branch traces it too, and a dry run that printed a
|
||||||
|
# live token would be the worse leak of the pair.
|
||||||
|
is "both traced URLs are redacted" "2" \
|
||||||
|
"$(grep -c 'redact_url "\$p_repo"' $D)"
|
||||||
|
# Exercise the real implementation lifted straight out of dotup. A copy of the
|
||||||
|
# sed expression here would keep passing after someone edited the original.
|
||||||
|
rd() { eval "$(sed -n '/^redact_url()/p' $D)"; redact_url "$1"; }
|
||||||
|
is "redact_url strips userinfo" "https://<redacted>@git.example.com/x.git" \
|
||||||
|
"$(rd 'https://ben:deadbeefcafe@git.example.com/x.git')"
|
||||||
|
is "...and leaves a credential-free URL alone" "https://git.example.com/x.git" \
|
||||||
|
"$(rd 'https://git.example.com/x.git')"
|
||||||
|
# Two tiers, two configs. Sharing one meant the public installer silently ate
|
||||||
|
# the private tier's seven promptStringOnce answers.
|
||||||
|
is "private init carries its own -c" "1" \
|
||||||
|
"$(grep -c '\-c "\$PRIV_CFG"' $D)"
|
||||||
|
is "PRIV_CFG is not the default config path" "0" \
|
||||||
|
"$(grep -c 'PRIV_CFG=.*chezmoi/chezmoi.toml' $D)"
|
||||||
|
is "the private source is locked down after clone" "1" \
|
||||||
|
"$(grep -c 'chmod -R go-rwx "\$PRIV_SRC"' $D)"
|
||||||
|
|
||||||
printf '\n\033[1mrisk model — the invariants that matter\033[0m\n'
|
printf '\n\033[1mrisk model — the invariants that matter\033[0m\n'
|
||||||
reset
|
reset
|
||||||
|
|||||||
@@ -375,6 +375,10 @@ find_tool() {
|
|||||||
# empty, so a channel with nothing in it prints no header.
|
# empty, so a channel with nothing in it prints no header.
|
||||||
norm() { printf '%s\n' "$*" | tr ' ' '\n' | grep . | tr '\n' ' ' | sed 's/ $//'; }
|
norm() { printf '%s\n' "$*" | tr ' ' '\n' | grep . | tr '\n' ' ' | sed 's/ $//'; }
|
||||||
|
|
||||||
|
# Strip userinfo from a URL for tracing: https://user:token@host/p -> https://host/p
|
||||||
|
# Used wherever a URL that may carry a credential is about to be printed.
|
||||||
|
redact_url() { printf '%s\n' "$1" | sed 's#://[^/@]*@#://<redacted>@#'; }
|
||||||
|
|
||||||
run() {
|
run() {
|
||||||
if [ "$DRYRUN" -eq 1 ]; then printf ' + %s\n' "$*"; return 0; fi
|
if [ "$DRYRUN" -eq 1 ]; then printf ' + %s\n' "$*"; return 0; fi
|
||||||
printf '%s + %s%s\n' "$DIM" "$*" "$R" >&2
|
printf '%s + %s%s\n' "$DIM" "$*" "$R" >&2
|
||||||
@@ -570,6 +574,59 @@ install_bespoke() {
|
|||||||
run_sh "${SUDO:+$SUDO }rm -rf /usr/local/go && ${SUDO:+$SUDO }tar -xzf /tmp/go.tgz -C /usr/local" \
|
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; }
|
|| { note_fail "$key" "tarball extract failed"; continue; }
|
||||||
run_sh "rm -f /tmp/go.tgz" ;;
|
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)
|
core/chezmoi)
|
||||||
# Circular by nature: dotup arrives *via* chezmoi. Present already
|
# Circular by nature: dotup arrives *via* chezmoi. Present already
|
||||||
# in every case that matters; here for the one where it is not.
|
# in every case that matters; here for the one where it is not.
|
||||||
@@ -658,6 +715,22 @@ cmd_install() {
|
|||||||
PRIV_SRC=${DOTUP_PRIVATE_SRC:-${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles-private}
|
PRIV_SRC=${DOTUP_PRIVATE_SRC:-${XDG_DATA_HOME:-$HOME/.local/share}/dotfiles-private}
|
||||||
BWS_TOKEN=${DOTUP_BWS_TOKEN:-${XDG_CONFIG_HOME:-$HOME/.config}/bitwarden/bws-token}
|
BWS_TOKEN=${DOTUP_BWS_TOKEN:-${XDG_CONFIG_HOME:-$HOME/.config}/bitwarden/bws-token}
|
||||||
|
|
||||||
|
# The private tier gets its OWN config file, and this is load-bearing.
|
||||||
|
#
|
||||||
|
# chezmoi renders .chezmoi.toml.tmpl to the config path, and without -c that is
|
||||||
|
# ~/.config/chezmoi/chezmoi.toml for BOTH tiers. The private template asks seven
|
||||||
|
# [data] questions once (promptStringOnce) -- name, email, signing key, four
|
||||||
|
# gitea addresses. Re-running the PUBLIC installer afterwards rewrites that same
|
||||||
|
# file, and since the public tier's rendered config carries no [data] block, the
|
||||||
|
# seven answers are simply gone.
|
||||||
|
#
|
||||||
|
# The failure is silent, which is what makes it worth a separate file rather
|
||||||
|
# than a warning: the private templates degrade politely when their data is
|
||||||
|
# missing -- config.local emits a comment telling you to re-run init instead of
|
||||||
|
# failing the apply -- so the first symptom is `git commit` not knowing who you
|
||||||
|
# are, days later, with nothing connecting it to the install you ran.
|
||||||
|
PRIV_CFG=${DOTUP_PRIVATE_CFG:-${XDG_CONFIG_HOME:-$HOME/.config}/chezmoi/private.toml}
|
||||||
|
|
||||||
cmd_private() {
|
cmd_private() {
|
||||||
rows=$(selected_private)
|
rows=$(selected_private)
|
||||||
[ -n "$rows" ] || return 0
|
[ -n "$rows" ] || return 0
|
||||||
@@ -720,8 +793,28 @@ cmd_private() {
|
|||||||
|
|
||||||
if [ "$want_repo" -eq 1 ]; then
|
if [ "$want_repo" -eq 1 ]; then
|
||||||
if [ -n "$p_repo" ]; then
|
if [ -n "$p_repo" ]; then
|
||||||
run chezmoi init --apply --source "$PRIV_SRC" "$p_repo" \
|
# NOT `run`. It echoes its whole argv to stderr, and this argv ends
|
||||||
|
# in https://user:TOKEN@host/... -- which would put a live git
|
||||||
|
# credential into terminal scrollback, any `dotup 2>log`, and any
|
||||||
|
# agent or CI transcript capturing the run. Trace a redacted form
|
||||||
|
# and execute quietly.
|
||||||
|
mkdir -p "$(dirname "$PRIV_CFG")"
|
||||||
|
if [ "$DRYRUN" -eq 1 ]; then
|
||||||
|
printf ' + chezmoi init --apply --source %s -c %s %s\n' \
|
||||||
|
"$PRIV_SRC" "$PRIV_CFG" "$(redact_url "$p_repo")"
|
||||||
|
else
|
||||||
|
printf '%s + chezmoi init --apply --source %s -c %s %s%s\n' \
|
||||||
|
"$DIM" "$PRIV_SRC" "$PRIV_CFG" "$(redact_url "$p_repo")" "$R" >&2
|
||||||
|
chezmoi init --apply --source "$PRIV_SRC" -c "$PRIV_CFG" "$p_repo" \
|
||||||
|| err "private repo init failed"
|
|| err "private repo init failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The clone lands with the caller's umask, which on a stock Ubuntu
|
||||||
|
# is 022 -- world-readable. This tree holds ssh config, accepted
|
||||||
|
# keys and machine identity, and its .git/config stores the
|
||||||
|
# credential-bearing remote URL in plain text. On a shared or
|
||||||
|
# multi-user box that is readable by anyone with an account.
|
||||||
|
[ ! -d "$PRIV_SRC" ] || chmod -R go-rwx "$PRIV_SRC" 2>/dev/null || :
|
||||||
else err "the blob carried no PRIVATE_REPO_URL"; fi
|
else err "the blob carried no PRIVATE_REPO_URL"; fi
|
||||||
fi
|
fi
|
||||||
unset p_repo
|
unset p_repo
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
@needs agents/specify-cli core/uv
|
@needs agents/specify-cli core/uv
|
||||||
@needs core/mermaid-cli core/node
|
@needs core/mermaid-cli core/node
|
||||||
@needs core/neovim core/imagemagick core/mermaid-cli
|
@needs core/neovim core/imagemagick core/mermaid-cli
|
||||||
|
@needs private/bws-secrets core/unzip
|
||||||
#
|
#
|
||||||
# npm names carry scopes that the plugin's short name does not. `npm i -g
|
# npm names carry scopes that the plugin's short name does not. `npm i -g
|
||||||
# rpiv-btw` installs somebody else's package.
|
# rpiv-btw` installs somebody else's package.
|
||||||
@@ -63,6 +64,7 @@ core mermaid-cli safe -npm -npm mmdc — nvim renders mermaid fences with it
|
|||||||
core btop safe btop btop
|
core btop safe btop btop
|
||||||
core htop safe htop htop
|
core htop safe htop htop
|
||||||
core ncdu safe ncdu ncdu
|
core ncdu safe ncdu ncdu
|
||||||
|
core unzip safe unzip unzip the bws release ships as a .zip, and 24.04 minimal has no unzip
|
||||||
core tree safe tree tree
|
core tree safe tree tree
|
||||||
core cmake safe cmake cmake
|
core cmake safe cmake cmake
|
||||||
core ninja safe ninja-build ninja package name differs from binary
|
core ninja safe ninja-build ninja package name differs from binary
|
||||||
@@ -116,4 +118,4 @@ virt libvirt invasive libvirt-daemon-system - daemon + group membership
|
|||||||
virt virt-manager invasive virt-manager -
|
virt virt-manager invasive virt-manager -
|
||||||
@private one password, typed after the install finishes
|
@private one password, typed after the install finishes
|
||||||
private private-repo private - - ~/.local/share/dotfiles-private — agent config, ssh config
|
private private-repo private - - ~/.local/share/dotfiles-private — agent config, ssh config
|
||||||
private bws-secrets private - - 7 API keys into ~/.config/zsh/secrets.zsh
|
private bws-secrets private -tarball -tarball installs bws, then 7 API keys into ~/.config/zsh/secrets.zsh
|
||||||
|
|||||||
|
Can't render this file because it contains an unexpected character in line 17 and column 49.
|
Reference in New Issue
Block a user