Installer step 4b: vault first, then set, then a PVE login (R-1145); host key off curl's argv (review F4); harness green and a gate (R-1146); hub 0.158.0 deploy evidence; independent review + verdicts
gates / gates (push) Successful in 7m23s
gates / gates (push) Successful in 7m23s
Live on demo-hp (--rotate-recovery-only): vault time moved, new value 200, old 401. Harness 52/0; the R1145 cases fail against HEAD. Publishes only with the next installer tag.
This commit is contained in:
1 parent
4c319ddeb9
commit
88aecf55b6
17 files changed
+374
-27
No files matched your search
@@ -1628,6 +1628,11 @@ run_rescope_acl() {
|
||||
# --rotate-recovery (a re-set would strand the operator's saved copy).
|
||||
# Defined HERE, ahead of the argument parse (R-1141): --rotate-recovery-only is dispatched before the
|
||||
# install's mode validation and calls it. Moved unchanged; the install still calls it from Main.
|
||||
# _root_shadow_fingerprint — sha256 of root's /etc/shadow line (never the line itself): did the password change?
|
||||
# FELHOM_SHADOW_FILE is a test-harness-only override (hostinstall-mode-harness.sh R1145-*).
|
||||
_root_shadow_fingerprint() {
|
||||
grep '^root:' "${FELHOM_SHADOW_FILE:-/etc/shadow}" 2>/dev/null | sha256sum | cut -c1-64
|
||||
}
|
||||
step_break_glass() {
|
||||
log_step "4b/8 break-glass credential (root@pam console password → hub vault)"
|
||||
if [[ -z "${HOST_ID:-}" || -z "${HOST_API_KEY:-}" ]]; then
|
||||
@@ -1635,38 +1640,74 @@ step_break_glass() {
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; chpasswd ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key)"
|
||||
log_dry "openssl rand → strong root@pam password (never logged) ; PUT $HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential (Bearer host key) FIRST ; then chpasswd ; then a PVE login check (R-1145)"
|
||||
_state_mark break_glass; return 0
|
||||
fi
|
||||
if _state_has break_glass && ! $ROTATE_RECOVERY; then
|
||||
log_skip " recovery credential already vaulted (use --rotate-recovery to regenerate)"
|
||||
return 0
|
||||
fi
|
||||
# R-1145 (2026-10-18): VAULT FIRST, THEN SET. Until 1.33.0 the password was set before the vault; a vault that
|
||||
# then failed left root@pam with a password nobody holds — and the console is the last door into a broken box.
|
||||
# Now: generate → PUT to the hub (200 = stored: the hub answers only after its DB write) → chpasswd (one retry)
|
||||
# → a PVE API login with the new value. The possible end states, each logged in words, never the value:
|
||||
# vault refused → nothing changed (root keeps its password, the hub its old copy) — die
|
||||
# vault ok, set ok → root and the hub agree — then the PVE login is checked
|
||||
# vault ok, set failed ×2 → the hub holds a password root does NOT have; root keeps its previous one (checked:
|
||||
# its shadow line did not change). A host key cannot read the vault back (by design),
|
||||
# so the previous value cannot be re-vaulted — die, and the next run fixes it.
|
||||
# set ok, PVE login fails → root and the hub agree on the shell; the web console refuses — die, say so.
|
||||
# Pinned by hostinstall-mode-harness.sh R1145-a..d (the step run with fake commands, call order recorded).
|
||||
# Strong password: 24 url-safe bytes (~144 bits). Kept ONLY in a local shell var, never on disk.
|
||||
local newpw
|
||||
local newpw before after code attempt set_ok=false
|
||||
newpw=$(openssl rand -base64 24 2>/dev/null | tr -d '\n' | tr '+/' '-_')
|
||||
[[ ${#newpw} -ge 24 ]] || die "failed to generate a strong recovery password"
|
||||
# Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log.
|
||||
if ! printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then
|
||||
newpw="" # scrub
|
||||
die "chpasswd failed to set the root@pam recovery password"
|
||||
fi
|
||||
# Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-).
|
||||
local code
|
||||
before=$(_root_shadow_fingerprint)
|
||||
# 1. Vault to the hub over the host-key-authenticated channel; password only on stdin (-d @-). The host key rides
|
||||
# a 0600 header FILE (`-H @file`, written by the printf builtin), never curl's argv — `ps` showed it there
|
||||
# (independent review 2026-10-18, F4).
|
||||
local hdr
|
||||
hdr=$(mktemp) || die "cannot create a temp file for the vault request; the root@pam password was NOT changed"
|
||||
chmod 600 "$hdr"
|
||||
printf 'Authorization: Bearer %s\n' "$HOST_API_KEY" > "$hdr"
|
||||
code=$(printf '{"username":"root@pam","password":"%s"}' "$newpw" \
|
||||
| curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
||||
"$HUB_URL/api/v1/hosts/$HOST_ID/recovery-credential" \
|
||||
-H "Authorization: Bearer $HOST_API_KEY" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var the moment it is vaulted
|
||||
-H @"$hdr" -H 'Content-Type: application/json' -d @- 2>/dev/null)
|
||||
rm -f "$hdr"
|
||||
case "$code" in
|
||||
200) log_success " root@pam password set + vaulted to the hub (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)." ;;
|
||||
401|403) die "recovery-credential vault rejected ($code) — host key/authorization problem" ;;
|
||||
*) die "recovery-credential vault failed (HTTP $code)" ;;
|
||||
200) log_info " recovery password vaulted at the hub (stored) — now setting it on root@pam" ;;
|
||||
401|403) newpw=""; die "recovery-credential vault rejected ($code) — host key/authorization problem; the root@pam password was NOT changed" ;;
|
||||
*) newpw=""; die "recovery-credential vault failed (HTTP ${code:-000}); the root@pam password was NOT changed" ;;
|
||||
esac
|
||||
# 2. Set root@pam (= the Linux root user on PVE) via chpasswd on STDIN — no argv, no log. One retry.
|
||||
for attempt in 1 2; do
|
||||
if printf 'root:%s\n' "$newpw" | chpasswd 2>/dev/null; then set_ok=true; break; fi
|
||||
[[ $attempt -eq 1 ]] && { log_warn " chpasswd failed — retrying once"; sleep 2; }
|
||||
done
|
||||
after=$(_root_shadow_fingerprint)
|
||||
if ! $set_ok && [[ -n "$after" && "$after" != "$before" ]]; then
|
||||
set_ok=true # chpasswd reported failure but root's shadow line changed: the password IS the new one
|
||||
fi
|
||||
if ! $set_ok; then
|
||||
newpw=""
|
||||
log_error " STATE: the hub now holds a recovery password that root@pam does NOT have; root@pam keeps its previous password (its shadow line is unchanged)."
|
||||
die "chpasswd failed twice after the vault — re-run (--rotate-recovery-only on an installed box): it vaults a new password first again"
|
||||
fi
|
||||
# 3. The proof a person needs: the PVE web login takes the new value (password on stdin, never argv).
|
||||
code=$(printf '%s' "$newpw" | curl -sk -o /dev/null -w '%{http_code}' --max-time 20 \
|
||||
--data-urlencode 'username=root@pam' --data-urlencode 'password@-' \
|
||||
"https://127.0.0.1:8006/api2/json/access/ticket" 2>/dev/null)
|
||||
newpw="" # scrub the plaintext from the shell var
|
||||
if [[ "$code" != "200" ]]; then
|
||||
log_error " STATE: root@pam and the hub hold the same NEW password, but the PVE web login answered HTTP ${code:-000}."
|
||||
die "the PVE login with the new recovery password failed (HTTP ${code:-000}) — check pveproxy (:8006); the shell login uses the new password"
|
||||
fi
|
||||
log_success " root@pam password vaulted, set, and the PVE login takes it (retrieve via the operator /admin path; never logged here)"
|
||||
# Drill F-8: say this OUT LOUD — the operator was live-surprised by a 401 at the PVE
|
||||
# GUI. Pointer only, never the credential.
|
||||
log_warn " NOTE: the root@pam password just CHANGED — the old one now fails at the PVE web GUI (:8006)."
|
||||
log_warn " Retrieve the new one at hub → host page (vaulted recovery credential)."
|
||||
_state_mark break_glass
|
||||
}
|
||||
|
||||
@@ -2556,12 +2597,12 @@ align_apt_repos() {
|
||||
log_info " disabled $(basename "$f") (Enabled: no)"
|
||||
done
|
||||
# 1b. legacy .list enterprise line → commented
|
||||
for f in "$d/pve-enterprise.list"; do
|
||||
[[ -f "$f" ]] || continue
|
||||
grep -qE '^[[:space:]]*deb[[:space:]].*enterprise\.proxmox\.com' "$f" || continue
|
||||
# (R-1146: one file, so no loop — the old one-item `for` was shellcheck SC2066; same file, same effect)
|
||||
f="$d/pve-enterprise.list"
|
||||
if [[ -f "$f" ]] && grep -qE '^[[:space:]]*deb[[:space:]].*enterprise\.proxmox\.com' "$f"; then
|
||||
sed -i 's|^[[:space:]]*\(deb[[:space:]].*enterprise\.proxmox\.com.*\)$|# \1 # felhom: no-subscription|' "$f"; changed=1
|
||||
log_info " commented enterprise line in $(basename "$f")"
|
||||
done
|
||||
fi
|
||||
# 2. add pve-no-subscription if it is not already present in any list/sources
|
||||
if ! grep -rqsE 'download\.proxmox\.com/debian/pve([[:space:]/]|$)' "$d"/*.sources "$d"/*.list 2>/dev/null; then
|
||||
cat > "$d/pve-no-subscription.sources" <<SRC
|
||||
|
||||
Reference in new issue
Block a user