Keep temporary sudo grants bounded through lifecycle failures

This commit is contained in:
Afonso Oliveira committed 2026-09-07 21:50:47 +01:00
1 parent bc235d2807
commit 4ae25cd4d2
6 files changed
+440 -79

No files matched your search

+51 -19
View File
@@ -13,7 +13,7 @@ trap 'rm -rf "$test_tmp"' EXIT
function_prefix() {
printf 'source %q\n' "$security_library_path"
awk '/^source .*omarchy-security-functions/ { next } /^case "\$\{1:-\}" in$/ { exit } { print }' "$command_path"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
}
# Exercise the validation code itself. Leading zeroes remain numeric, but zero,
@@ -23,7 +23,7 @@ function_prefix() {
for minutes in 1 15 1440 00015; do
valid_minutes "$minutes" || fail "passwordless sudo accepts bounded duration $minutes"
done
for minutes in 0 1441 -1 1m '1;id' ''; do
for minutes in 0 1441 -1 1m '1;id' '' 18446744073709551617; do
! valid_minutes "$minutes" || fail "passwordless sudo rejects invalid duration '$minutes'"
done
)
@@ -45,8 +45,6 @@ pass "passwordless sudo derives and validates trusted account identity"
# model that publishes a token only when -N is missing.
grep -Fxq '#!/bin/bash -p' "$command_path" ||
fail "passwordless sudo no longer suppresses Bash startup injection"
grep -F '[[ ${argv[1]:-} == -p ]]' "$security_library_path" >/dev/null ||
fail "passwordless sudo accepts a decoy post-script -p"
public_sudo_stub="$test_tmp/public-sudo"
public_gum_stub="$test_tmp/public-gum"
@@ -67,19 +65,20 @@ if [[ ${1:-} == -N ]]; then no_update=1; shift; fi
[[ ${1:-} != -- ]] || shift
((no_update)) || : >"$TEST_PUBLIC_TOKEN"
case "${2:-}" in
__status) exit 1 ;;
__status) exit "${TEST_PUBLIC_STATUS:-3}" ;;
__enable|__disable) exit 0 ;;
*) exit 2 ;;
esac
STUB
cat >"$public_gum_stub" <<'STUB'
#!/bin/bash
[[ -z ${TEST_PUBLIC_GUM_LOG:-} ]] || : >"$TEST_PUBLIC_GUM_LOG"
[[ ! -e $TEST_PUBLIC_TOKEN ]] || : >"$TEST_PUBLIC_EXPLOIT"
exit 1
STUB
chmod 0755 "$public_sudo_stub" "$public_gum_stub"
public_flow="$test_tmp/passwordless-public-flow"
/usr/bin/cp "$security_library_path" "$test_tmp/omarchy-security-functions"
/usr/bin/sed "s#/usr/bin/sudo#$public_sudo_stub#g" "$security_library_path" >"$test_tmp/omarchy-security-functions"
/usr/bin/sed \
-e "s#/usr/bin/sudo#$public_sudo_stub#g" \
-e "s#/usr/bin/gum#$public_gum_stub#g" \
@@ -89,6 +88,16 @@ TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
/usr/bin/bash -p "$public_flow" 15 >/dev/null
[[ ! -e $public_token && ! -e $public_exploit ]] ||
fail "passwordless confirmation inherited a reusable status credential"
for status in 1 2; do
if TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
TEST_PUBLIC_STATUS="$status" TEST_PUBLIC_GUM_LOG="$test_tmp/unsafe-status-confirmation" \
/usr/bin/bash -p "$public_flow" 15 >"$test_tmp/status-error.output" 2>&1; then
fail "passwordless sudo treats status/authorization failure $status as inactive"
fi
[[ ! -e $test_tmp/unsafe-status-confirmation ]] || fail "failed status inspection opens the enable prompt"
grep -q 'Could not safely inspect passwordless sudo' "$test_tmp/status-error.output" ||
fail "failed status inspection lacks recovery guidance"
done
startup_env="$test_tmp/passwordless-bash-env"
startup_marker="$test_tmp/passwordless-bash-env-ran"
@@ -341,6 +350,9 @@ pkgs_candidates=(
pkgs_root=""
for candidate in "${pkgs_candidates[@]}"; do
if [[ -n $candidate && -d $candidate/pkgbuilds/omarchy-settings ]]; then
pkgs_root=$candidate/pkgbuilds
break
elif [[ -n $candidate && -d $candidate/omarchy-settings ]]; then
pkgs_root=$candidate
break
fi
@@ -348,31 +360,36 @@ done
[[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage"
for package_name in omarchy-settings omarchy-settings-dev; do
install_script="$pkgs_root/pkgbuilds/$package_name/$package_name.install"
install_script="$pkgs_root/$package_name/$package_name.install"
transformed_install="$test_tmp/$package_name.install"
removal_root="$test_tmp/$package_name-remove"
removal_sudoers="$removal_root/etc/sudoers.d"
mkdir -p "$removal_sudoers"
mkdir -p "$removal_sudoers" "$removal_root/run/lock" "$removal_root/etc/tmpfiles.d"
: >"$removal_sudoers/99-omarchy-nopasswd-1000"
: >"$removal_sudoers/99-omarchy-nopasswd-legacy-user"
: >"$removal_sudoers/omarchy-dns"
ln -s ../usr/share/omarchy/etc-overrides/os-release "$removal_root/etc/os-release"
grep -Fq 'ln -s ../usr/share/omarchy/etc-overrides/os-release /etc/os-release' "$install_script" ||
fail "$package_name installation does not select package-owned OS metadata"
sed "s#/etc/#$removal_root/etc/#g" "$install_script" >"$transformed_install"
ln -s ../administrator/os-release "$removal_root/etc/os-release"
package_stat="$test_tmp/package-stat"
cat >"$package_stat" <<'STUB'
#!/bin/bash
if [[ $2 == '%u' ]]; then printf '0\n'; else /usr/bin/stat "$@"; fi
STUB
chmod +x "$package_stat"
sed -e "s#/etc/#$removal_root/etc/#g" \
-e "s#/run#$removal_root/run#g" \
-e "s#/usr/bin/stat#$package_stat#g" "$install_script" >"$transformed_install"
(
source "$transformed_install"
pre_remove
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
post_remove
) || fail "$package_name removal revokes active passwordless grants"
! find "$removal_sudoers" -name '99-omarchy-nopasswd-*' -print -quit | grep -q . ||
fail "$package_name removal leaves a passwordless grant behind"
[[ -e $removal_sudoers/omarchy-dns ]] ||
fail "$package_name removal deletes an unrelated sudoers policy"
[[ -L $removal_root/etc/os-release ]] &&
[[ $(readlink "$removal_root/etc/os-release") == ../usr/lib/os-release ]] ||
fail "$package_name removal does not restore the standard OS selector"
ln -sfn ../administrator/os-release "$removal_root/etc/os-release"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal changes unrelated OS metadata"
: >"$removal_sudoers/99-omarchy-nopasswd-1001"
(
source "$transformed_install"
@@ -382,8 +399,23 @@ for package_name in omarchy-settings omarchy-settings-dev; do
fail "$package_name removal overwrites an administrator OS selector"
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1001 ]] ||
fail "$package_name removal grant cleanup depends on OS selector state"
(
source "$transformed_install"
_etc_overrides_apply() { :; }
if post_install; then exit 1; fi
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_root/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
post_install
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_sudoers/99-omarchy-nopasswd-1002"
pre_upgrade
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1002 ]]
post_upgrade
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
) || fail "$package_name restores grant availability only after boot cleanup is installed"
done
pass "settings package removal revokes grants and preserves package-selector ownership"
pass "settings package transitions revoke grants and preserve unrelated configuration"
# Exercise the production flock wrapper under contention. mkdir is an atomic
# overlap detector; all workers must enter and leave the protected region.
@@ -418,7 +450,7 @@ pass "passwordless sudo serializes concurrent operations"
# Same-boot expiry calls the fixed installed cleanup command, and cleanup
# removes policy before touching a timer so timer failures cannot extend it.
grep -F '"$INSTALLED_SELF" __expire "$uid"' "$command_path" >/dev/null
grep -F '"$INSTALLED_SELF" __expire "$uid" "$timer"' "$command_path" >/dev/null
cleanup_body=$(awk '/^cleanup_uid_locked\(\) \{/ { in_body=1 } in_body { print } in_body && /^}/ { exit }' "$command_path")
rm_line=$(grep -n '/usr/bin/rm -f' <<<"$cleanup_body" | head -1 | cut -d: -f1)
stop_line=$(grep -n 'stop_timer' <<<"$cleanup_body" | tail -1 | cut -d: -f1)
@@ -0,0 +1,199 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
children=()
cleanup() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup EXIT
# All policy, state, locks and command mutations stay in this private fixture.
# Native visudo validates inert fragments; no test installs host sudo policy.
mkdir -p "$test_tmp/bin" "$test_tmp/state" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock"
export TEST_GRANT_ROOT="$test_tmp"
cat >"$test_tmp/bin/stat" <<'STUB'
#!/bin/bash
case $2 in
'%u') printf '0\n' ;;
'%a') if [[ -d ${@: -1} ]]; then printf '755\n'; else printf '644\n'; fi ;;
'%u %a') if [[ -d ${@: -1} ]]; then printf '0 755\n'; else printf '0 644\n'; fi ;;
*) exec /usr/bin/stat "$@" ;;
esac
STUB
cat >"$test_tmp/bin/install" <<'STUB'
#!/bin/bash
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
STUB
cat >"$test_tmp/bin/rm" <<'STUB'
#!/bin/bash
for path in "$@"; do
if [[ ${TEST_FAIL_TEMP_CLEANUP:-0} == 1 && $path == "$TEST_GRANT_ROOT/state/".sudoers.* ]]; then exit 1; fi
if [[ ${TEST_FAIL_RULE_DELETE:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/"* ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
STUB
cat >"$test_tmp/bin/systemctl" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_GRANT_ROOT/systemctl.log"
exit 0
STUB
chmod +x "$test_tmp/bin/"*
library="$test_tmp/grant-functions.sh"
{
printf 'source %q\n' "$ROOT/bin/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$ROOT/bin/omarchy-sudo-passwordless"
} | sed \
-e "s|/var/lib/omarchy/sudo-passwordless|$test_tmp/state|g" \
-e "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|g" \
-e "s|/etc/tmpfiles.d|$test_tmp/etc/tmpfiles.d|g" \
-e "s|/run/lock/omarchy-sudo-passwordless.lock|$test_tmp/run/lock/omarchy-sudo-passwordless.lock|g" \
-e "s|/run/omarchy-sudo-passwordless-package-removing|$test_tmp/run/omarchy-sudo-passwordless-package-removing|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/install|$test_tmp/bin/install|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
-e "s|/usr/bin/systemctl|$test_tmp/bin/systemctl|g" \
-e 's|/usr/bin/chown|/usr/bin/true|g' >"$library"
printf 'r! /etc/sudoers.d/99-omarchy-nopasswd-*\n' >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
# The expected policy text is mapped along with its filename in this fixture.
sed -i "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|" "$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
(
source "$library"
for name in 'buildbot$' audituser aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
valid_account_name "$name" || fail "supported account name rejected: $name"
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$name" >"$test_tmp/name-policy"
/usr/sbin/visudo -cf "$test_tmp/name-policy" >/dev/null
done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
! valid_account_name "$name" || fail "invalid account name accepted"
done
! valid_uid 18446744073709551617 || fail "overflowed UID accepted"
printf 'buildbot$ ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot$"
remove_known_legacy_rules
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot\$ ]]
) || fail "supported account names and legacy cleanup disagree"
pass "provisioning-compatible names validate as sudoers and clean up correctly"
transaction_setup() {
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
start_expiry_timer() { printf '%s\n' "$3" >>"$test_tmp/armed"; }
stop_timer() { printf '%s\n' "$1" >>"$test_tmp/stopped"; }
}
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) && ! -e $(state_file 1000) && -s $test_tmp/stopped ]]
) || fail "post-publication cleanup failure did not revoke before timer cleanup"
pass "failed temporary cleanup after publication revokes the live policy"
rm -f "$test_tmp/stopped"
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 TEST_FAIL_RULE_DELETE=1 enable_locked 1000 15 && exit 1
[[ -f $(rule_file 1000) && -f $(state_file 1000) && ! -e $test_tmp/stopped ]]
if TEST_FAIL_RULE_DELETE=1 revoke_inactive_grant 1000; then exit 1; else status=$?; fi
(( status == 2 ))
) || fail "failed policy revocation disarmed expiry or claimed inactive status"
pass "failed revocation preserves expiry jobs and returns a distinct error"
(
source "$library"
transaction_setup
current_timer=$(read_state_timer 1000)
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ -f $(rule_file 1000) ]]
expire_locked 1000
[[ -f $(rule_file 1000) ]]
expire_locked 1000 "$current_timer"
[[ ! -e $(rule_file 1000) ]]
) || fail "a predecessor timer invalidates its replacement"
pass "old and legacy timer callbacks preserve a newer valid grant"
(
source "$library"
transaction_setup
start_expiry_timer() {
: >"$REMOVAL_BLOCKER"
return 0
}
enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) ]]
) || fail "publication ignores a lost package prerequisite"
rm "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
pass "grant publication rechecks package availability after timer setup"
pkgs_path=${OMARCHY_PKGS_PATH:-$ROOT/../omarchy-pkgs}
[[ ! -d $pkgs_path/pkgbuilds ]] || pkgs_path=$pkgs_path/pkgbuilds
package_script="$pkgs_path/omarchy-settings/omarchy-settings.install"
[[ -f $package_script ]] || fail "package checkout is required for shared lifecycle coverage"
sed -e "s|/etc/|$test_tmp/etc/|g" \
-e "s|/run|$test_tmp/run|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" "$package_script" >"$test_tmp/package.install"
worker="$test_tmp/publisher.sh"
{
printf '#!/bin/bash\nset -euo pipefail\nsource %q\n' "$library"
declare -f transaction_setup
printf 'test_tmp=%q\ntransaction_setup\n' "$test_tmp"
cat <<'WORKER'
publish_rule() {
: >"$test_tmp/publisher.entered"
while [[ ! -e $test_tmp/publisher.release ]]; do sleep 0.02; done
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file "$1")"
}
with_root_lock enable_locked 1000 15
WORKER
} >"$worker"
bash "$worker" >"$test_tmp/publisher.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/publisher.entered ]] || break
sleep 0.02
done
[[ -e $test_tmp/publisher.entered ]] || fail "grant publisher did not enter the shared lock"
bash -euo pipefail -c 'source "$1"; : >"$2"; pre_remove; post_remove' bash \
"$test_tmp/package.install" "$test_tmp/removal.started" >"$test_tmp/removal.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/removal.started ]] || break
sleep 0.02
done
[[ -e $test_tmp/removal.started ]] || fail "package removal did not start"
touch "$test_tmp/publisher.release"
for child in "${children[@]}"; do wait "$child" || fail "shared lifecycle worker failed"; done
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "removal left a concurrently published grant"
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not block later publication"
(
source "$library"
transaction_setup
! with_root_lock enable_locked 1000 15
) || fail "a publisher can create a grant after package removal begins"
pass "package removal shares the grant lock and blocks later publication"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
if TEST_FAIL_RULE_DELETE=1 bash -euo pipefail -c 'source "$1"; post_remove' bash "$test_tmp/package.install" >"$test_tmp/removal-failure.output" 2>&1; then
fail "package removal hid a failed policy deletion"
fi
grep -q 'Administrator cleanup is required' "$test_tmp/removal-failure.output" || fail "package deletion failure lacks recovery guidance"
pass "package removal reports cleanup failures instead of successful revocation"