diff --git a/nix/host-modules/lib/oidc-client-secrets.nix b/nix/host-modules/lib/oidc-client-secrets.nix index ca4217c55..248e9c66b 100644 --- a/nix/host-modules/lib/oidc-client-secrets.nix +++ b/nix/host-modules/lib/oidc-client-secrets.nix @@ -16,11 +16,12 @@ # its digest but the store could not be read, so the caller finishes its work # from the digests and then retries. # -# 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` +# Pure function of `shell`, the path of a real shell; 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`. -{ }: +{ shell }: '' # . 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). @@ -43,10 +44,11 @@ # 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. + # `Digest:` line leaves it. `script` runs the command as `$SHELL -c`, and the + # service user's login shell is nologin, so the call sets `SHELL` to `shell`. client_secret_digest() { printf '%s\n' "$1" \ - | timeout 60 script -qec "authelia crypto hash generate pbkdf2 --variant sha512 --no-confirm" /dev/null \ + | SHELL=${shell} timeout 60 script -qec "authelia crypto hash generate pbkdf2 --variant sha512 --no-confirm" /dev/null \ | tr -d '\r' | sed -n 's/^Digest: *//p' } diff --git a/nix/host-modules/swarm-authelia.nix b/nix/host-modules/swarm-authelia.nix index 341de8e38..4dab96b67 100644 --- a/nix/host-modules/swarm-authelia.nix +++ b/nix/host-modules/swarm-authelia.nix @@ -464,7 +464,7 @@ let '' ); - oidcClientSecrets = import ./lib/oidc-client-secrets.nix { }; + oidcClientSecrets = import ./lib/oidc-client-secrets.nix { shell = lib.getExe pkgs.bash; }; # Every client with a secret to mint: a native client is public and has none. secretClients = lib.filter (c: c.kind != "native") cfg.oidc.clients; diff --git a/nix/script-tests/oidc-client-secrets.nix b/nix/script-tests/oidc-client-secrets.nix index 08b823fca..766958ac7 100644 --- a/nix/script-tests/oidc-client-secrets.nix +++ b/nix/script-tests/oidc-client-secrets.nix @@ -74,13 +74,13 @@ let esac ''; - # Runs the command it is given, as util-linux's `script -qec /dev/null` + # Runs the command it is given with `$SHELL -c`, as util-linux's `script -qec /dev/null` # does, without needing a pty in the build sandbox. The real pty is checked # against the real authelia by hand. fakeScript = pkgs.writeShellScriptBin "script" '' echo "script $1 $3" >> "$FAKE_AUTHELIA_LOG" [ "$1" = -qec ] || exit 99 - exec ${pkgs.bash}/bin/bash -c "$2" + exec "$SHELL" -c "$2" ''; in pkgs.runCommand "hyperhive-script-test-oidc-client-secrets" @@ -88,7 +88,7 @@ pkgs.runCommand "hyperhive-script-test-oidc-client-secrets" HELPER = pkgs.writeText "oidc-client-secrets.sh" ( import ../host-modules/lib/store-seeded-secret.nix { } + import ../host-modules/lib/atomic-write-secret.nix { } - + import ../host-modules/lib/oidc-client-secrets.nix { } + + import ../host-modules/lib/oidc-client-secrets.nix { shell = "${pkgs.bash}/bin/bash"; } ); FAKE_BIN = pkgs.symlinkJoin { name = "fake-bao-and-authelia"; diff --git a/nix/script-tests/oidc-client-secrets.sh b/nix/script-tests/oidc-client-secrets.sh index d0d83959e..311f13bc1 100644 --- a/nix/script-tests/oidc-client-secrets.sh +++ b/nix/script-tests/oidc-client-secrets.sh @@ -28,6 +28,8 @@ setup() { : >"$FAKE_BAO_LOG" : >"$FAKE_AUTHELIA_LOG" clients=$dir/clients + printf '#!/bin/sh\necho "This account is currently not available." >&2\nexit 1\n' >"$dir/nologin" + chmod +x "$dir/nologin" digest=$clients/$id.digest legacy=$clients/$id.secret stored=$FAKE_BAO_KV/$(printf '%s' "$leaf" | tr / _) @@ -35,11 +37,14 @@ setup() { # run — the function for $id and $leaf, with the stubs first on # PATH, after the store login the units make first and, when $PREFETCH is set, -# `store_prefetch` of the leaf. Sets $rc and $minted. +# `store_prefetch` of the leaf. `SHELL` is a nologin stub, as the service +# user's is. Sets $rc and $minted. run() { minted=$( set -e PATH="$FAKE_BIN/bin:$PATH" + SHELL=$dir/nologin + export SHELL unset BAO_TOKEN # shellcheck source=/dev/null . "$HELPER" @@ -174,6 +179,13 @@ if [ "$(stat -c %a "$digest")" != 600 ]; then fail "digest mode is $(stat -c %a if ! logged "oidc client $id: wrote the digest from the secret in the store"; then fail "no info line"; fi no_secret_in_argv +setup "mint: the digest is computed although the login shell is nologin" +printf 'old-secret' >"$stored" +run mint_client_secret +expect 0 +digest_is digest-of-old-secret +if grep -q "not available" "$dir/err" "$dir/out"; then fail "the nologin shell ran"; fi + setup "mint: a plaintext file the store lacks seeds the store once, mints nothing and keeps the digest and the file" printf 'old-secret\n' >"$legacy" printf 'old-digest\n' >"$digest"