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
53 lines
2.2 KiB
Nix
53 lines
2.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.
|
|
#
|
|
# Pure function — NOT a NixOS module. Call it from a module's `let`:
|
|
#
|
|
# atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
|
# ...
|
|
# script = ''
|
|
# ${atomicWriteSecret}
|
|
# printf '%s\n' "$secret" | atomic_write_secret 0600 "" "$path"
|
|
# printf '%s\n' "$secret" | atomic_write_secret 0400 "grafana:0" "$path"
|
|
# '';
|
|
#
|
|
# Third argument to `atomic_write_secret` is the target path; the second is
|
|
# an owner for `chown` (`user:group` or a bare uid), or "" to leave the
|
|
# mktemp-created root:root ownership as it is. Reads its content from
|
|
# stdin. Requires `coreutils` on the caller's `path`.
|
|
{ }:
|
|
''
|
|
atomic_write_secret() {
|
|
local mode="$1" owner="$2" target="$3" 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
|
|
cat > "$tmp"
|
|
chmod "$mode" "$tmp"
|
|
if [ -n "$owner" ]; then
|
|
chown "$owner" "$tmp"
|
|
fi
|
|
)
|
|
mv -f "$tmp" "$target"
|
|
}
|
|
''
|