Bound sudo policy natively and guard expiry package transactions
This commit is contained in:
1 parent
4ae25cd4d2
commit
a6385e60b8
4 files changed
+106
-20
No files matched your search
@@ -28,6 +28,7 @@ readonly STATE_DIR=/var/lib/omarchy/sudo-passwordless
|
||||
readonly RUNTIME_DIR=/run/omarchy/sudo-passwordless
|
||||
readonly LOCK_FILE=/run/lock/omarchy-sudo-passwordless.lock
|
||||
readonly BOOT_CLEANUP_FILE=/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf
|
||||
readonly PACKAGE_HOOK=/usr/share/libalpm/hooks/05-omarchy-passwordless-revoke.hook
|
||||
readonly REMOVAL_BLOCKER=/run/omarchy-sudo-passwordless-package-removing
|
||||
readonly INSTALLED_SELF=/usr/bin/omarchy-sudo-passwordless
|
||||
readonly STATUS_INACTIVE=3
|
||||
@@ -182,7 +183,11 @@ classify_generated_rule() {
|
||||
|
||||
if [[ $suffix =~ ^[0-9]+$ ]]; then
|
||||
name=${contents%' ALL=(ALL) NOPASSWD: ALL'}
|
||||
valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]
|
||||
if valid_account_name "$name" && [[ $contents == "$name ALL=(ALL) NOPASSWD: ALL" ]]; then
|
||||
return 0
|
||||
fi
|
||||
name=${contents%%' ALL=(ALL) NOTAFTER='*}
|
||||
valid_account_name "$name" && [[ $contents =~ ^[a-z_][a-z0-9_-]*\$?\ ALL=\(ALL\)\ NOTAFTER=[0-9]{14}Z\ NOPASSWD:\ ALL$ ]]
|
||||
elif valid_account_name "$suffix" && [[ $contents == "$suffix ALL=(ALL) NOPASSWD: ALL" ]]; then
|
||||
GENERATED_RULE_LEGACY_TIMER="omarchy-nopasswd-expire-${suffix}"
|
||||
else
|
||||
@@ -256,17 +261,16 @@ cleanup_all_locked() {
|
||||
return "$failed"
|
||||
}
|
||||
|
||||
verify_boot_cleanup() {
|
||||
local owner mode canonical current active_rules
|
||||
[[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
|
||||
[[ -f $BOOT_CLEANUP_FILE && ! -L $BOOT_CLEANUP_FILE ]] || return 1
|
||||
canonical=$(/usr/bin/realpath -e -- "$BOOT_CLEANUP_FILE") || return 1
|
||||
[[ $canonical == "$BOOT_CLEANUP_FILE" ]] || return 1
|
||||
owner=$(/usr/bin/stat -Lc '%u' -- "$BOOT_CLEANUP_FILE") || return 1
|
||||
mode=$(/usr/bin/stat -Lc '%a' -- "$BOOT_CLEANUP_FILE") || return 1
|
||||
verify_root_policy_file() {
|
||||
local file=$1 owner mode canonical current
|
||||
[[ -f $file && ! -L $file ]] || return 1
|
||||
canonical=$(/usr/bin/realpath -e -- "$file") || return 1
|
||||
[[ $canonical == "$file" ]] || return 1
|
||||
owner=$(/usr/bin/stat -Lc '%u' -- "$file") || return 1
|
||||
mode=$(/usr/bin/stat -Lc '%a' -- "$file") || return 1
|
||||
[[ $owner == 0 && $mode =~ ^[0-7]+$ ]] && ! ((8#$mode & 022)) || return 1
|
||||
|
||||
current=${BOOT_CLEANUP_FILE%/*}
|
||||
current=${file%/*}
|
||||
while :; do
|
||||
[[ -d $current && ! -L $current ]] || return 1
|
||||
canonical=$(/usr/bin/realpath -e -- "$current") || return 1
|
||||
@@ -277,9 +281,36 @@ verify_boot_cleanup() {
|
||||
current=${current%/*}
|
||||
[[ -n $current ]] || current=/
|
||||
done
|
||||
}
|
||||
|
||||
verify_boot_cleanup() {
|
||||
local active_rules hook
|
||||
[[ ! -e $REMOVAL_BLOCKER && ! -L $REMOVAL_BLOCKER ]] || return 1
|
||||
verify_root_policy_file "$BOOT_CLEANUP_FILE" || return 1
|
||||
active_rules=$(/usr/bin/awk '!/^[[:space:]]*(#|$)/ { print }' "$BOOT_CLEANUP_FILE") || return 1
|
||||
[[ $active_rules == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]]
|
||||
[[ $active_rules == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] || return 1
|
||||
verify_root_policy_file "$PACKAGE_HOOK" || return 1
|
||||
hook=$(/usr/bin/cat -- "$PACKAGE_HOOK") || return 1
|
||||
[[ $hook == '[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Remove
|
||||
Type = Package
|
||||
Target = omarchy-settings
|
||||
Target = omarchy-settings-dev
|
||||
|
||||
[Action]
|
||||
Description = Revoking temporary Omarchy sudo grants before settings changes...
|
||||
When = PreTransaction
|
||||
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
|
||||
AbortOnFail' ]]
|
||||
}
|
||||
|
||||
package_removing_locked() {
|
||||
# ALPM must abort before removing the helper or boot cleanup if revocation
|
||||
# fails. The marker also blocks publication after this lock is released.
|
||||
(umask 077; : >"$REMOVAL_BLOCKER") || return 1
|
||||
/usr/bin/rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-* || return 1
|
||||
cleanup_all_locked
|
||||
}
|
||||
|
||||
prepare_state_file() {
|
||||
@@ -306,10 +337,13 @@ start_expiry_timer() {
|
||||
}
|
||||
|
||||
publish_rule() {
|
||||
local uid="$1" name="$2" destination tmp
|
||||
local uid="$1" name="$2" expires="$3" destination tmp deadline
|
||||
valid_expiry "$expires" || return 1
|
||||
deadline=$(/usr/bin/date -u -d "@$expires" +%Y%m%d%H%M%SZ) || return 1
|
||||
[[ $deadline =~ ^[0-9]{14}Z$ ]] || return 1
|
||||
destination=$(rule_file "$uid")
|
||||
tmp=$(/usr/bin/mktemp "$STATE_DIR/.sudoers.XXXXXX") || return 1
|
||||
if ! /usr/bin/printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$name" >"$tmp" ||
|
||||
if ! /usr/bin/printf '%s ALL=(ALL) NOTAFTER=%s NOPASSWD: ALL\n' "$name" "$deadline" >"$tmp" ||
|
||||
! /usr/bin/chown root:root "$tmp" || ! /usr/bin/chmod 0440 "$tmp" ||
|
||||
! /usr/sbin/visudo -cf "$tmp" >/dev/null ||
|
||||
! /usr/bin/install -o root -g root -m 0440 -- "$tmp" "$destination"; then
|
||||
@@ -339,7 +373,7 @@ enable_locked() {
|
||||
valid_minutes "$minutes" || return 1
|
||||
prepare_root_state || return 1
|
||||
verify_boot_cleanup || {
|
||||
echo "omarchy-sudo-passwordless: package-owned boot cleanup rule is missing or unsafe" >&2
|
||||
echo "omarchy-sudo-passwordless: package-owned boot cleanup or transaction hook is missing or unsafe" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -363,7 +397,7 @@ enable_locked() {
|
||||
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
|
||||
return 1
|
||||
fi
|
||||
if ! verify_boot_cleanup || ! publish_rule "$uid" "$ACCOUNT_NAME"; then
|
||||
if ! verify_boot_cleanup || ! publish_rule "$uid" "$ACCOUNT_NAME" "$expires"; then
|
||||
abort_enable_locked "$uid" "$timer" "$old_timer" "$pending_state"
|
||||
return 1
|
||||
fi
|
||||
@@ -464,12 +498,16 @@ root_dispatch() {
|
||||
(($# == 0)) && ((EUID == 0)) || return 1
|
||||
with_root_lock cleanup_all_locked
|
||||
;;
|
||||
__package-removing)
|
||||
(($# == 0)) && ((EUID == 0)) || return 1
|
||||
with_root_lock package_removing_locked
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
__status|__enable|__disable|__expire|__cleanup-all)
|
||||
__status|__enable|__disable|__expire|__cleanup-all|__package-removing)
|
||||
action=$1
|
||||
shift
|
||||
root_dispatch "$action" "$@"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Remove
|
||||
Type = Package
|
||||
Target = omarchy-settings
|
||||
Target = omarchy-settings-dev
|
||||
|
||||
[Action]
|
||||
Description = Revoking temporary Omarchy sudo grants before settings changes...
|
||||
When = PreTransaction
|
||||
Exec = /usr/bin/omarchy-sudo-passwordless __package-removing
|
||||
AbortOnFail
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## Grant lifecycle
|
||||
|
||||
Root state records the resolved account name, absolute expiry epoch and unique timer name. A calendar timer is armed and verified before the generated policy becomes active. Publication rechecks the package-owned boot cleanup before and after installing policy. Policy revocation must succeed before expiry jobs are stopped; a deletion error leaves those jobs armed and reports that administrator cleanup is required.
|
||||
Root state records the resolved account name, absolute expiry epoch and unique timer name. A calendar timer is armed and verified before the generated policy becomes active. The sudoers rule also embeds the same UTC deadline with `NOTAFTER`, so sudo independently rejects it after expiry even if timer cleanup is delayed. Publication rechecks the package-owned boot cleanup before and after installing policy. Policy revocation must succeed before expiry jobs are stopped; a deletion error leaves those jobs armed and reports that administrator cleanup is required.
|
||||
|
||||
An internal status result is `0` for an active, validated grant and `3` for confirmed inactive access. All other results are errors, including failed authentication and failed revocation. The user interface only offers a new grant after result `3`. It must not turn an inspection failure into a claim that no grant exists.
|
||||
|
||||
@@ -12,9 +12,9 @@ Each new expiry callback carries its timer identity. A delayed predecessor canno
|
||||
|
||||
## Package ownership
|
||||
|
||||
The packaging companion must put the publication/expiry command, `omarchy-security-functions` and `omarchy-nopasswd-sudo.conf` in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
|
||||
The packaging companion must put the publication/expiry command, `omarchy-security-functions` `omarchy-nopasswd-sudo.conf` and the pre-transaction revocation hook in the settings package together. Removing the desktop runtime alone must leave a working expiry command behind. Stable and development package pairs must transfer ownership in one transaction without duplicate files.
|
||||
|
||||
Before settings removal or upgrade, its scriptlet acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing policy. The marker prevents a waiting publisher from creating a new grant while package files change. A successful installation clears the marker only after boot cleanup exists. Failed scriptlet cleanup returns an error and prints recovery guidance; a package-manager scriptlet failure must not be represented as an automatic transaction rollback.
|
||||
Before settings removal or upgrade, the installed ALPM `PreTransaction` hook invokes the fixed `__package-removing` action, acquires the same grant lock, sets `/run/omarchy-sudo-passwordless-package-removing` and revokes existing policy. The marker prevents a waiting publisher from creating a new grant while package files change. A successful installation clears the marker only after boot cleanup exists. The hook uses `AbortOnFail` because a scriptlet failure alone does not abort pacman. The scriptlets repeat cleanup as a fallback for upgrades from older packages that have no installed hook. New grants require both the boot rule and hook before publication. Failed or interrupted transactions leave the marker set; retry the package transaction successfully before requesting another grant.
|
||||
|
||||
The runtime marker need not survive reboot: pre-removal revokes the old grants before package files disappear, and a new invocation independently verifies boot cleanup. Both root operations use fixed machine paths. The marker is not a user-controlled mode switch.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ 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"
|
||||
mkdir -p "$test_tmp/bin" "$test_tmp/state" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/hooks"
|
||||
export TEST_GRANT_ROOT="$test_tmp"
|
||||
cat >"$test_tmp/bin/stat" <<'STUB'
|
||||
#!/bin/bash
|
||||
@@ -60,6 +60,7 @@ library="$test_tmp/grant-functions.sh"
|
||||
-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|/usr/share/libalpm/hooks|$test_tmp/hooks|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" \
|
||||
@@ -68,6 +69,8 @@ library="$test_tmp/grant-functions.sh"
|
||||
-e "s|/usr/bin/systemctl|$test_tmp/bin/systemctl|g" \
|
||||
-e 's|/usr/bin/chown|/usr/bin/true|g' >"$library"
|
||||
|
||||
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
|
||||
|
||||
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"
|
||||
@@ -197,3 +200,36 @@ if TEST_FAIL_RULE_DELETE=1 bash -euo pipefail -c 'source "$1"; post_remove' bash
|
||||
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"
|
||||
|
||||
(
|
||||
source "$library"
|
||||
transaction_setup
|
||||
rm -f "$REMOVAL_BLOCKER"
|
||||
enable_locked 1000 5
|
||||
record=$(read_state_record 1000)
|
||||
expiry=${record#*$'\t'}
|
||||
expiry=${expiry%%$'\t'*}
|
||||
deadline=$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)
|
||||
[[ $(cat "$(rule_file 1000)") == "audituser ALL=(ALL) NOTAFTER=$deadline NOPASSWD: ALL" ]]
|
||||
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
|
||||
classify_generated_rule "$(rule_file 1000)"
|
||||
rm -f "$(state_file 1000)"
|
||||
remove_known_legacy_rules
|
||||
[[ ! -e $(rule_file 1000) ]]
|
||||
) || fail "native sudo deadline or state-independent bounded rule cleanup is incorrect"
|
||||
pass "sudo policy contains the same deadline and bounded orphan rules are recognized"
|
||||
|
||||
(
|
||||
source "$library"
|
||||
transaction_setup
|
||||
rm -f "$REMOVAL_BLOCKER"
|
||||
enable_locked 1000 5
|
||||
if TEST_FAIL_RULE_DELETE=1 package_removing_locked; then exit 1; fi
|
||||
[[ -f $REMOVAL_BLOCKER && -f $(rule_file 1000) ]]
|
||||
! enable_locked 1000 5
|
||||
package_removing_locked
|
||||
[[ ! -e $(rule_file 1000) ]]
|
||||
rm -f "$REMOVAL_BLOCKER" "$PACKAGE_HOOK"
|
||||
! enable_locked 1000 5
|
||||
) || fail "pre-transaction revocation error or missing hook does not prevent new grants"
|
||||
pass "package hook fails closed and grants require its installed policy"
|
||||
Reference in new issue
Block a user