# 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//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" } ''