* Pin PATH to trusted dirs when omarchy-dns holds root A dev link prepends a user-writable checkout bin/ to sudo's secure_path, so the passwordless `omarchy-dns Cloudflare` sudoers rule lets root resolve a bare helper (dirname, install, tee, nmcli, ...) out of that checkout — turning checkout-write access into arbitrary root execution. Pin PATH to trusted system directories once EUID is 0, leaving the unprivileged wrapper phase free to locate sudo/pkexec on the caller's PATH. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YUWoHbBoKMjsjV6X3nu1H5 * Assert the trusted-PATH pin is gated on root, not merely present The EUID assertion matched `(( EUID == 0 ))` anywhere in the file, and require_root has carried that exact test since long before the pin existed. Deleting the pin left the assertion passing, so it stood for nothing: a run with the pin neutered reached the behavioural probe with both greps green. Anchor on the unindented guard and require the pin to be the line it opens, which no other construct in the script satisfies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Skip the DNS trusted-PATH probe where user namespaces are unavailable `fail` ends the file, so a sandbox or hardened kernel that refuses unprivileged user namespaces did not just lose the probe — it took the two elevation assertions below it down as well, reporting a product defect where there was only a missing capability. The non-graphical suites are meant to run on any machine and treat a skip as a passing test, the way require_compositor and plugin-add-test.sh already do. Gate the probe on the namespace it needs and say so when it is absent; the static checks above and the elevation checks below run either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Codex XHigh <noreply@openai.com>
162 lines
6.7 KiB
Bash
Executable File
162 lines
6.7 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(dirname "$0")/base-test.sh"
|
|
|
|
dns="$ROOT/bin/omarchy-dns"
|
|
sudoers_file="$ROOT/etc/sudoers.d/omarchy-dns"
|
|
rule='%wheel ALL=(root) NOPASSWD: /usr/bin/omarchy-dns Cloudflare, /usr/bin/omarchy-dns Google, /usr/bin/omarchy-dns DHCP'
|
|
|
|
# Exactly one rule, matched whole. A second line -- or the same command with its
|
|
# arguments dropped, which sudoers reads as "any arguments" -- would widen the
|
|
# grant while leaving this line in place.
|
|
rules=$(grep -vE '^[[:space:]]*(#|$)' "$sudoers_file")
|
|
[[ $rules == "$rule" ]] ||
|
|
fail "dns sudoers file carries exactly the stock-provider rule and nothing else" "got: $rules"
|
|
|
|
if command -v visudo >/dev/null; then
|
|
visudo -cf "$sudoers_file" >/dev/null || fail "dns sudoers rule parses"
|
|
fi
|
|
|
|
grep -Fx 'PACKAGED_PATH=/usr/bin/omarchy-dns' "$dns" >/dev/null ||
|
|
fail "omarchy-dns elevates the path the sudoers rule names"
|
|
|
|
# `sudo -l` on its own answers whether a command is permitted, not whether it
|
|
# is passwordless, and Omarchy ships a blanket %wheel rule that permits
|
|
# everything. Only the long listing prints the matched entry's tags.
|
|
grep -E 'sudo -n -l -l' "$dns" >/dev/null ||
|
|
fail "omarchy-dns reads the grant from the long sudo listing"
|
|
|
|
pass "dns sudoers rule is scoped to the stock providers"
|
|
|
|
# The privileged half runs as root under sudo's secure_path, and a dev link
|
|
# (etc/sudoers.d/omarchy-dev-path) prepends a user-writable checkout bin/ to it.
|
|
# Every helper the script calls by bare name -- dirname, install, tee, nmcli,
|
|
# systemctl, awk -- is a system tool, so once it holds root the script pins PATH
|
|
# to trusted system directories and never resolves one of them out of the
|
|
# checkout. The unprivileged wrapper phase keeps the caller's PATH, which is why
|
|
# the pin is gated on EUID rather than set unconditionally.
|
|
grep -Eq '^\s*export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin' "$dns" ||
|
|
fail "omarchy-dns pins PATH to trusted system directories when it holds root"
|
|
# require_root carries its own `(( EUID == 0 ))`, so matching that text alone
|
|
# would pass with the pin deleted. Anchor on the unindented guard and require the
|
|
# pin to be the line it opens.
|
|
gated=$(grep -A1 -E '^if \(\( EUID == 0 \)\); then$' "$dns" || true)
|
|
[[ $gated == *"export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin"* ]] ||
|
|
fail "omarchy-dns gates the trusted-PATH pin on holding root"
|
|
|
|
# The no-argument path only reads DNS config, so exercise the privileged phase
|
|
# directly when the suite is root and as namespaced root otherwise. This reaches
|
|
# tr while EUID is 0 without giving an ordinary test run any host privileges.
|
|
root_runner=()
|
|
if (( EUID != 0 )); then
|
|
root_runner=(unshare --user --map-root-user)
|
|
fi
|
|
|
|
# A sandbox or a hardened kernel can refuse unprivileged user namespaces, and
|
|
# the non-graphical suites have to stay green on any machine -- a skip is a
|
|
# passing test. Only the runtime probe needs the namespace; the static checks
|
|
# above and the elevation checks below run either way.
|
|
if (( EUID == 0 )) || unshare --user --map-root-user true 2>/dev/null; then
|
|
poison_dir=$(mktemp -d)
|
|
poison_ran="$poison_dir/ran"
|
|
for helper in tr awk dirname install tee; do
|
|
cat >"$poison_dir/$helper" <<SH
|
|
#!/bin/bash
|
|
printf 'x' >"$poison_ran"
|
|
exec "/usr/bin/$helper" "\$@"
|
|
SH
|
|
chmod +x "$poison_dir/$helper"
|
|
done
|
|
|
|
if ! PATH="$poison_dir:$PATH" "${root_runner[@]}" bash "$dns" </dev/null >/dev/null 2>&1; then
|
|
rm -rf "$poison_dir"
|
|
fail "root omarchy-dns failed its read-only trusted-PATH probe"
|
|
fi
|
|
if [[ -e $poison_ran ]]; then
|
|
rm -rf "$poison_dir"
|
|
fail "root omarchy-dns resolved a bare helper from the front of PATH instead of a trusted system path"
|
|
fi
|
|
rm -rf "$poison_dir"
|
|
pass "root omarchy-dns resolves system helpers from a trusted PATH, not the invocation PATH"
|
|
else
|
|
pass "no unprivileged user namespace; skipping the root trusted-PATH probe"
|
|
fi
|
|
|
|
# require_root returns immediately for root, so the stubs below would not stand
|
|
# between the script and the host's real NetworkManager and resolved config.
|
|
if (( EUID == 0 )); then
|
|
pass "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
|
|
exit 0
|
|
fi
|
|
|
|
test_tmp=$(mktemp -d)
|
|
trap 'rm -rf "$test_tmp"' EXIT
|
|
|
|
stub_bin="$test_tmp/bin"
|
|
mkdir -p "$stub_bin"
|
|
|
|
# pkexec stands in for the exec at the end of require_root, so the DNS writes
|
|
# below it never run and real pkexec is never reached.
|
|
cat >"$stub_bin/pkexec" <<'SH'
|
|
#!/bin/bash
|
|
printf 'pkexec %s\n' "$*" >"$ELEVATION_LOG"
|
|
SH
|
|
chmod +x "$stub_bin/pkexec"
|
|
|
|
# The sudo stub plays both parts: it answers the passwordless probe from
|
|
# STUB_GRANTED, the providers etc/sudoers.d/omarchy-dns covers on this machine,
|
|
# and logs the elevation otherwise. STUB_GRANTED empty stands for an install
|
|
# whose omarchy-settings predates the file.
|
|
cat >"$stub_bin/sudo" <<'SH'
|
|
#!/bin/bash
|
|
if [[ $1 == -n && $2 == -l ]]; then
|
|
for granted in ${STUB_GRANTED-Cloudflare Google DHCP}; do
|
|
[[ ${!#} == "$granted" ]] || continue
|
|
echo " Options: !authenticate"
|
|
exit 0
|
|
done
|
|
echo " Matched: ${!#}"
|
|
exit 0
|
|
fi
|
|
printf 'sudo %s\n' "$*" >"$ELEVATION_LOG"
|
|
SH
|
|
chmod +x "$stub_bin/sudo"
|
|
|
|
elevation_for() {
|
|
: >"$test_tmp/elevation"
|
|
ELEVATION_LOG="$test_tmp/elevation" \
|
|
PATH="$stub_bin:$PATH" \
|
|
bash "$dns" "$1" </dev/null >/dev/null
|
|
cat "$test_tmp/elevation"
|
|
}
|
|
|
|
for provider in Cloudflare Google DHCP; do
|
|
elevation=$(elevation_for "$provider")
|
|
[[ $elevation == "sudo /usr/bin/omarchy-dns $provider" ]] ||
|
|
fail "omarchy-dns takes the passwordless sudo grant for $provider without a terminal" "got: $elevation"
|
|
done
|
|
|
|
pass "omarchy-dns elevates the stock providers through sudo, not polkit"
|
|
|
|
# A dev-linked checkout elevates the packaged path like everyone else, rather
|
|
# than handing sudo a path no rule can name and losing the grant.
|
|
dev_linked=$(OMARCHY_PATH="$test_tmp/checkout" elevation_for Cloudflare)
|
|
[[ $dev_linked == "sudo /usr/bin/omarchy-dns Cloudflare" ]] ||
|
|
fail "omarchy-dns elevates the system install wherever OMARCHY_PATH points" "got: $dev_linked"
|
|
|
|
custom=$(elevation_for Custom)
|
|
[[ $custom == "pkexec /usr/bin/omarchy-dns Custom" ]] ||
|
|
fail "omarchy-dns leaves Custom on the polkit path, since no sudoers rule covers it" "got: $custom"
|
|
|
|
# The grant is what makes sudo passwordless, so its absence -- an install still
|
|
# on an older omarchy-settings, or a user outside %wheel the rule cannot match
|
|
# -- has to route to polkit. Betting on sudo here leaves the panel's one-click
|
|
# toggle execing into a password prompt it has no terminal to show.
|
|
ungranted=$(STUB_GRANTED="" elevation_for Cloudflare)
|
|
[[ $ungranted == "pkexec /usr/bin/omarchy-dns Cloudflare" ]] ||
|
|
fail "omarchy-dns falls back to polkit where the sudoers grant is not installed" "got: $ungranted"
|
|
|
|
pass "omarchy-dns falls back to polkit wherever the grant does not reach"
|