The pipe contract had a gap: if a producer piped into atomic_write_secret exited non-zero after writing partial output, cat still saw a clean EOF and wrote that partial content through to the live target via mv — pipefail only reported the failure afterward, once the bad write was already committed. The helper now takes the value as its 4th argument and writes it itself with printf (a shell builtin, so the value never touches an external process's own argv/environ, same as a function argument never does), so there is no pipe left to fail silently. Callers that compute the value with a command now capture it into a variable first (`value=$(cmd)`), which fails under `set -e` before atomic_write_secret is ever called — swarm-bao.nix's pin.env site is the one that needed this (`pin_env_value="BAO_HSM_PIN=$(cat ...)"`). All seven call sites converted; output is byte-identical (same printf '%s\n' framing, now applied inside the helper instead of by each caller). Refs #4723
69 lines
3.2 KiB
Nix
69 lines
3.2 KiB
Nix
# Shared shell step for a systemd oneshot that lands a freshly-fetched
|
|
# secret on disk: never `> path` the live file directly, because a reader
|
|
# racing the write can open it between the truncate and the write, or
|
|
# between the write and a `chmod` that follows it, and see an empty file
|
|
# or one at the wrong mode. `mktemp` always creates its file at 0600
|
|
# regardless of umask, so the temp file is private for its whole life; only
|
|
# the `chmod`/`chown`/`mv -f` sequence below ever makes the target mode and
|
|
# owner visible, and only once the content is already final.
|
|
#
|
|
# The value is a function ARGUMENT, not piped in on stdin: a producer that
|
|
# exits non-zero partway through a pipe still leaves `cat` a clean EOF, so
|
|
# `atomic_write_secret`'s own writer has no way to tell a truncated read from
|
|
# an intentionally short one — the partial content would still land at the
|
|
# live path, `mv` and all, with only `pipefail` reporting the failure
|
|
# afterwards. A caller that computes the value with a command captures it
|
|
# into a variable first (`value=$(cmd)`, which fails under `set -e` before
|
|
# this function is ever called) and passes the finished value in.
|
|
#
|
|
# `$4` is never handed to an external program — it's a shell function
|
|
# argument, not a process's `argv`/environment, so it never appears in that
|
|
# process's own `/proc/<pid>/cmdline`; the write inside uses `printf`, a
|
|
# shell builtin, for the same reason.
|
|
#
|
|
# Pure function — NOT a NixOS module. Call it from a module's `let`:
|
|
#
|
|
# atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
|
# ...
|
|
# script = ''
|
|
# ${atomicWriteSecret}
|
|
# atomic_write_secret 0600 "" "$path" "$secret"
|
|
# atomic_write_secret 0400 "grafana:0" "$path" "$secret"
|
|
# '';
|
|
#
|
|
# Args: mode, an owner for `chown` (`user:group` or a bare uid, or "" to
|
|
# leave the mktemp-created root:root ownership as it is), the target path,
|
|
# and the value. The value is written followed by a trailing newline (every
|
|
# call site's prior `printf '%s\n' "$value"` behaviour) — a caller building
|
|
# a multi-field value (e.g. `KEY=$value`) assembles the whole string first
|
|
# and passes that. Requires `coreutils` on the caller's `path`.
|
|
{ }:
|
|
''
|
|
atomic_write_secret() {
|
|
local mode="$1" owner="$2" target="$3" value="$4" tmp
|
|
tmp="$(mktemp "$(dirname -- "$target")/.$(basename -- "$target").XXXXXX")"
|
|
# 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
|
|
printf '%s\n' "$value" > "$tmp"
|
|
chmod "$mode" "$tmp"
|
|
if [ -n "$owner" ]; then
|
|
chown "$owner" "$tmp"
|
|
fi
|
|
)
|
|
mv -f "$tmp" "$target"
|
|
}
|
|
''
|