Ask for the sudo password once per omarchy update (#13323)

* Ask for the sudo password once per omarchy update

Every sudo call in omarchy update prompted, because the no-update wrapper
covered the whole run on top of per-phase revokes, and stay-awake revoked
the timestamp on its own entry and exit. A single update could ask four
times before the snapshot finished (#13319).

Authorize once, right after confirmation, starting from a revoked
timestamp so the prompt always belongs to this update. A background
keepalive refreshes it until the update is done. Prune, snapshot,
stay-awake, keyring, system packages, migrations, orphan removal, service
restarts, the post-update hook, and mise all share that authorization.

AUR builds run third-party PKGBUILD code, so they move to the end and run
cold: the keepalive stops, the timestamp is revoked, and yay and any bare
sudo use the no-update wrapper. The timestamp is revoked again after AUR
and on every exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the single authorization for passwordless sudo and ttyless inhibition

Authorize by running a command instead of sudo -v. Under the default
verifypw=all, -v prompts even when passwordless sudo is enabled, which
would have added a prompt those users never had.

Inside an update without a terminal, stay-awake now reuses the update's
authorization with a non-interactive sudo instead of asking again through
polkit. It falls back to polkit only if that authorization is gone.

The test sudo refuses a cold non-interactive call, as the real one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier HanssonandClaude Opus 5.5 authored and GitHub committed 2026-09-26 15:31:40 +02:00
1 parent 7b336b1b0d
commit e1614f2bdb
9 files changed
+163 -51

No files matched your search

+58 -21
View File
@@ -18,20 +18,62 @@ set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# Logging and lock acquisition re-exec this command with a sanitized PATH.
# Preserve the caller's path only for the later unprivileged hook/mise phases.
# Preserve the caller's path only for the later hook/mise phases.
user_path=${OMARCHY_UPDATE_USER_PATH:-$PATH}
unset OMARCHY_UPDATE_USER_PATH
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
# exits through the cleanup path. Starting cold means the single password
# prompt below always belongs to this update, never to an earlier session.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
# Validate the no-update wrapper up front: the AUR phase depends on it. Every
# other phase shares the one authorization, so it stays off PATH until then.
omarchy_security_enable_no_update_sudo
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
unset OMARCHY_SUDO_NO_UPDATE
# Helpers that manage their own sudo boundary leave this authorization to us.
export PATH OMARCHY_UPDATE_SUDO_SESSION=1
update_stay_awake_stopped=0
sudo_keepalive_pid=""
# Ask for the password once, then keep sudo's timestamp fresh so long package
# downloads, migrations, hooks, and mise never outlast it and prompt again.
# Authorize by running a command rather than sudo -v: under the default
# verifypw=all, -v prompts even when passwordless sudo is enabled.
authorize_update() {
/usr/bin/sudo /usr/bin/true || return 1
local update_pid=$$
(
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
sleep_pid=""
trap - EXIT HUP INT
trap '[[ -z $sleep_pid ]] || kill "$sleep_pid" 2>/dev/null; exit 0' TERM
while :; do
sleep 60 &
sleep_pid=$!
wait "$sleep_pid"
kill -0 "$update_pid" 2>/dev/null || exit 0
/usr/bin/sudo -n /usr/bin/true 2>/dev/null || exit 0
done
) &
sudo_keepalive_pid=$!
}
end_update_authorization() {
if [[ -n $sudo_keepalive_pid ]]; then
kill "$sudo_keepalive_pid" 2>/dev/null || true
wait "$sudo_keepalive_pid" 2>/dev/null || true
sudo_keepalive_pid=""
fi
omarchy_security_revoke_sudo_timestamp
}
cleanup_update() {
local status=$?
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
if ! end_update_authorization; then
echo "Could not invalidate sudo before update cleanup." >&2
omarchy_security_exit_with_revoked_sudo 1
fi
@@ -61,6 +103,8 @@ omarchy-update-requires-free-space
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
authorize_update
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
# after it frees nothing until that snapshot ages out.
omarchy-update-pkg-prune
@@ -82,33 +126,26 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
# takes the update with it rather than migrating against what is still on disk.
omarchy-update-system-pkgs
# Historical migrations are strictly ordered and mix user hooks/downloaded
# tooling with privileged repairs. The no-update sudo wrapper has covered the
# whole update, so neither the package transaction nor a later repair can
# publish a timestamp to a detached migration child.
omarchy_security_revoke_sudo_timestamp
omarchy-migrate
omarchy-update-orphan-pkgs
omarchy-update-analyze-logs
omarchy-update-status
# Service restart helpers can need sudo. Run them before any user-controlled
# update tooling; the reboot-only phase below performs no privileged work.
# Service restart helpers can need sudo. The reboot-only phase below
# performs no privileged work.
omarchy-update-restart --services-only
# AUR package installation must also use the no-update wrapper. Finish
# update-owned system work before build code, hooks, or mise can run.
omarchy_security_revoke_sudo_timestamp
omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# Hooks and mise run with the caller's PATH so user-installed tools resolve.
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
# Hooks and mise execute user-controlled code. Give each a cold credential
# boundary and run mise last so it cannot wait for a legitimate hook sudo.
# Only the unprivileged reboot prompt follows them.
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
# AUR builds run third-party PKGBUILD code, so they go last and never see the
# update's authorization: revoke it, then authenticate each sudo call with
# the no-update wrapper, for yay and any bare sudo a PKGBUILD runs, so an AUR
# install prompts without caching anything.
end_update_authorization
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it
+14 -3
View File
@@ -15,6 +15,13 @@ omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# omarchy update authorizes once and revokes when it is done. Revoking here
# would throw that away and prompt again for the rest of the update.
update_sudo_session=0
if [[ ${OMARCHY_UPDATE_SUDO_SESSION:-} == "1" ]]; then
update_sudo_session=1
omarchy_security_revoke_sudo_timestamp() { :; }
fi
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
@@ -504,11 +511,15 @@ start_locked() {
exec "${hold_command[@]}"
) &
launcher_pid=$!
elif [[ -t 0 ]]; then
elif [[ -t 0 ]] || { (( update_sudo_session )) && /usr/bin/sudo -n -N /usr/bin/true 2>/dev/null; }; then
# Inside an update without a terminal, reuse its authorization
# non-interactively rather than asking again through polkit.
sudo_options=(-N -b)
[[ -t 0 ]] || sudo_options=(-n -N -b)
if ! (
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec {state_lock_fd}>&-
exec /usr/bin/sudo -N -b -- "${hold_command[@]}"
exec /usr/bin/sudo "${sudo_options[@]}" -- "${hold_command[@]}"
); then
return 1
fi
@@ -535,7 +546,7 @@ start_locked() {
return 1
fi
(( readiness_attempts += 1 ))
if [[ -t 0 ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
if [[ -z $launcher_pid ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
echo "The update sleep inhibitor did not become ready." >&2
return 1
fi