derive a missing client digest from the secret that exists
A client with a secret in the store or in a leftover file but no digest gets the digest of that secret instead of stopping the unit. The password reaches authelia's prompt through a pty, not argv. A lost mint race takes the winner's secret. Also name the mint, not the publisher, in the remaining rust docs.
This commit is contained in:
parent
d30cb7d122
commit
05665c05f3
1 changed files with 127 additions and 57 deletions
|
|
@ -3,24 +3,27 @@
|
|||
# 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.
|
||||
# `mint_client_secret` keeps every client with a digest. A client without one
|
||||
# gets it from the secret that exists: the plaintext file an earlier release
|
||||
# left, else the store's value. Only a client with none of the three 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.
|
||||
#
|
||||
# `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).
|
||||
# Return statuses: 0 done, 1 retry later (the store could not answer, or a
|
||||
# digest could not be computed), 3 (migration only) the file and the store hold
|
||||
# different secrets, and only an operator can say which is right.
|
||||
#
|
||||
# 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`.
|
||||
# `$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
|
||||
|
|
@ -40,25 +43,50 @@
|
|||
return 2
|
||||
}
|
||||
|
||||
# <client id> <leaf>. Sets MINTED when it created a secret.
|
||||
# <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
|
||||
if [ -e "$legacy" ]; then
|
||||
if [ -s "$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
|
||||
client_secret_derive_digest "$id" "$(< "$legacy")" "$legacy"
|
||||
return
|
||||
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
|
||||
if [ -s "$dig" ]; then
|
||||
chmod 0600 "$dig"
|
||||
return 0
|
||||
fi
|
||||
chmod 0600 "$dig"
|
||||
return 0
|
||||
client_secret_derive_digest "$id" "$CLIENT_SECRET_VALUE" "the store at $leaf"
|
||||
return
|
||||
fi
|
||||
if [ "$state" = 2 ]; then
|
||||
if [ -s "$dig" ]; then
|
||||
|
|
@ -83,8 +111,9 @@
|
|||
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
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue