144 lines
5.3 KiB
Nix
144 lines
5.3 KiB
Nix
# Shared shell steps for a unit that keeps a secret it once generated on disk
|
|
# in the swarm secret store instead. The store's value wins; a store holding
|
|
# none is seeded from the old file, so the secret keeps its value; a new value
|
|
# is minted only when there is no file either.
|
|
#
|
|
# `store_seeded` prints the value and never rotates one: the store is written
|
|
# with `-cas=0`, so only a leaf that holds nothing takes a value, and a lost
|
|
# race reads the winner's back. A read of the leaf that fails for any reason
|
|
# but "No value found" prints the store's error and neither mints nor writes.
|
|
#
|
|
# `store_prefetch` reads many leaves concurrently, and the first `store_read` of
|
|
# a leaf returns that answer, status and error included, instead of asking
|
|
# again, so what a caller decides from a read is the same. Later reads of the
|
|
# leaf, such as one that checks a write, ask the store.
|
|
#
|
|
# Pure function — NOT a NixOS module. Call it from a module's `let`:
|
|
#
|
|
# storeSeededSecret = import ./lib/store-seeded-secret.nix { };
|
|
# script = ''
|
|
# ${storeSeededSecret}
|
|
# store_init
|
|
# store_login
|
|
# key="$(store_seeded secret/swarm/services/foo/key /var/lib/foo/key mint_key)" || exit 1
|
|
# '';
|
|
#
|
|
# Needs `bao`, `coreutils` and `gnugrep` on the caller's `path` and the `BAO_*` client
|
|
# environment set. Takes the EXIT trap, to remove its scratch file.
|
|
{ }:
|
|
''
|
|
# Bounded, so a store that drops packets cannot hold the caller past its
|
|
# start timeout; one that refuses answers at once.
|
|
store_bao() {
|
|
timeout 15 bao "$@"
|
|
}
|
|
|
|
store_init() {
|
|
STORE_ERR="$(mktemp)"
|
|
STORE_UP=0
|
|
STORE_PREFETCH="$(mktemp -d)"
|
|
trap 'rm -rf "$STORE_ERR" "$STORE_PREFETCH"' EXIT
|
|
}
|
|
|
|
# Sets STORE_UP. A store that cannot be logged in to leaves every secret to
|
|
# its file.
|
|
store_login() {
|
|
local token
|
|
if token="$(store_bao login -method=cert -token-only 2>"$STORE_ERR")"; then
|
|
BAO_TOKEN="$token"
|
|
export BAO_TOKEN
|
|
STORE_UP=1
|
|
else
|
|
cat "$STORE_ERR" >&2
|
|
echo "could not log in to the swarm secret store; secrets keep the value of their files." >&2
|
|
fi
|
|
}
|
|
|
|
# <leaf>… Starts the first read of each leaf now, at most STORE_PREFETCH_JOBS
|
|
# at a time, and returns when all have ended. Each read is bounded by
|
|
# `store_bao`, and its answer is kept for `store_read`. Does nothing without
|
|
# a login.
|
|
store_prefetch() {
|
|
local leaf key
|
|
[ "$STORE_UP" = 1 ] || return 0
|
|
for leaf in "$@"; do
|
|
while [ "$(jobs -rp | wc -l)" -ge "''${STORE_PREFETCH_JOBS:-8}" ]; do
|
|
wait -n || true
|
|
done
|
|
key="$STORE_PREFETCH/$(printf '%s' "$leaf" | tr / _)"
|
|
(
|
|
rc=0
|
|
store_bao kv get -field=value "$leaf" >"$key.out" 2>"$key.err" || rc=$?
|
|
echo "$rc" >"$key.rc"
|
|
) &
|
|
done
|
|
wait
|
|
}
|
|
|
|
# <leaf>. `store_bao kv get -field=value <leaf>`, with the error in
|
|
# $STORE_ERR: the answer `store_prefetch` kept, once, then the store's.
|
|
store_read() {
|
|
local key="$STORE_PREFETCH/$(printf '%s' "$1" | tr / _)" rc
|
|
if [ -e "$key.rc" ]; then
|
|
rc="$(< "$key.rc")"
|
|
cat "$key.out"
|
|
cat "$key.err" >"$STORE_ERR"
|
|
rm -f "$key.out" "$key.err" "$key.rc"
|
|
return "$rc"
|
|
fi
|
|
store_bao kv get -field=value "$1" 2>"$STORE_ERR"
|
|
}
|
|
|
|
# <leaf> <earlier file> <mint command…>. The value on stdout; status 1 when
|
|
# neither the store nor the file can give one. `kv get` exits 2 both for a
|
|
# leaf that holds nothing ("No value found at <path>") and for a refused
|
|
# read, so only that message means absent. Any other failure of the first
|
|
# read, the `timeout` (124) included, neither mints nor writes: the file's
|
|
# value is used when it has one, and the call fails when it has none.
|
|
store_seeded() {
|
|
local leaf="$1" earlier="$2" value candidate rc=0
|
|
shift 2
|
|
if [ "$STORE_UP" = 1 ]; then
|
|
value="$(store_read "$leaf")" || rc=$?
|
|
if [ "$rc" = 0 ] && [ -n "$value" ]; then
|
|
printf '%s' "$value"
|
|
return 0
|
|
fi
|
|
if [ "$rc" != 0 ] && ! grep -q '^No value found at ' "$STORE_ERR"; then
|
|
echo "could not read $leaf from the swarm secret store (exit status $rc):" >&2
|
|
cat "$STORE_ERR" >&2
|
|
if [ -s "$earlier" ]; then
|
|
echo "using the value of $earlier for $leaf; nothing is written to the store." >&2
|
|
printf '%s' "$(< "$earlier")"
|
|
return 0
|
|
fi
|
|
echo "no value for $leaf in the store or at $earlier." >&2
|
|
return 1
|
|
fi
|
|
if [ -s "$earlier" ]; then
|
|
candidate="$(< "$earlier")"
|
|
echo "storing the value of $earlier in $leaf." >&2
|
|
else
|
|
candidate="$("$@")"
|
|
echo "no earlier value; minting $leaf." >&2
|
|
fi
|
|
if ! printf '%s' "$candidate" | store_bao kv put -cas=0 "$leaf" value=- >/dev/null 2>"$STORE_ERR"; then
|
|
echo "could not create $leaf; reading it back in case another run created it." >&2
|
|
cat "$STORE_ERR" >&2
|
|
fi
|
|
if value="$(store_bao kv get -field=value "$leaf" 2>"$STORE_ERR")" && [ -n "$value" ]; then
|
|
printf '%s' "$value"
|
|
return 0
|
|
fi
|
|
cat "$STORE_ERR" >&2
|
|
echo "the store did not return $leaf." >&2
|
|
fi
|
|
if [ -s "$earlier" ]; then
|
|
echo "using the value of $earlier for $leaf." >&2
|
|
printf '%s' "$(< "$earlier")"
|
|
return 0
|
|
fi
|
|
echo "no value for $leaf in the store or at $earlier." >&2
|
|
return 1
|
|
}
|
|
''
|