#!/bin/bash # The one-shot channel for a secret the WebUI collected. # # scripts/dev/lp-secret-channel-test # # The browser cannot run restic, so a repository password typed in the WebUI has # to reach the host. Both channels that already existed leak it: as part of a # task's command string it lands in frontend/data/tasks/*.json — 0644, inside a # world-readable directory, and visible in `ps` while the task runs — and as a # plain file there it is either world-readable at 0644 or unreadable by the # manager at 0640. That password is the key to every backup the user has. # # So the container writes it into a directory root prepares for this: owned # :, mode 2730. The setgid bit gives the file the manager's # group, the container writes it 0640, and the directory is unlistable. The # manager redeems it once and unlinks it, and the value never enters argv. # # The ownership half needs two real accounts and root, so it is checked only # when those are present; the redemption rules are checked always. REPO="$(cd "$(dirname "$0")/../.." && pwd)" BASE="$(mktemp -d "${TMPDIR:-/tmp}/lp-secret-test-XXXXXX")" trap 'rm -rf "$BASE"' EXIT fail=0 chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } mkdir -p "$BASE/webui/frontend/data/.secrets" webuiDir(){ printf '%s/webui' "$BASE"; } isError(){ LAST_ERR="$*"; } source "$REPO/scripts/webui/webui_secret.sh" printf 'hunter2-repo-password' > "$BASE/webui/frontend/data/.secrets/abcd1234efgh" echo "--- a reference is redeemed once ---" chk "first read" "$(webuiSecretConsume abcd1234efgh)" "hunter2-repo-password" chk "second read" "$(webuiSecretConsume abcd1234efgh 2>/dev/null)" "" chk "file is gone" "$([[ -e "$BASE/webui/frontend/data/.secrets/abcd1234efgh" ]] && echo yes || echo no)" "no" echo "--- ids that could escape the directory are refused ---" for bad in '../../../etc/passwd' 'a/b' 'x' '' '../secrets2/leak' 'name with space' "$(printf 'a%.0s' {1..80})"; do if webuiSecretConsume "$bad" >/dev/null 2>&1; then echo " FAIL accepted '$bad'"; fail=1 fi done echo " ok all rejected" # Nothing outside the drop may be touched even when the name resolves to a file. printf 'do-not-read' > "$BASE/outside" webuiSecretConsume "../outside" >/dev/null 2>&1 chk "file outside untouched" "$([[ -f "$BASE/outside" ]] && echo yes || echo no)" "yes" echo "--- resolve: references redeemed, plain values passed through ---" printf 'sekrit' > "$BASE/webui/frontend/data/.secrets/refref1234ab" chk "reference" "$(webuiSecretResolve 'secret:refref1234ab')" "sekrit" chk "plain value" "$(webuiSecretResolve 'just-a-password')" "just-a-password" chk "empty stays" "$(webuiSecretResolve '')" "" echo "--- abandoned drops are swept ---" printf 'stale' > "$BASE/webui/frontend/data/.secrets/staleaaaa111" touch -d '2 hours ago' "$BASE/webui/frontend/data/.secrets/staleaaaa111" printf 'fresh' > "$BASE/webui/frontend/data/.secrets/freshbbbb222" webuiSecretSweep chk "old removed" "$([[ -e "$BASE/webui/frontend/data/.secrets/staleaaaa111" ]] && echo yes || echo no)" "no" chk "fresh kept" "$([[ -e "$BASE/webui/frontend/data/.secrets/freshbbbb222" ]] && echo yes || echo no)" "yes" echo "--- the ownership boundary (needs root + both accounts) ---" if [[ $EUID -eq 0 ]] && id libreportal >/dev/null 2>&1 && id dockerinstall >/dev/null 2>&1 \ && [[ -d /libreportal-containers/libreportal/frontend/data ]]; then /usr/local/lib/libreportal/libreportal-ownership secret-dir d=/libreportal-containers/libreportal/frontend/data/.secrets chk "dir mode" "$(stat -c '%a' "$d")" "2730" chk "dir owner" "$(stat -c '%U:%G' "$d")" "dockerinstall:libreportal" su -s /bin/bash -c "umask 027; printf 'x' > $d/testtesttest" dockerinstall chk "file group inherited" "$(stat -c '%G' "$d/testtesttest")" "libreportal" su -s /bin/bash -c "test -r $d/testtesttest" libreportal \ && echo " ok manager can read" || { echo " FAIL manager cannot read"; fail=1; } su -s /bin/bash -c "test -r $d/testtesttest" nobody 2>/dev/null \ && { echo " FAIL another account can read"; fail=1; } || echo " ok others cannot read" rm -f "$d/testtesttest" else echo " SKIP not root, or the install is not present" fi echo "" if (( fail )); then echo "FAILED"; exit 1; fi echo "All secret-channel checks passed."