Features: the credential scrub aborted, and a missing report means more than one thing
Two updates to the memberCredentials entry, both from fleetd #388 and #394. #388: the scrub now runs from the generated `.zshenv` as well as `.zshrc` and `.zlogin`. `.zshenv` is the only file zsh reads unconditionally, so the control no longer depends on enumerating which kinds of shell exist. #394: a missing `scrub-report.txt` had been read as evidence about which shell ran. It is not. The report block is the last statement in the script, so its absence proves only that the script did not reach the end. The real cause was `export UID=`, a fatal zsh parameter error that terminates the whole sourced file, with the message swallowed by the loop's `2>/dev/null`. The severity was the selection, not the count: `env` lists inherited names first and a startup file's own exports last, so the loop blanked the harmless half and died immediately before the operator's own exports. Also added, because both cost real time: - do not infer shell kind from argv[0], and never from a probe run inside an agent — opencode forks a fresh `zsh -c` per command, so the probe answers about itself - plant a decoy when auditing this: allow-listed names come back blank whether or not the scrub ran, so their blankness proves nothing - `.zcompdump` present plus `scrub-report.txt` absent is only explained by "sourced, then aborted partway" Root cause found by the fleet01 lead; the inverted-selection wording is theirs.
+38
-2
@@ -1918,14 +1918,50 @@ That is weaker, so put members on a zsh account.
|
||||
`.zlogin`, and zsh reads `.zlogin` only for a *login* shell. herdr does not open the same kind of
|
||||
shell everywhere: a macOS pane runs `-zsh` (login), a Linux pane runs a plain `/usr/bin/zsh`
|
||||
(interactive, not login). So it protected the developer's Mac and would have protected nothing at all
|
||||
on Linux, with no error anywhere. The scrub now runs from both the generated `.zshrc` and `.zlogin`.
|
||||
If you port this to another terminal backend, check what kind of shell it opens before trusting it.
|
||||
on Linux, with no error anywhere. The scrub now runs from the generated `.zshrc`, `.zlogin` **and
|
||||
`.zshenv`** (#388) — `.zshenv` is the only file zsh reads unconditionally, so the control no longer
|
||||
depends on enumerating which kinds of shell exist. If you port this to another terminal backend,
|
||||
check what kind of shell it opens before trusting it.
|
||||
|
||||
**Do not infer the shell kind from `argv[0]`.** A bare `/usr/bin/zsh` proves *not login* and says
|
||||
nothing about interactive. Both this repo's javadoc and a host's `fleetd.yaml` once claimed
|
||||
"interactive but NOT login" on that evidence alone, and neither had measured it. Worse, a probe run
|
||||
from *inside* an agent measures the agent's own `zsh -c` child, not the pane — opencode forks a
|
||||
fresh non-interactive shell per command, so it reports truthfully about the wrong process. Read the
|
||||
pane shell's own `/proc/<pid>/environ`, or replicate the shape with `script -qec zsh /dev/null`.
|
||||
|
||||
**How to tell it actually ran.** Each pane writes a `scrub-report.txt`, and the daemon logs
|
||||
`allowed N of M environment variables` when that pane stops — the denominator is the point. A
|
||||
**missing** report is logged at WARN: the scrub then cannot be confirmed to have run at all, and a
|
||||
silently dead control is the failure this policy exists to remove.
|
||||
|
||||
**Gotcha — a missing report has more than one meaning, and reading it as one cost a day (#394).**
|
||||
The report block is the *last* statement in `scrub.zsh`, so its absence proves only that the script
|
||||
did not reach the end. It does **not** tell you which shell ran. On one host the missing report was
|
||||
read as evidence that the pane shell was neither login nor interactive; the real cause was that
|
||||
`export UID=` in zsh is a **fatal parameter error** which terminates the whole sourced file. The
|
||||
blanking loop is wrapped in `{ ... } 2>/dev/null`, so the message was swallowed too.
|
||||
|
||||
The severity was the *selection*, not the count. `env` lists inherited names first and a startup
|
||||
file's own exports last, so the loop blanked the harmless inherited half and died immediately
|
||||
before the operator's own exports — the credentials the policy exists to remove. Measured there:
|
||||
`UID` was name 42 of 57, and a `~/.zshrc` decoy at 58 survived on 8 of 8 spawns. A partial scrub
|
||||
got precisely the wrong half.
|
||||
|
||||
Fixed by routing each attempt through `eval "export ${n}=" 2>/dev/null`, which contains the error
|
||||
to one iteration, rather than by skipping the known-fatal names (`UID EUID GID EGID PPID LINENO`).
|
||||
A skip-list has to be complete forever; this is a security control, so it must not depend on an
|
||||
enumeration being right. The report's first line now reads `allowed N of M failed F`, unblankable
|
||||
names are listed with a `!` prefix, and the daemon WARNs naming them — so "could not blank this
|
||||
one" is now visible instead of being an abort you learn about from a missing file.
|
||||
|
||||
**Two things to plant when you audit this yourself.** First, a **decoy**: a non-secret variable the
|
||||
operator's chain exports that is not on the allow-list is the only thing that separates "scrub
|
||||
skipped" from "nothing was there to scrub" — allow-listed names come back blank either way, so
|
||||
their blankness proves nothing. Second, check for `.zcompdump` in the pane's `ZDOTDIR`: its
|
||||
presence proves an interactive zsh ran `compinit` there. A `.zcompdump` present *and*
|
||||
`scrub-report.txt` absent is only explained by "sourced, then aborted partway".
|
||||
|
||||
**Measured, not assumed.** A real login zsh started from a clean parent kept **15 of 78** names with
|
||||
zero profiles configured; an earlier prototype run kept 3 of 28. The test asserts *equality* between
|
||||
the survivors and `baseline ∩ derived allow-list`, not a spot check of a few blocked names.
|
||||
|
||||
Reference in New Issue
Block a user