A leftover plaintext client secret is stored once with -cas=0 when the store holds none, so nothing rotates. A store value wins over a different file. The file is no longer read back, compared or removed, and the migration unit is gone.
122 lines
5.2 KiB
Nix
122 lines
5.2 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` keeps every client with a digest. A client without one
|
|
# gets it from the store's secret, which an earlier release's plaintext file
|
|
# seeds once when the store holds none. Only a client with neither is minted,
|
|
# and then the store's leaf is created (`-cas=0`, so an existing value is never
|
|
# replaced) before the digest is written, so a digest on disk always has a
|
|
# secret behind it. A store that cannot be read never mints, because a second
|
|
# secret would rotate the first.
|
|
#
|
|
# Return statuses: 0 done, 1 retry later (the store could not answer, or a
|
|
# digest could not be computed).
|
|
#
|
|
# 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`, `script`
|
|
# (util-linux), `coreutils`, `grep` 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
|
|
}
|
|
|
|
# <secret>. Prints authelia's digest of it, the form `mint` would have
|
|
# written. The CLI takes a password only on a terminal prompt or in argv,
|
|
# and argv is world-readable in /proc, so the prompt is answered through a
|
|
# pty from stdin. The pty echoes the password into the pipe, and only the
|
|
# `Digest:` line leaves it.
|
|
client_secret_digest() {
|
|
printf '%s\n' "$1" \
|
|
| timeout 60 script -qec "authelia crypto hash generate pbkdf2 --variant sha512 --no-confirm" /dev/null \
|
|
| tr -d '\r' | sed -n 's/^Digest: *//p'
|
|
}
|
|
|
|
# <client id> <secret> <where the secret came from>. Writes the digest of an
|
|
# existing secret; the secret itself is untouched.
|
|
client_secret_derive_digest() {
|
|
local id="$1" secret="$2" source="$3" digest
|
|
digest="$(client_secret_digest "$secret")" || digest=""
|
|
if [ -z "$digest" ]; then
|
|
echo "<3>oidc client $id: could not compute a digest from the secret in $source" >&2
|
|
return 1
|
|
fi
|
|
atomic_write_secret 0600 "" "$clients/$id.digest" "$digest" || return 1
|
|
MINTED=1
|
|
echo "oidc client $id: wrote the digest from the secret in $source; the secret is unchanged"
|
|
}
|
|
|
|
# <client id> <leaf>. Sets MINTED when it wrote a digest.
|
|
mint_client_secret() {
|
|
local id="$1" leaf="$2" dig="$clients/$1.digest" legacy="$clients/$1.secret"
|
|
local state=0 out secret digest
|
|
client_secret_read "$leaf" || state=$?
|
|
if [ "$state" = 1 ] && [ -s "$legacy" ]; then
|
|
# A file an earlier release left: its value goes into the store once, so
|
|
# the secret keeps its value. The file stays, unused.
|
|
printf '%s' "$(< "$legacy")" | store_bao kv put -cas=0 "$leaf" value=- >/dev/null 2>"$STORE_ERR" \
|
|
|| cat "$STORE_ERR" >&2
|
|
state=0
|
|
client_secret_read "$leaf" || state=$?
|
|
if [ "$state" = 1 ]; then
|
|
echo "oidc client $id: could not store the secret from $legacy in $leaf; not minting" >&2
|
|
return 1
|
|
fi
|
|
echo "oidc client $id: stored the secret from $legacy in the store"
|
|
fi
|
|
if [ "$state" = 0 ]; then
|
|
if [ -s "$dig" ]; then
|
|
chmod 0600 "$dig"
|
|
return 0
|
|
fi
|
|
client_secret_derive_digest "$id" "$CLIENT_SECRET_VALUE" "the store at $leaf"
|
|
return
|
|
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 "oidc client $id: $leaf holds a value another run created; using it"
|
|
client_secret_derive_digest "$id" "$CLIENT_SECRET_VALUE" "the store at $leaf"
|
|
return
|
|
fi
|
|
atomic_write_secret 0600 "" "$dig" "$digest" || return 1
|
|
MINTED=1
|
|
echo "minted client secret for $id into the store"
|
|
}
|
|
''
|