diff --git a/nix/host-modules/glue-matrix-bao-token.nix b/nix/host-modules/glue-matrix-bao-token.nix index d1e36de0..0bb4e716 100644 --- a/nix/host-modules/glue-matrix-bao-token.nix +++ b/nix/host-modules/glue-matrix-bao-token.nix @@ -241,7 +241,7 @@ in exit 0 fi - printf '%s\n' "$token" | atomic_write_secret 0600 "" ${lib.escapeShellArg (toString deployCfg.matrix.appserviceTokenFile)} + atomic_write_secret 0600 "" ${lib.escapeShellArg (toString deployCfg.matrix.appserviceTokenFile)} "$token" # Re-stamp the registration file from the token just written. The # token is half an agreement — the registration the homeserver loads diff --git a/nix/host-modules/glue-queue-agent-credential.nix b/nix/host-modules/glue-queue-agent-credential.nix index 33efb086..d2a48a89 100644 --- a/nix/host-modules/glue-queue-agent-credential.nix +++ b/nix/host-modules/glue-queue-agent-credential.nix @@ -283,11 +283,11 @@ in install -d -m 0755 ${lib.escapeShellArg credentialDir} - printf '%s\n' "$secret" | atomic_write_secret 0600 "" ${lib.escapeShellArg secretFile} + atomic_write_secret 0600 "" ${lib.escapeShellArg secretFile} "$secret" # `0644` on purpose: an OIDC client id is presented to the token # endpoint on every connection and is public by construction. - printf '%s\n' "$client_id" | atomic_write_secret 0644 "" ${lib.escapeShellArg clientIdFile} + atomic_write_secret 0644 "" ${lib.escapeShellArg clientIdFile} "$client_id" ''; }; }) diff --git a/nix/host-modules/lib/atomic-write-secret.nix b/nix/host-modules/lib/atomic-write-secret.nix index cf50e12a..1456775c 100644 --- a/nix/host-modules/lib/atomic-write-secret.nix +++ b/nix/host-modules/lib/atomic-write-secret.nix @@ -7,24 +7,40 @@ # 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} -# printf '%s\n' "$secret" | atomic_write_secret 0600 "" "$path" -# printf '%s\n' "$secret" | atomic_write_secret 0400 "grafana:0" "$path" +# atomic_write_secret 0600 "" "$path" "$secret" +# atomic_write_secret 0400 "grafana:0" "$path" "$secret" # ''; # -# 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`. +# 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" tmp + 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 @@ -42,7 +58,7 @@ exit "$rc" } trap _atomic_write_secret_cleanup EXIT - cat > "$tmp" + printf '%s\n' "$value" > "$tmp" chmod "$mode" "$tmp" if [ -n "$owner" ]; then chown "$owner" "$tmp" diff --git a/nix/host-modules/swarm-bao.nix b/nix/host-modules/swarm-bao.nix index 05daa859..d44ddce2 100644 --- a/nix/host-modules/swarm-bao.nix +++ b/nix/host-modules/swarm-bao.nix @@ -1841,7 +1841,7 @@ in # an argument in /proc the way `install <<<"$secret"` or an `echo` # from `path` would. install -d -m 0755 ${lib.escapeShellArg forwarderHostSecretDir} - printf '%s\n' "$secret" | atomic_write_secret 0400 root:root ${lib.escapeShellArg forwarderHostSecretPath} + atomic_write_secret 0400 root:root ${lib.escapeShellArg forwarderHostSecretPath} "$secret" ''; }; }) @@ -2647,7 +2647,12 @@ in chgrp ${tokenGroup} ${tokenStoreDir}/tpm2_pkcs11.sqlite3 chmod 0660 ${tokenStoreDir}/tpm2_pkcs11.sqlite3 - printf 'BAO_HSM_PIN=%s\n' "$(cat ${tokenStoreDir}/user-pin)" | atomic_write_secret 0400 "" ${pinEnvFile} + # Captured first, not piped: a `$(cmd)` that fails aborts here + # under `set -e`, before the helper — which cannot see a + # producer's exit code once its output is inside the pipe — + # is ever called. + pin_env_value="BAO_HSM_PIN=$(cat ${tokenStoreDir}/user-pin)" + atomic_write_secret 0400 "" ${pinEnvFile} "$pin_env_value" ''; }; diff --git a/nix/host-modules/swarm-grafana.nix b/nix/host-modules/swarm-grafana.nix index 355e8c6f..026ad54d 100644 --- a/nix/host-modules/swarm-grafana.nix +++ b/nix/host-modules/swarm-grafana.nix @@ -693,7 +693,7 @@ in # grants the group nothing; if this mode ever widens, the gid has to be # discovered at runtime rather than assumed. install -d -m 0755 ${lib.escapeShellArg hostSecretDir} - printf '%s\n' "$secret" | atomic_write_secret 0400 ${lib.escapeShellArg "${toString config.ids.uids.grafana}:0"} ${lib.escapeShellArg hostSecretPath} + atomic_write_secret 0400 ${lib.escapeShellArg "${toString config.ids.uids.grafana}:0"} ${lib.escapeShellArg hostSecretPath} "$secret" ''; }; diff --git a/nix/host-modules/swarm-otel.nix b/nix/host-modules/swarm-otel.nix index 3b1f2e38..9782f3ed 100644 --- a/nix/host-modules/swarm-otel.nix +++ b/nix/host-modules/swarm-otel.nix @@ -866,7 +866,7 @@ in # no uid to give this to — `LoadCredential` reads it as root before # the sandbox exists and re-exposes it to whichever uid the unit got. install -d -m 0755 ${lib.escapeShellArg collectorHostSecretDir} - printf '%s\n' "$secret" | atomic_write_secret 0400 root:root ${lib.escapeShellArg collectorHostSecretPath} + atomic_write_secret 0400 root:root ${lib.escapeShellArg collectorHostSecretPath} "$secret" ''; };