Harden CUPS printer discovery
Run cups-browsed as a locked service account with a dedicated cache and a focused systemd sandbox. Restrict automatic queues to driverless IPP printers, remove wheel from passwordless CUPS administration, replace cups-pdf with Polkit-backed setup, and migrate existing systems safely. Reported-By: Erik Hunstad (Bad Sector Labs) Co-Authored-By: Daybreak Blue <noreply@openai.com>
This commit is contained in:
co-authored by
Daybreak Blue
parent
946704f309
commit
5c336885d2
@@ -1,4 +1,8 @@
|
||||
# Omarchy override of cups-browsed's shipped config. The only behavioural
|
||||
# change vs the upstream default (all-commented) is enabling auto-registration
|
||||
# of remote IPP printers discovered via Avahi/mDNS.
|
||||
CreateRemotePrinters Yes
|
||||
# Keep state away from /var/cache/cups, which is writable by the account CUPS
|
||||
# uses for print filters. cups-browsed is the only writer to this directory.
|
||||
CacheDir /var/cache/cups-browsed
|
||||
|
||||
# Auto-create queues only for modern driverless IPP printers. Remote queues
|
||||
# exported by another CUPS server can still be added manually when needed.
|
||||
CreateIPPPrinterQueues Driverless
|
||||
CreateRemoteCUPSPrinterQueues No
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[Service]
|
||||
User=cups-browsed
|
||||
Group=cups-browsed
|
||||
CacheDirectory=cups-browsed
|
||||
CacheDirectoryMode=0750
|
||||
UMask=0027
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
PrivateTmp=yes
|
||||
RestrictSUIDSGID=yes
|
||||
@@ -0,0 +1 @@
|
||||
u cups-browsed - "CUPS printer discovery" / -
|
||||
@@ -7,5 +7,6 @@ run_logged "$OMARCHY_INSTALL/config/ssh-keepalive.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/docker.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/snapper.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/locate.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/printing.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/enable-services.sh"
|
||||
run_logged "$OMARCHY_INSTALL/config/firewall.sh"
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# cups-browsed manages queues through CUPS and does not need Unix root. Give
|
||||
# only its locked service account passwordless CUPS administration; interactive
|
||||
# users go through cups-pk-helper and Polkit instead.
|
||||
cups_files_conf="${OMARCHY_CUPS_FILES_CONF:-/etc/cups/cups-files.conf}"
|
||||
cups_browsed_sysusers_conf="${OMARCHY_CUPS_BROWSED_SYSUSERS_CONF:-/etc/sysusers.d/omarchy-cups-browsed.conf}"
|
||||
|
||||
if [[ -f $cups_browsed_sysusers_conf ]]; then
|
||||
systemd-sysusers "$cups_browsed_sysusers_conf"
|
||||
fi
|
||||
|
||||
if [[ -L $cups_files_conf ]]; then
|
||||
echo "Refusing to rewrite symlinked CUPS authorization config: $cups_files_conf" >&2
|
||||
false
|
||||
elif [[ -f $cups_files_conf ]]; then
|
||||
staged_conf=$(mktemp --tmpdir="${cups_files_conf%/*}" ".${cups_files_conf##*/}.XXXXXX")
|
||||
|
||||
if ! awk '
|
||||
NR == FNR {
|
||||
if ($1 == "SystemGroup") {
|
||||
for (i = 2; i <= NF; i++) {
|
||||
if (substr($i, 1, 1) == "#")
|
||||
break
|
||||
if ($i != "wheel" && !seen_group[$i]) {
|
||||
system_groups[++system_group_count] = $i
|
||||
seen_group[$i] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
$1 == "SystemGroup" {
|
||||
comment_start = index($0, "#")
|
||||
if (!wrote_system_group) {
|
||||
printf "SystemGroup"
|
||||
for (i = 1; i <= system_group_count; i++)
|
||||
printf " %s", system_groups[i]
|
||||
if (!seen_group["cups-browsed"])
|
||||
printf " cups-browsed"
|
||||
if (comment_start)
|
||||
printf " %s", substr($0, comment_start)
|
||||
print ""
|
||||
wrote_system_group = 1
|
||||
} else if (comment_start) {
|
||||
print substr($0, comment_start)
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
$1 == "PeerCred" {
|
||||
comment_start = index($0, "#")
|
||||
if (!saw_peer_cred) {
|
||||
printf "PeerCred on"
|
||||
if (comment_start)
|
||||
printf " %s", substr($0, comment_start)
|
||||
print ""
|
||||
} else if (comment_start) {
|
||||
print substr($0, comment_start)
|
||||
}
|
||||
saw_peer_cred = 1
|
||||
next
|
||||
}
|
||||
|
||||
{ print }
|
||||
|
||||
END {
|
||||
if (!wrote_system_group)
|
||||
print "SystemGroup sys root cups-browsed"
|
||||
if (!saw_peer_cred)
|
||||
print "PeerCred on"
|
||||
}
|
||||
' "$cups_files_conf" "$cups_files_conf" >"$staged_conf"; then
|
||||
rm -f "$staged_conf"
|
||||
false
|
||||
fi
|
||||
|
||||
if ! chmod --reference="$cups_files_conf" "$staged_conf" ||
|
||||
! chown --reference="$cups_files_conf" "$staged_conf" ||
|
||||
! mv -f "$staged_conf" "$cups_files_conf"; then
|
||||
rm -f "$staged_conf"
|
||||
false
|
||||
fi
|
||||
fi
|
||||
@@ -19,7 +19,7 @@ cliamp
|
||||
cups
|
||||
cups-browsed
|
||||
cups-filters
|
||||
cups-pdf
|
||||
cups-pk-helper
|
||||
ddcutil
|
||||
docker
|
||||
docker-buildx
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
echo "Separate printer discovery from root and print-filter access"
|
||||
|
||||
machine_marker="${OMARCHY_CUPS_MIGRATION_MARKER:-/var/lib/omarchy/migrations/1787815267}"
|
||||
|
||||
[[ ! -e $machine_marker ]] || exit 0
|
||||
|
||||
# CUPS-PDF accepts a job-controlled post-processing command in a backend that
|
||||
# CUPS launches as root. Native application print-to-file support replaces it.
|
||||
omarchy-pkg-drop cups-pdf
|
||||
|
||||
# system-config-printer uses this helper to request printer administration
|
||||
# through Polkit now that the desktop user's wheel group is no longer @SYSTEM.
|
||||
if omarchy-pkg-present cups; then
|
||||
omarchy-pkg-add cups-pk-helper
|
||||
fi
|
||||
|
||||
cups_browsed_was_active=0
|
||||
if systemctl is-active --quiet cups-browsed.service 2>/dev/null; then
|
||||
cups_browsed_was_active=1
|
||||
sudo systemctl stop cups-browsed.service
|
||||
fi
|
||||
|
||||
if omarchy-pkg-present cups; then
|
||||
sudo env OMARCHY_PATH="$OMARCHY_PATH" \
|
||||
bash -euo pipefail "$OMARCHY_PATH/install/config/printing.sh"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl try-reload-or-restart cups.service
|
||||
fi
|
||||
|
||||
if (( cups_browsed_was_active )) && omarchy-pkg-present cups-browsed; then
|
||||
sudo systemctl restart cups-browsed.service
|
||||
fi
|
||||
|
||||
sudo install -Dm644 /dev/null "$machine_marker"
|
||||
@@ -64,6 +64,72 @@ verify_services() {
|
||||
pass "user audio services are running"
|
||||
}
|
||||
|
||||
verify_printing_security() {
|
||||
local cups_browsed_pid lpinfo_output printer_name printer_process printer_tmp
|
||||
|
||||
! pacman -Q cups-pdf >/dev/null 2>&1 || fail "CUPS-PDF is absent"
|
||||
pass "the root CUPS-PDF backend is not installed"
|
||||
|
||||
getent passwd cups-browsed >/dev/null || fail "the cups-browsed service account exists"
|
||||
[[ $(systemctl show -P User cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service account"
|
||||
[[ $(systemctl show -P Group cups-browsed.service) == "cups-browsed" ]] ||
|
||||
fail "cups-browsed runs as its service group"
|
||||
systemctl is-active --quiet cups-browsed.service || fail "cups-browsed is running"
|
||||
|
||||
cups_browsed_pid=$(systemctl show -P MainPID cups-browsed.service)
|
||||
[[ -r /proc/$cups_browsed_pid/status ]] || fail "cups-browsed has a readable process status"
|
||||
[[ $(awk '/^Uid:/{print $2}' "/proc/$cups_browsed_pid/status") != 0 ]] ||
|
||||
fail "cups-browsed does not run with root UID"
|
||||
[[ $(awk '/^CapEff:/{print $2}' "/proc/$cups_browsed_pid/status") == "0000000000000000" ]] ||
|
||||
fail "cups-browsed has no effective Linux capabilities"
|
||||
|
||||
[[ $(stat -c '%a %U:%G' /var/cache/cups-browsed) == "750 cups-browsed:cups-browsed" ]] ||
|
||||
fail "cups-browsed has an isolated cache" "$(stat -c '%a %U:%G' /var/cache/cups-browsed)"
|
||||
! id -nG cups-browsed | grep -qw cups ||
|
||||
fail "cups-browsed is separate from the print-filter group"
|
||||
|
||||
if lpinfo_output=$(LC_ALL=C timeout 10 lpinfo -v </dev/null 2>&1); then
|
||||
fail "the desktop user cannot administer CUPS without authentication"
|
||||
elif [[ $lpinfo_output != *"Forbidden"* ]]; then
|
||||
fail "CUPS explicitly denies unauthenticated desktop administration" "$lpinfo_output"
|
||||
fi
|
||||
|
||||
pass "CUPS discovery is isolated from root, filters, and passwordless desktop administration"
|
||||
|
||||
# A live driverless printer proves the non-root daemon can still discover and
|
||||
# create queues without the CAP_NET_BIND_SERVICE Ubuntu carries downstream.
|
||||
printer_name="OmarchyAcceptancePrinter"
|
||||
printer_tmp=$(mktemp -d)
|
||||
printf '#!/bin/bash\nexit 0\n' >"$printer_tmp/command"
|
||||
chmod 0700 "$printer_tmp/command"
|
||||
mkdir -m 0700 "$printer_tmp/spool"
|
||||
|
||||
ippeveprinter -p 18631 -d "$printer_tmp/spool" -c "$printer_tmp/command" "$printer_name" \
|
||||
>"$printer_tmp/ippeveprinter.log" 2>&1 &
|
||||
printer_process=$!
|
||||
|
||||
printing_test_cleanup() {
|
||||
kill "$printer_process" >/dev/null 2>&1 || true
|
||||
wait "$printer_process" >/dev/null 2>&1 || true
|
||||
rm -rf "$printer_tmp"
|
||||
}
|
||||
trap printing_test_cleanup EXIT
|
||||
|
||||
for _ in {1..30}; do
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
lpstat -v "$printer_name" 2>/dev/null | grep -q "implicitclass://$printer_name/" ||
|
||||
fail "non-root cups-browsed discovers a driverless IPP printer" "$(<"$printer_tmp/ippeveprinter.log")"
|
||||
|
||||
printing_test_cleanup
|
||||
trap - EXIT
|
||||
|
||||
pass "non-root cups-browsed still creates driverless IPP queues without capabilities"
|
||||
}
|
||||
|
||||
verify_runtime_tools() {
|
||||
# Docker access is intentionally NOT granted to the desktop user: the docker
|
||||
# group is root-equivalent, so a rogue process running as the user could
|
||||
@@ -107,7 +173,7 @@ verify_user_setup() {
|
||||
pass "Omarchy user state and shell configuration exist"
|
||||
}
|
||||
|
||||
for check in verify_core_packages verify_defaults verify_services verify_runtime_tools verify_user_setup; do
|
||||
for check in verify_core_packages verify_defaults verify_services verify_printing_security verify_runtime_tools verify_user_setup; do
|
||||
if ! ("$check"); then
|
||||
status=1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
packages="$ROOT/install/omarchy-base.packages"
|
||||
cups_browsed_conf="$ROOT/etc/cups/cups-browsed.conf"
|
||||
sysusers_conf="$ROOT/etc/sysusers.d/omarchy-cups-browsed.conf"
|
||||
service_dropin="$ROOT/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf"
|
||||
|
||||
grep -qxF cups-browsed "$packages" || fail "cups-browsed remains in the base package set"
|
||||
grep -qxF cups-pk-helper "$packages" || fail "Polkit printer administration is installed"
|
||||
! grep -qxF cups-pdf "$packages" || fail "the root CUPS-PDF backend is removed"
|
||||
|
||||
pass "the base install keeps discovery and replaces CUPS-PDF with Polkit administration"
|
||||
|
||||
grep -qxF 'CacheDir /var/cache/cups-browsed' "$cups_browsed_conf" ||
|
||||
fail "cups-browsed keeps state outside the print-filter cache"
|
||||
grep -qxF 'CreateIPPPrinterQueues Driverless' "$cups_browsed_conf" ||
|
||||
fail "automatic queues are limited to driverless IPP printers"
|
||||
grep -qxF 'CreateRemoteCUPSPrinterQueues No' "$cups_browsed_conf" ||
|
||||
fail "remote CUPS queues are not created automatically"
|
||||
! grep -q 'CreateRemotePrinters' "$cups_browsed_conf" ||
|
||||
fail "the unsupported CreateRemotePrinters directive is gone"
|
||||
|
||||
pass "cups-browsed uses explicit supported discovery policy and an isolated cache"
|
||||
|
||||
grep -qxF 'u cups-browsed - "CUPS printer discovery" / -' "$sysusers_conf" ||
|
||||
fail "a locked cups-browsed system account is declared"
|
||||
|
||||
for setting in \
|
||||
'User=cups-browsed' \
|
||||
'Group=cups-browsed' \
|
||||
'CacheDirectory=cups-browsed' \
|
||||
'CacheDirectoryMode=0750' \
|
||||
'UMask=0027' \
|
||||
'NoNewPrivileges=yes' \
|
||||
'ProtectSystem=strict' \
|
||||
'ProtectHome=yes' \
|
||||
'PrivateTmp=yes' \
|
||||
'RestrictSUIDSGID=yes'; do
|
||||
grep -qxF "$setting" "$service_dropin" ||
|
||||
fail "cups-browsed service hardening includes $setting"
|
||||
done
|
||||
|
||||
! grep -q '^\(Ambient\|CapabilityBoundingSet\).*CAP_NET_BIND_SERVICE' "$service_dropin" ||
|
||||
fail "cups-browsed is not granted an unverified network capability"
|
||||
|
||||
pass "cups-browsed runs as its confined service account without added capabilities"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
mkdir -p "$mock_bin" "$test_tmp/etc/cups" "$test_tmp/var/lib/omarchy/migrations"
|
||||
|
||||
cat >"$mock_bin/systemd-sysusers" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'sysusers\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
SH
|
||||
cat >"$mock_bin/chown" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'chown\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
SH
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == "cups" || $1 == "cups-browsed" ]]
|
||||
SH
|
||||
for command in omarchy-pkg-add omarchy-pkg-drop; do
|
||||
cat >"$mock_bin/$command" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\t%s\n' "${0##*/}" "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
SH
|
||||
done
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
exit 0
|
||||
SH
|
||||
cat >"$mock_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
printf 'sudo\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG"
|
||||
exec "$@"
|
||||
SH
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
authorization_conf="$test_tmp/etc/cups/cups-files.conf"
|
||||
cat >"$authorization_conf" <<'CONF'
|
||||
# Keep this custom preamble.
|
||||
SystemGroup sys root wheel custom-admin wheel # Keep this inline comment.
|
||||
SystemGroup wheel print-operators # Keep this second inline comment.
|
||||
PeerCred off # Keep this PeerCred comment.
|
||||
PeerCred off # Keep this second PeerCred comment.
|
||||
CONF
|
||||
|
||||
log="$test_tmp/actions.log"
|
||||
export OMARCHY_CUPS_TEST_LOG="$log"
|
||||
|
||||
run_printing_setup() {
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_CUPS_FILES_CONF="$authorization_conf" \
|
||||
OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \
|
||||
bash -euo pipefail "$ROOT/install/config/printing.sh"
|
||||
}
|
||||
|
||||
run_printing_setup
|
||||
|
||||
grep -qxF 'SystemGroup sys root custom-admin print-operators cups-browsed # Keep this inline comment.' "$authorization_conf" ||
|
||||
fail "printing setup reserves CUPS administration for the service account"
|
||||
grep -qxF '# Keep this second inline comment.' "$authorization_conf" ||
|
||||
fail "printing setup preserves comments from consolidated SystemGroup directives"
|
||||
grep -qxF 'PeerCred on # Keep this PeerCred comment.' "$authorization_conf" ||
|
||||
fail "printing setup enables peer credentials for the service account"
|
||||
grep -qxF '# Keep this second PeerCred comment.' "$authorization_conf" ||
|
||||
fail "printing setup preserves comments from duplicate PeerCred directives"
|
||||
grep -qxF '# Keep this custom preamble.' "$authorization_conf" ||
|
||||
fail "printing setup preserves unrelated CUPS configuration"
|
||||
[[ $(grep -c '^SystemGroup ' "$authorization_conf") == 1 ]] ||
|
||||
fail "printing setup emits one SystemGroup directive"
|
||||
|
||||
cp "$authorization_conf" "$test_tmp/first-run.conf"
|
||||
run_printing_setup
|
||||
cmp -s "$authorization_conf" "$test_tmp/first-run.conf" ||
|
||||
fail "printing setup is idempotent"
|
||||
|
||||
pass "printing setup narrows CUPS authorization without clobbering other configuration"
|
||||
|
||||
ln -s "$authorization_conf" "$test_tmp/etc/cups/symlinked.conf"
|
||||
if PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_CUPS_FILES_CONF="$test_tmp/etc/cups/symlinked.conf" \
|
||||
OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \
|
||||
bash -euo pipefail "$ROOT/install/config/printing.sh" 2>/dev/null; then
|
||||
fail "printing setup refuses a symlinked authorization file"
|
||||
fi
|
||||
|
||||
pass "printing setup refuses to rewrite a symlinked privileged configuration"
|
||||
|
||||
marker="$test_tmp/var/lib/omarchy/migrations/1787815267"
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_FILES_CONF="$authorization_conf" \
|
||||
OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
|
||||
grep -qxF $'omarchy-pkg-drop\tcups-pdf' "$log" ||
|
||||
fail "the migration removes CUPS-PDF"
|
||||
grep -qxF $'omarchy-pkg-add\tcups-pk-helper' "$log" ||
|
||||
fail "the migration installs authenticated printer administration"
|
||||
grep -qxF $'systemctl\tstop cups-browsed.service' "$log" ||
|
||||
fail "the migration stops the root cups-browsed process before reconfiguration"
|
||||
grep -qxF $'systemctl\tdaemon-reload' "$log" ||
|
||||
fail "the migration reloads the hardened service"
|
||||
grep -qxF $'systemctl\ttry-reload-or-restart cups.service' "$log" ||
|
||||
fail "the migration applies narrowed CUPS authorization"
|
||||
grep -qxF $'systemctl\trestart cups-browsed.service' "$log" ||
|
||||
fail "the migration resumes an active cups-browsed service"
|
||||
[[ -f $marker ]] || fail "the migration records machine-wide completion"
|
||||
|
||||
actions_after_first_run=$(wc -l <"$log")
|
||||
PATH="$mock_bin:$PATH" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_CUPS_MIGRATION_MARKER="$marker" \
|
||||
bash -euo pipefail "$ROOT/migrations/1787815267.sh"
|
||||
[[ $(wc -l <"$log") == "$actions_after_first_run" ]] ||
|
||||
fail "the machine-wide migration repeats privileged work"
|
||||
|
||||
pass "the migration safely converts an active existing installation once"
|
||||
Reference in New Issue
Block a user