This host has no `bao` CLI, only Docker, so every authenticated command is a
`docker compose exec` -- and every obvious way to get a token in there leaks it.
`-e BAO_TOKEN=<value>` puts it in the docker process's argv, which
/proc/<pid>/cmdline exposes to every user on the box. An inline
`BAO_TOKEN=<value> cmd` adds shell history on top.
The stdin trick 0eb7f26 uses cannot be the general answer, and the reason is
capability rather than ergonomics: bao's kvbuilder consumes stdin exactly once,
and the operator needs it for `policy write NAME -`, `write PATH -` (a JSON
body) and `key=-` (a single secret value). Spend stdin on the token and an OIDC
client secret has nowhere left to go but argv, reintroducing the leak that was
just closed. update.sh keeps piping because it runs one unattended command that
needs no stdin of its own; interactive work needs something else.
So baoctl is a SESSION wrapper. `baoctl login` prompts once with echo off --
verified against v2.6.2 that bao reads it through termios and requires a TTY --
and afterwards commands are typed verbatim with stdin free.
Three details that are load-bearing, all source-verified at v2.6.2:
- `bao login` prints the token in its success table. Without -no-print the
interactive path dumps the root token into the exec session's scrollback,
which is worse than what it replaces.
- The token you type is not what the session keeps. baoctl immediately mints a
short-lived child and swaps it in via `bao token create -field=token |
bao login -no-print -`, so the value never reaches an argv or a stdout, the
session expires on its own, and logout can revoke it without killing the root
token. A failed mint discards the login rather than leaving the typed token
sitting in the session.
- logout both revokes AND removes the file. `bao token revoke -self` does not
delete it and there is no `bao logout` in 2.6.x, so revoking alone leaves a
stale file that fails with permission errors instead of "not logged in".
The session lives at /dev/shm/.bao-session in the container, pointed at by
BAO_TOKEN_PATH (new in 2.6.0). /dev/shm is already a per-container tmpfs, so the
token never touches disk and dies with the container -- and arranging that
needed no compose change, which matters because recreating this container means
a seal cycle and three unseal keys typed by a human.
It verifies TLS instead of reaching for -tls-skip-verify: ./tls is already
mounted read-only into the container and the generated cert carries
IP:127.0.0.1 in its SANs, so BAO_CACERT validates against the real listener.
-tls-skip-verify would have been the lazy default and is strictly worse.
Also warns when BAO_TOKEN is set in the caller's shell: baoctl never forwards
it, but an operator who set one will assume it is in play and debug the wrong
credential.
Caught while testing: `--ttl` with no value exited SILENTLY, because `shift 2`
with one argument left fails and set -e takes the script down before the
validation ran. Now the argument count is checked first -- another member of
this repo's set -e trap family.
Verified without a live host: help works with no stack present and touches no
docker; the missing-stack path errors cleanly; all four option-validation paths
report rather than exiting silently; the payload carries baoctl.
Not verified: login, the mint-and-swap and logout against a running vault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>