From 3885172a0ede8c17f274639639c97fddefb14a22 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 17:11:09 -0400 Subject: [PATCH 1/2] =?UTF-8?q?fix(b-0852.3b-supersede):=20narrow=20passph?= =?UTF-8?q?rase=20env-exposure=20window=20=E2=80=94=20non-exported=20shell?= =?UTF-8?q?=20var=20ZETA=5FCREDS=5FPASSPHRASE=5FVAL=20+=20inline-set=20for?= =?UTF-8?q?=20sudo=20only=20+=20unconditional=20unset=20(supersedes=20#563?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses both Copilot findings on #5638: Finding 1 (P1 — wider env-exposure window than necessary): PR #5638's Step 6.56 exported ZETA_CREDS_PASSPHRASE into the installer process env at line ~574 and left it there for ~750 lines until the Step 6.95 picker invocation. During that window the passphrase was readable via /proc//environ to any process that could read it (root + same-UID processes). Fix: capture the passphrase into a NON-EXPORTED shell variable ZETA_CREDS_PASSPHRASE_VAL at Step 6.56. Bash shell variables (no 'export' keyword) live in the shell's own variable table but are NOT copied into /proc//environ. At Step 6.95 invocation use inline env-set syntax: `ZETA_CREDS_PASSPHRASE="$ZETA_CREDS_PASSPHRASE_VAL" sudo --preserve-env=...` which sets the env var ONLY in the sudo subprocess (visible to the picker via --passphrase-env reference) without touching the parent installer shell's env. Finding 2 (P1 — unset only in picker-ran branch): PR #5638's `unset ZETA_CREDS_PASSPHRASE` happened only inside the picker-ran branch of the if/else. If the operator entered a passphrase but the picker was skipped (ZETA_CREDS_PICKER=0, /etc/zeta/no-picker present, /etc/zeta/usb-uuid missing), the exported passphrase remained live in the installer env until process exit. Fix: restructure — move the `unset ZETA_CREDS_PASSPHRASE_VAL` OUTSIDE the if/else block so it fires UNCONDITIONALLY after the picker block, in BOTH the picker-ran AND picker-skipped branches. The shell-var-not-env-var distinction from Finding 1's fix means even before this unset the passphrase wasn't in /proc/.../environ, but `unset` still scrubs it from the shell's own variable table — matters for any later `set` / `declare -p` invocation. Substrate-honest: - Both findings are CORRECT — the supersede addresses both - Validation: bash -n OK; docker harness 22s SUCCESS - Audit confirmed via grep: no 'export ZETA_CREDS_PASSPHRASE' or 'export ZETA_CREDS_PASSPHRASE_VAL' anywhere in zeta-install.sh - The constitutional rail at line 452 still holds verbatim: 'secrets shouldn't transit non-operator surfaces; operator-typed at install time is the safest path' Closes #5638 with both findings addressed from the start. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../usb-nixos-installer/zeta-install.sh | 99 +++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/full-ai-cluster/usb-nixos-installer/zeta-install.sh b/full-ai-cluster/usb-nixos-installer/zeta-install.sh index b18ebc4006..7882b17547 100755 --- a/full-ai-cluster/usb-nixos-installer/zeta-install.sh +++ b/full-ai-cluster/usb-nixos-installer/zeta-install.sh @@ -568,6 +568,74 @@ else fi echo +# ── Step 6.56: B-0852.3b cred-blob passphrase prompt ──────────── +# +# Supersedes PR #5638 with both Copilot P1 findings addressed from +# the start: +# - line 574 / passphrase exposure window: passphrase held in +# NON-EXPORTED shell variable so it never appears in +# /proc//environ; inline-set for the single sudo invocation +# at Step 6.95-picker only +# - line 1321 / unset only in picker-ran branch: shell var +# unconditionally unset after Step 6.95 picker block regardless +# of which branch ran +# +# Operator pain point 2026-05-27: "i'm witing on the tool to be +# resable so i don't have to enter credentals over and over +# everytime." +# +# Closes precondition #2 of 3 for the cred-persistence picker at +# Step 6.95-picker (precondition #1 = ZETA_CREDS_PICKER default-on +# via PR #5639; precondition #3 = /etc/zeta/usb-uuid auto-captured +# at iter-4.2 via PR #5637; this step closes #2). +# +# Same operator-typed-once-on-console pattern as iter-5.3 password +# (constitutional rail per zeta-install.sh line 452 verbatim: +# "secrets shouldn't transit non-operator surfaces; operator-typed +# at install time is the safest path"). +echo +echo "[B-0852.3b] ── cred-blob passphrase prompt (B-0852 Phase 1) ──" +echo "[B-0852.3b] Set a passphrase to encrypt your credentials onto" +echo "[B-0852.3b] this USB. Future boots can RESTORE creds via the" +echo "[B-0852.3b] same passphrase (no more re-entering gh login etc." +echo "[B-0852.3b] on every reboot). Encryption: AES-256-GCM with key" +echo "[B-0852.3b] derived via scrypt -> HKDF chain bound to this USB's" +echo "[B-0852.3b] UUID (per tools/installer/zeta-creds-crypto.ts)." +echo "[B-0852.3b]" +echo "[B-0852.3b] Press Enter to SKIP (no cred-blob persistence;" +echo "[B-0852.3b] keeps current per-reboot re-entry behavior)." +echo +ZETA_CREDS_PASSPHRASE_INPUT="" +ZETA_CREDS_PASSPHRASE_CONFIRM="" +# -s = silent (hidden); -p = inline prompt +read -r -s -p "[B-0852.3b] Passphrase (or Enter to skip): " ZETA_CREDS_PASSPHRASE_INPUT +echo +if [ -n "$ZETA_CREDS_PASSPHRASE_INPUT" ]; then + read -r -s -p "[B-0852.3b] Confirm: " ZETA_CREDS_PASSPHRASE_CONFIRM + echo + if [ "$ZETA_CREDS_PASSPHRASE_INPUT" != "$ZETA_CREDS_PASSPHRASE_CONFIRM" ]; then + echo "[B-0852.3b] WARN: passphrases don't match; skipping (no cred-blob persistence)" + ZETA_CREDS_PASSPHRASE_INPUT="" + fi +fi +unset ZETA_CREDS_PASSPHRASE_CONFIRM +# Initialize ZETA_CREDS_PASSPHRASE_VAL to empty unconditionally so the +# Step 6.95-picker gate check works whether or not operator entered a +# passphrase. Per Copilot P1 finding on PR #5638: do NOT export — keep +# in a non-exported shell variable to avoid /proc//environ exposure. +ZETA_CREDS_PASSPHRASE_VAL="" +if [ -n "$ZETA_CREDS_PASSPHRASE_INPUT" ]; then + ZETA_CREDS_PASSPHRASE_VAL="$ZETA_CREDS_PASSPHRASE_INPUT" + unset ZETA_CREDS_PASSPHRASE_INPUT + echo "[B-0852.3b] passphrase captured + held in non-exported shell variable" + echo "[B-0852.3b] (NOT in /proc/self/environ; inline-set for sudo only at 6.95;" + echo "[B-0852.3b] shell var unset in ALL branches after Step 6.95 picker block)" +else + unset ZETA_CREDS_PASSPHRASE_INPUT + echo "[B-0852.3b] skipped — no cred-blob persistence this install" +fi +echo + # ── Step 6.6: iter-5.2 hostname injection (B-0792) ────────────── # # Per the maintainer 2026-05-26: "since our different roles are @@ -1311,9 +1379,9 @@ if [ -d "$ZETA_HOME" ]; then elif [ ! -f /etc/zeta/usb-uuid ]; then PICKER_OPT_OUT=1 PICKER_SKIP_REASON="/etc/zeta/usb-uuid missing (B-0852.3a-prep did not capture UUID)" - elif [ -z "${ZETA_CREDS_PASSPHRASE:-}" ]; then + elif [ -z "${ZETA_CREDS_PASSPHRASE_VAL:-}" ]; then PICKER_OPT_OUT=1 - PICKER_SKIP_REASON="ZETA_CREDS_PASSPHRASE empty (operator skipped passphrase at Step 6.56)" + PICKER_SKIP_REASON="ZETA_CREDS_PASSPHRASE_VAL empty (operator skipped passphrase at Step 6.56)" fi if [ "$PICKER_OPT_OUT" = "0" ]; then USB_UUID="$(cat /etc/zeta/usb-uuid)" @@ -1324,17 +1392,34 @@ if [ -d "$ZETA_HOME" ]; then # patterns at lines 1119-1141; without it, bun is not on the PATH the # subshell sees (mise installs bun via shims; activate sets PATH). # BUN_INSTALL pin matches sibling pattern too. - sudo --preserve-env=ZETA_CREDS_PASSPHRASE -u "#$ZETA_UID" \ + # + # B-0852.3b-supersede (fix Copilot PR #5638 finding 1): inline-env-set + # `ZETA_CREDS_PASSPHRASE="$ZETA_CREDS_PASSPHRASE_VAL"` exports the var + # into the sudo invocation's process env ONLY (not the parent installer + # shell). Combined with `--preserve-env=ZETA_CREDS_PASSPHRASE` this lets + # the picker bash -c subshell read it via --passphrase-env. The parent + # installer shell never has ZETA_CREDS_PASSPHRASE exported, so it never + # appears in /proc//environ. + ZETA_CREDS_PASSPHRASE="$ZETA_CREDS_PASSPHRASE_VAL" sudo --preserve-env=ZETA_CREDS_PASSPHRASE -u "#$ZETA_UID" \ HOME="$ZETA_HOME" BUN_INSTALL="$ZETA_HOME/.bun" \ bash -c "set -o pipefail; eval \"\$(mise activate bash 2>/dev/null || true)\"; cd '$ZETA_HOME/Zeta' && bun tools/installer/zeta-creds-picker.ts --usb-uuid '$USB_UUID' --output /esp/zeta-creds.enc --passphrase-env ZETA_CREDS_PASSPHRASE" || \ echo "[iter-5.5.0] WARN: picker exited non-zero; cred-blob may be partial" - # B-0852.3b discipline: unset passphrase from installer-script env - # IMMEDIATELY after picker completes to minimize env-exposure window. - unset ZETA_CREDS_PASSPHRASE - echo "[iter-5.5.0] ZETA_CREDS_PASSPHRASE unset from installer env (post-picker)" else echo "[iter-5.5.0] SKIP 6.95-picker: $PICKER_SKIP_REASON" fi + # B-0852.3b-supersede (fix Copilot PR #5638 finding 2): unset + # ZETA_CREDS_PASSPHRASE_VAL UNCONDITIONALLY after the picker block — + # fires in BOTH the picker-ran branch AND the picker-skipped branch. + # Prior code only unset inside the picker-ran branch, leaving the + # passphrase live in the installer shell for the rest of execution + # whenever ZETA_CREDS_PICKER=0 / /etc/zeta/no-picker / usb-uuid-missing + # path was taken. Note: this is a non-exported shell var (no `export` + # in Step 6.56), so even before this unset it was NOT visible via + # /proc//environ — but `unset` still scrubs it from the + # shell's own variable table, which matters for any later `set` / + # `declare -p` invocation that might log shell state. + unset ZETA_CREDS_PASSPHRASE_VAL + echo "[iter-5.5.0] ZETA_CREDS_PASSPHRASE_VAL unset from installer shell (post-picker block; fires in both branches)" # 6.95b — interactive claude login (mirror iter-5.4.0 gh auth login) CLAUDE_BIN="$ZETA_HOME/.bun/bin/claude" From e9ffa9b9001c71d70e57c39939df47457a8339f1 Mon Sep 17 00:00:00 2001 From: Lior Date: Wed, 27 May 2026 17:21:03 -0400 Subject: [PATCH 2/2] fixup(b-0852.3b-supersede): replace hard-coded line numbers with step-label refs + align picker doc to ZETA_CREDS_PASSPHRASE_VAL (Copilot threads on #5643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 — hard-coded line numbers drift Prior commit's Step 6.56 doc block referenced "line 574 / passphrase exposure window" and "line 1321 / unset only in picker-ran branch". Those line refs will drift as the script evolves. Repo convention: reference step labels (6.56, 6.95-picker) and/or describe issues semantically. Fix: rewrite Step 6.56 doc as semantic two-step-lifecycle description (Step 6.56 → Step 6.95-picker → Step 6.95 post-picker). Names what each step does + why; no line numbers. P2 — picker doc + SECURITY comment mismatched on var name Step 6.95-picker doc still referenced "ZETA_CREDS_PASSPHRASE (PR #5638 closes this via Step 6.56 prompt)" as the precondition, but the actual gate keys on ZETA_CREDS_PASSPHRASE_VAL after the supersede. SECURITY comment also referenced only the env-var side of the discipline. Fix: align doc to reference ZETA_CREDS_PASSPHRASE_VAL + expand SECURITY block to name the full 2-stage discipline (non-exported shell var in parent, inline-set into sudo subprocess only). Validation: bash -n OK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../usb-nixos-installer/zeta-install.sh | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/full-ai-cluster/usb-nixos-installer/zeta-install.sh b/full-ai-cluster/usb-nixos-installer/zeta-install.sh index 7882b17547..00a6e033ff 100755 --- a/full-ai-cluster/usb-nixos-installer/zeta-install.sh +++ b/full-ai-cluster/usb-nixos-installer/zeta-install.sh @@ -570,15 +570,26 @@ echo # ── Step 6.56: B-0852.3b cred-blob passphrase prompt ──────────── # -# Supersedes PR #5638 with both Copilot P1 findings addressed from -# the start: -# - line 574 / passphrase exposure window: passphrase held in -# NON-EXPORTED shell variable so it never appears in -# /proc//environ; inline-set for the single sudo invocation -# at Step 6.95-picker only -# - line 1321 / unset only in picker-ran branch: shell var -# unconditionally unset after Step 6.95 picker block regardless -# of which branch ran +# Two-step lifecycle for the operator-entered passphrase, designed +# to minimize /proc//environ exposure window: +# +# - Step 6.56 (here): captured into the NON-EXPORTED shell +# variable ZETA_CREDS_PASSPHRASE_VAL. Bash shell variables +# without `export` live in the shell's own variable table but +# are NOT copied into /proc//environ for child processes +# to read. +# +# - Step 6.95-picker: inline-set +# `ZETA_CREDS_PASSPHRASE="$ZETA_CREDS_PASSPHRASE_VAL" sudo +# --preserve-env=ZETA_CREDS_PASSPHRASE ...` exports the env +# var into the sudo subprocess ONLY (where the picker bash -c +# reads it via --passphrase-env). Parent installer shell never +# has ZETA_CREDS_PASSPHRASE exported. +# +# - Step 6.95 post-picker: ZETA_CREDS_PASSPHRASE_VAL `unset` +# unconditionally after the if/else block so it fires whether +# the picker actually ran OR was skipped (env opt-out / file +# marker / missing UUID). # # Operator pain point 2026-05-27: "i'm witing on the tool to be # resable so i don't have to enter credentals over and over @@ -1348,9 +1359,10 @@ if [ -d "$ZETA_HOME" ]; then # deferred subset. # # Default behavior (B-0852.3c flip, 2026-05-27): AUTO-ENABLE when - # both /etc/zeta/usb-uuid (PR #5637 closes this) and - # ZETA_CREDS_PASSPHRASE (PR #5638 closes this via Step 6.56 prompt) - # are present. Explicit opt-out via ZETA_CREDS_PICKER=0 (env or + # both /etc/zeta/usb-uuid (PR #5637 closes this) and the + # ZETA_CREDS_PASSPHRASE_VAL shell variable (populated by Step 6.56 + # prompt; held non-exported per B-0852.3b-supersede discipline) are + # present. Explicit opt-out via ZETA_CREDS_PICKER=0 (env or # /etc/zeta/no-picker marker file). # # Rationale: with all 3 preconditions auto-populated by the install @@ -1365,10 +1377,14 @@ if [ -d "$ZETA_HOME" ]; then # 2. /etc/zeta/no-picker marker file present # 3. Operator entered empty passphrase at Step 6.56 (no PASSPHRASE) # - # SECURITY (Copilot review on PR #5450): the passphrase is FORWARDED VIA SUDO - # --preserve-env=ZETA_CREDS_PASSPHRASE, NOT inlined in bash -c arg-string (the - # latter leaked the literal passphrase into the process arglist visible to ps). - # The picker reads it via --passphrase-env which references the env-var-NAME only. + # SECURITY: the passphrase is FORWARDED VIA SUDO --preserve-env=ZETA_CREDS_PASSPHRASE, + # NOT inlined in bash -c arg-string (the latter would leak the literal passphrase + # into the process arglist visible to ps). The picker reads it via --passphrase-env + # which references the env-var-NAME only. The env var name ZETA_CREDS_PASSPHRASE + # is set INLINE-IN-SUDO-INVOCATION (`ZETA_CREDS_PASSPHRASE="$ZETA_CREDS_PASSPHRASE_VAL" + # sudo --preserve-env=ZETA_CREDS_PASSPHRASE ...`) so it lives in the sudo + # subprocess env only; the parent installer shell holds the secret in the + # NON-EXPORTED shell var ZETA_CREDS_PASSPHRASE_VAL, never exported anywhere. PICKER_OPT_OUT=0 if [ "${ZETA_CREDS_PICKER:-1}" = "0" ]; then PICKER_OPT_OUT=1