host-modules: fix atomic_write_secret's leftover-tmp bug; convert swarm-bao's forwarder-oidc writer too

atomic_write_secret's cleanup trap used RETURN, which never fires when
set -e aborts the function mid-body (a failing cat/chmod/chown), so a
secret-bearing temp file was left behind instead of being removed.
The write now runs in a subshell with its own EXIT trap, invoked via a
named handler (so `local rc=$?` is a normal, shellcheck-visible
assignment) that only removes the temp file when the subshell's exit
status is nonzero — the subshell's trap table is private, so a calling
unit's own EXIT trap is untouched. Reproduced the leftover-tmp bug
against the prior commit, confirmed it's gone, and confirmed both the
success path and a caller's own EXIT trap still work as before.

swarm-bao.nix's swarm-bao-forwarder-oidc unit had the identical
write-then-chmod-on-live-path defect as the four sites already fixed
here (fetches an OIDC client secret from swarm-bao, printfs it to the
live host path, chowns/chmods after) and was missed by the original
sweep. Converted it to atomic_write_secret; content and final
owner/mode (root:root, 0400) are unchanged.

Refs #4723
This commit is contained in:
atlas 2026-09-26 15:42:30 +02:00 • committed by mara
commit 5466cec066
2 changed files with 27 additions and 10 deletions

View file

@ -26,12 +26,28 @@
atomic_write_secret() {
local mode="$1" owner="$2" target="$3" tmp
tmp="$(mktemp "$(dirname -- "$target")/.$(basename -- "$target").XXXXXX")"
trap 'rm -f "$tmp"' RETURN
cat > "$tmp"
chmod "$mode" "$tmp"
if [ -n "$owner" ]; then
chown "$owner" "$tmp"
fi
# The write happens in a subshell so its own EXIT trap cleans up `$tmp`
# on failure (a `RETURN` trap does not fire when `set -e` aborts the
# function mid-body) without touching an EXIT trap the calling script's
# own `script` may already have — subshell traps are local to the
# subshell. A named handler, not an inline trap string, so `local rc=$?`
# is a normal function-local assignment shellcheck can see: it only
# removes `$tmp` when `$?` is nonzero — on the ordinary path the
# subshell also exits successfully, and `$tmp` has to survive that to
# reach the `mv` below.
(
_atomic_write_secret_cleanup() {
local rc=$?
[ "$rc" -eq 0 ] || rm -f "$tmp"
exit "$rc"
}
trap _atomic_write_secret_cleanup EXIT
cat > "$tmp"
chmod "$mode" "$tmp"
if [ -n "$owner" ]; then
chown "$owner" "$tmp"
fi
)
mv -f "$tmp" "$target"
}
''

View file

@ -764,6 +764,8 @@ let
forwarderSecretInContainer = "/var/lib/swarm-bao-otel-oidc/${cfg.otel.clientId}.secret";
forwarderHostSecretPath = "/var/lib/nixos-containers/${cfg.machine}${forwarderSecretInContainer}";
forwarderHostSecretDir = builtins.dirOf forwarderHostSecretPath;
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
# The collector runs under `DynamicUser` and opens `client_secret_file`
# itself, so there is no uid to hand a 0400 file to. `LoadCredential` reads
# it as root before the sandbox exists and re-exposes it under a path that
@ -1797,6 +1799,8 @@ in
script = ''
set -euo pipefail
${atomicWriteSecret}
# `bao`'s own message is the only thing separating a missing value
# from a refused identity from an unreachable store, and this unit
# retries on all three — so it reports which one rather than
@ -1837,10 +1841,7 @@ in
# an argument in /proc the way `install <<<"$secret"` or an `echo`
# from `path` would.
install -d -m 0755 ${lib.escapeShellArg forwarderHostSecretDir}
umask 077
printf '%s\n' "$secret" > ${lib.escapeShellArg forwarderHostSecretPath}
chown root:root ${lib.escapeShellArg forwarderHostSecretPath}
chmod 0400 ${lib.escapeShellArg forwarderHostSecretPath}
printf '%s\n' "$secret" | atomic_write_secret 0400 root:root ${lib.escapeShellArg forwarderHostSecretPath}
'';
};
})