The mint creates each client's secret in the swarm secret store and writes only the digest to disk. It mints only when the store answers and holds none. A new unit moves the plaintext files an earlier release left into the store and removes each once the store holds the same value. The bridge reads its secret from the store into /run before each start. The secret publisher unit and its path unit are gone.
129 lines
5.3 KiB
Nix
129 lines
5.3 KiB
Nix
# Shared shell steps for authelia's OIDC client secrets, whose plaintext half
|
|
# lives in the swarm secret store and never on disk. authelia keeps only a
|
|
# digest of each, in `$clients/<id>.digest`, and starts from it whether or not
|
|
# the store answers.
|
|
#
|
|
# `mint_client_secret` is the only writer of a new secret. It creates the
|
|
# store's leaf (`-cas=0`, so an existing value is never replaced) before it
|
|
# writes the digest, so a digest on disk always has a secret behind it. It mints
|
|
# only after a read that says the store holds none; a store that cannot be read
|
|
# never mints, because a second secret would rotate the first.
|
|
#
|
|
# `migrate_client_secret` moves the plaintext file an earlier release left in
|
|
# `$clients/<id>.secret` into the store and removes it once a read-back shows
|
|
# the store holds the same value. It never overwrites a store value and never
|
|
# removes a file the store does not match.
|
|
#
|
|
# Return statuses of both: 0 done, 1 retry later (the store could not answer),
|
|
# 3 an operator has to look (logged at `err`; retrying changes nothing).
|
|
#
|
|
# Pure function — NOT a NixOS module. Call it after `./store-seeded-secret.nix`
|
|
# and `./atomic-write-secret.nix`, with `store_init` and `store_login` run and
|
|
# `$clients` set to the digest directory. Needs `authelia`, `bao`, `coreutils`
|
|
# and `sed` on the caller's `path`.
|
|
{ }:
|
|
''
|
|
# <leaf>. Sets CLIENT_SECRET_VALUE. 0: the store holds a value. 1: it holds
|
|
# none. 2: it cannot tell (not logged in, down, or refusing the read).
|
|
client_secret_read() {
|
|
CLIENT_SECRET_VALUE=""
|
|
[ "$STORE_UP" = 1 ] || return 2
|
|
if CLIENT_SECRET_VALUE="$(store_bao kv get -field=value "$1" 2>"$STORE_ERR")" \
|
|
&& [ -n "$CLIENT_SECRET_VALUE" ]; then
|
|
return 0
|
|
fi
|
|
CLIENT_SECRET_VALUE=""
|
|
if grep -q '^No value found at ' "$STORE_ERR"; then
|
|
return 1
|
|
fi
|
|
cat "$STORE_ERR" >&2
|
|
return 2
|
|
}
|
|
|
|
# <client id> <leaf>. Sets MINTED when it created a secret.
|
|
mint_client_secret() {
|
|
local id="$1" leaf="$2" dig="$clients/$1.digest" legacy="$clients/$1.secret"
|
|
local state=0 out secret digest
|
|
if [ -e "$legacy" ]; then
|
|
if [ -s "$dig" ]; then
|
|
return 0
|
|
fi
|
|
echo "<3>oidc client $id: $legacy exists but its digest does not; not minting a second secret. Restore the digest, or remove the file and the store's leaf $leaf to mint again." >&2
|
|
return 3
|
|
fi
|
|
client_secret_read "$leaf" || state=$?
|
|
if [ "$state" = 0 ]; then
|
|
if [ ! -s "$dig" ]; then
|
|
echo "<3>oidc client $id: the store holds a secret at $leaf but $dig is missing; not minting a second secret. Restore the digest, or remove the leaf to mint again." >&2
|
|
return 3
|
|
fi
|
|
chmod 0600 "$dig"
|
|
return 0
|
|
fi
|
|
if [ "$state" = 2 ]; then
|
|
if [ -s "$dig" ]; then
|
|
chmod 0600 "$dig"
|
|
echo "oidc client $id: the store cannot be read; keeping its digest" >&2
|
|
return 0
|
|
fi
|
|
echo "oidc client $id: the store cannot be read and there is no digest; not minting" >&2
|
|
return 1
|
|
fi
|
|
echo "oidc client $id: the store holds no secret at $leaf; minting one"
|
|
out="$(authelia crypto hash generate pbkdf2 --variant sha512 --random)"
|
|
secret="$(printf '%s' "$out" | sed -n 's/^Random Password: *//p')"
|
|
digest="$(printf '%s' "$out" | sed -n 's/^Digest: *//p')"
|
|
if [ -z "$secret" ] || [ -z "$digest" ]; then
|
|
echo "<3>authelia crypto hash generate produced no secret/digest for $id" >&2
|
|
return 1
|
|
fi
|
|
if ! printf '%s' "$secret" | store_bao kv put -cas=0 "$leaf" value=- >/dev/null 2>"$STORE_ERR"; then
|
|
cat "$STORE_ERR" >&2
|
|
echo "could not create $leaf; reading it back in case another run created it" >&2
|
|
fi
|
|
client_secret_read "$leaf" || return 1
|
|
if [ "$CLIENT_SECRET_VALUE" != "$secret" ]; then
|
|
echo "<3>oidc client $id: $leaf holds a different value than the one minted; not writing a digest" >&2
|
|
return 3
|
|
fi
|
|
atomic_write_secret 0600 "" "$dig" "$digest" || return 1
|
|
MINTED=1
|
|
echo "minted client secret for $id into the store"
|
|
}
|
|
|
|
# <client id> <leaf>
|
|
migrate_client_secret() {
|
|
local id="$1" leaf="$2" legacy="$clients/$1.secret" value state=0
|
|
[ -e "$legacy" ] || return 0
|
|
value="$(< "$legacy")"
|
|
if [ -z "$value" ]; then
|
|
echo "<3>oidc client $id: $legacy is empty; keeping it" >&2
|
|
return 3
|
|
fi
|
|
client_secret_read "$leaf" || state=$?
|
|
if [ "$state" = 2 ]; then
|
|
echo "oidc client $id: the store cannot be read; keeping $legacy" >&2
|
|
return 1
|
|
fi
|
|
if [ "$state" = 0 ]; then
|
|
if [ "$CLIENT_SECRET_VALUE" != "$value" ]; then
|
|
echo "<3>oidc client $id: the store at $leaf and $legacy hold different secrets; keeping both" >&2
|
|
return 3
|
|
fi
|
|
rm -f "$legacy" || return 1
|
|
echo "oidc client $id: already in the store, removed the file $legacy"
|
|
return 0
|
|
fi
|
|
if ! printf '%s' "$value" | store_bao kv put -cas=0 "$leaf" value=- >/dev/null 2>"$STORE_ERR"; then
|
|
cat "$STORE_ERR" >&2
|
|
echo "oidc client $id: could not create $leaf; reading it back in case another run created it" >&2
|
|
fi
|
|
client_secret_read "$leaf" || return 1
|
|
if [ "$CLIENT_SECRET_VALUE" != "$value" ]; then
|
|
echo "<3>oidc client $id: the store at $leaf and $legacy hold different secrets; keeping both" >&2
|
|
return 3
|
|
fi
|
|
rm -f "$legacy" || return 1
|
|
echo "oidc client $id: stored in the store, removed the file $legacy"
|
|
}
|
|
''
|