Pin trusted PATH in privileged DNS helper (#8172)

* 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>
This commit is contained in:
Mehmet INCE
2026-08-25 09:37:55 +02:00
committed by GitHub
co-authored by Claude Opus 5 Codex XHigh David Heinemeier Hansson
parent 68ab12f77d
commit 4637735aa2
2 changed files with 66 additions and 0 deletions
+12
View File
@@ -6,6 +6,18 @@
set -euo pipefail
# Whenever this runs as root — invoked directly through the passwordless
# sudoers rule, or re-execed by require_root below — sudo's secure_path decides
# where a bare helper resolves, and a dev link (etc/sudoers.d/omarchy-dev-path)
# prepends a user-writable checkout bin/ to it. Every helper this script calls
# by bare name (dirname, install, tee, rm, nmcli, systemctl, awk) is a system
# tool, never an omarchy-* command, so pin PATH to trusted system directories
# and keep root from resolving one out of that checkout. The unprivileged
# wrapper phase keeps the caller's PATH so it can still find sudo/pkexec.
if (( EUID == 0 )); then
export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin
fi
NM_DNS_CONF=/etc/NetworkManager/conf.d/20-omarchy-dns.conf
provider_from_arg() {
+54
View File
@@ -30,6 +30,60 @@ grep -E 'sudo -n -l -l' "$dns" >/dev/null ||
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