From 5c336885d22f97e4cb00b2512eea5d3dba74f38e Mon Sep 17 00:00:00 2001 From: Mehmet Ince Date: Thu, 27 Aug 2026 17:43:39 +0100 Subject: [PATCH 1/5] 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 --- etc/cups/cups-browsed.conf | 12 +- .../cups-browsed.service.d/10-omarchy.conf | 11 ++ etc/sysusers.d/omarchy-cups-browsed.conf | 1 + install/config/all.sh | 1 + install/config/printing.sh | 83 +++++++++ install/omarchy-base.packages | 2 +- migrations/1787815267.sh | 34 ++++ test/acceptance.d/system-test.sh | 68 ++++++- test/shell.d/cups-hardening-test.sh | 169 ++++++++++++++++++ 9 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 etc/systemd/system/cups-browsed.service.d/10-omarchy.conf create mode 100644 etc/sysusers.d/omarchy-cups-browsed.conf create mode 100644 install/config/printing.sh create mode 100644 migrations/1787815267.sh create mode 100644 test/shell.d/cups-hardening-test.sh diff --git a/etc/cups/cups-browsed.conf b/etc/cups/cups-browsed.conf index ed1bdbad..19863e59 100644 --- a/etc/cups/cups-browsed.conf +++ b/etc/cups/cups-browsed.conf @@ -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 diff --git a/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf b/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf new file mode 100644 index 00000000..54107197 --- /dev/null +++ b/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf @@ -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 diff --git a/etc/sysusers.d/omarchy-cups-browsed.conf b/etc/sysusers.d/omarchy-cups-browsed.conf new file mode 100644 index 00000000..fa602c18 --- /dev/null +++ b/etc/sysusers.d/omarchy-cups-browsed.conf @@ -0,0 +1 @@ +u cups-browsed - "CUPS printer discovery" / - diff --git a/install/config/all.sh b/install/config/all.sh index 91256dc7..aa1044f1 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -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" diff --git a/install/config/printing.sh b/install/config/printing.sh new file mode 100644 index 00000000..b99e03c3 --- /dev/null +++ b/install/config/printing.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 diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 4097b645..f4c5e2ef 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -19,7 +19,7 @@ cliamp cups cups-browsed cups-filters -cups-pdf +cups-pk-helper ddcutil docker docker-buildx diff --git a/migrations/1787815267.sh b/migrations/1787815267.sh new file mode 100644 index 00000000..c5b89c2f --- /dev/null +++ b/migrations/1787815267.sh @@ -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" diff --git a/test/acceptance.d/system-test.sh b/test/acceptance.d/system-test.sh index 53b2c1d7..c5c8da5c 100644 --- a/test/acceptance.d/system-test.sh +++ b/test/acceptance.d/system-test.sh @@ -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 &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 diff --git a/test/shell.d/cups-hardening-test.sh b/test/shell.d/cups-hardening-test.sh new file mode 100644 index 00000000..7ba8fea3 --- /dev/null +++ b/test/shell.d/cups-hardening-test.sh @@ -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" From 521f1ae9acfd01a62d11ef8cd8b9479b2f9d6e9a Mon Sep 17 00:00:00 2001 From: omabot Date: Thu, 27 Aug 2026 19:45:27 +0200 Subject: [PATCH 2/5] Resume cups-browsed on whether it is enabled, not on whether it was running The migration recomputed whether cups-browsed was active at the start of every run, after an earlier run may already have stopped it. A run interrupted between that stop and the completion marker left the service down, and the retry that followed read it as inactive, skipped the restart, and wrote the marker anyway: printer discovery stayed off until the next reboot with no migration left to bring it back. Keying the restart to whether the unit is enabled survives the interruption, and reports not-enabled for a unit the user masked or disabled, which restarting would fail on and abort the migration short of its marker. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- migrations/1787815267.sh | 9 +++-- test/shell.d/cups-hardening-test.sh | 55 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/migrations/1787815267.sh b/migrations/1787815267.sh index c5b89c2f..31a8e797 100644 --- a/migrations/1787815267.sh +++ b/migrations/1787815267.sh @@ -14,9 +14,8 @@ if omarchy-pkg-present cups; then omarchy-pkg-add cups-pk-helper fi -cups_browsed_was_active=0 +# Stop the root-running daemon before changing the authorization it relies on. if systemctl is-active --quiet cups-browsed.service 2>/dev/null; then - cups_browsed_was_active=1 sudo systemctl stop cups-browsed.service fi @@ -27,7 +26,11 @@ if omarchy-pkg-present cups; then sudo systemctl try-reload-or-restart cups.service fi -if (( cups_browsed_was_active )) && omarchy-pkg-present cups-browsed; then +# Resume on whether the unit is enabled, not on whether it was running when this +# run started: an interrupted earlier run leaves it stopped, and a retry that +# recomputed that would skip the restart and still write the marker below. A +# masked or disabled unit reports not-enabled and is left alone. +if systemctl is-enabled --quiet cups-browsed.service 2>/dev/null; then sudo systemctl restart cups-browsed.service fi diff --git a/test/shell.d/cups-hardening-test.sh b/test/shell.d/cups-hardening-test.sh index 7ba8fea3..f5c92e44 100644 --- a/test/shell.d/cups-hardening-test.sh +++ b/test/shell.d/cups-hardening-test.sh @@ -167,3 +167,58 @@ PATH="$mock_bin:$PATH" \ fail "the machine-wide migration repeats privileged work" pass "the migration safely converts an active existing installation once" + +# An interrupted earlier run leaves cups-browsed stopped, so the retry that +# follows finds it inactive. It must still be restarted: the retry records the +# machine-wide marker either way, so a restart skipped here would leave printer +# discovery off until the next reboot with nothing left to run. +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG" +[[ $1 == "is-active" ]] && exit 1 +exit 0 +SH +chmod +x "$mock_bin/systemctl" + +retry_log="$test_tmp/retry.log" +retry_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-retry" + +OMARCHY_CUPS_TEST_LOG="$retry_log" \ + PATH="$mock_bin:$PATH" \ + OMARCHY_PATH="$ROOT" \ + OMARCHY_CUPS_FILES_CONF="$authorization_conf" \ + OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ + OMARCHY_CUPS_MIGRATION_MARKER="$retry_marker" \ + bash -euo pipefail "$ROOT/migrations/1787815267.sh" + +grep -qxF $'systemctl\trestart cups-browsed.service' "$retry_log" || + fail "the retry resumes cups-browsed after an interrupted earlier run" + +pass "a run following an interrupted one still resumes printer discovery" + +# A unit the user masked or disabled reports not-enabled, and restarting it +# would fail and abort the migration before it records completion. +cat >"$mock_bin/systemctl" <<'SH' +#!/bin/bash +printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG" +[[ $1 == "is-active" || $1 == "is-enabled" ]] && exit 1 +exit 0 +SH +chmod +x "$mock_bin/systemctl" + +masked_log="$test_tmp/masked.log" +masked_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-masked" + +OMARCHY_CUPS_TEST_LOG="$masked_log" \ + PATH="$mock_bin:$PATH" \ + OMARCHY_PATH="$ROOT" \ + OMARCHY_CUPS_FILES_CONF="$authorization_conf" \ + OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ + OMARCHY_CUPS_MIGRATION_MARKER="$masked_marker" \ + bash -euo pipefail "$ROOT/migrations/1787815267.sh" + +! grep -qxF $'systemctl\trestart cups-browsed.service' "$masked_log" || + fail "the migration leaves a masked or disabled cups-browsed alone" +[[ -f $masked_marker ]] || fail "the migration completes with cups-browsed masked" + +pass "a masked or disabled cups-browsed is left alone and does not fail the migration" From 68fc0cf6e6a12c7774b35b1874d0344a61ddabd9 Mon Sep 17 00:00:00 2001 From: omabot Date: Thu, 27 Aug 2026 19:55:36 +0200 Subject: [PATCH 3/5] Match the CUPS directives being rewritten the way cupsd reads them cupsd compares directive names with _cups_strcasecmp, so a hand-edited "systemgroup sys root wheel" is live configuration, but matching $1 against the canonical spelling skipped it and appended a second directive at the end of the file. parse_groups accumulates the groups of every SystemGroup directive it reads rather than replacing them, so both lines took effect and wheel kept the passwordless administration this is meant to remove, with the migration reporting success. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- install/config/printing.sh | 6 +++--- test/shell.d/cups-hardening-test.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/install/config/printing.sh b/install/config/printing.sh index b99e03c3..31c969ea 100644 --- a/install/config/printing.sh +++ b/install/config/printing.sh @@ -16,7 +16,7 @@ elif [[ -f $cups_files_conf ]]; then if ! awk ' NR == FNR { - if ($1 == "SystemGroup") { + if (tolower($1) == "systemgroup") { for (i = 2; i <= NF; i++) { if (substr($i, 1, 1) == "#") break @@ -29,7 +29,7 @@ elif [[ -f $cups_files_conf ]]; then next } - $1 == "SystemGroup" { + tolower($1) == "systemgroup" { comment_start = index($0, "#") if (!wrote_system_group) { printf "SystemGroup" @@ -47,7 +47,7 @@ elif [[ -f $cups_files_conf ]]; then next } - $1 == "PeerCred" { + tolower($1) == "peercred" { comment_start = index($0, "#") if (!saw_peer_cred) { printf "PeerCred on" diff --git a/test/shell.d/cups-hardening-test.sh b/test/shell.d/cups-hardening-test.sh index f5c92e44..2f0bc96a 100644 --- a/test/shell.d/cups-hardening-test.sh +++ b/test/shell.d/cups-hardening-test.sh @@ -222,3 +222,31 @@ OMARCHY_CUPS_TEST_LOG="$masked_log" \ [[ -f $masked_marker ]] || fail "the migration completes with cups-browsed masked" pass "a masked or disabled cups-browsed is left alone and does not fail the migration" + +# cupsd compares directive names case-insensitively, so a hand-edited lowercase +# directive is live configuration. Matching it exactly would skip the line and +# append a second one, and cupsd accumulates the groups of every SystemGroup +# directive it reads -- leaving wheel with passwordless administration. +lowercase_conf="$test_tmp/etc/cups/lowercase.conf" +cat >"$lowercase_conf" <<'CONF' +systemgroup sys root wheel +peercred off +CONF + +PATH="$mock_bin:$PATH" \ + OMARCHY_CUPS_FILES_CONF="$lowercase_conf" \ + OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ + bash -euo pipefail "$ROOT/install/config/printing.sh" + +! grep -qiE '^[[:space:]]*systemgroup\b.*\bwheel\b' "$lowercase_conf" || + fail "printing setup removes wheel from a lowercase SystemGroup directive" "$(cat "$lowercase_conf")" +[[ $(grep -ciE '^[[:space:]]*systemgroup\b' "$lowercase_conf") == 1 ]] || + fail "printing setup leaves one SystemGroup directive whatever case it was written in" "$(cat "$lowercase_conf")" +grep -qxF 'SystemGroup sys root cups-browsed' "$lowercase_conf" || + fail "printing setup reserves administration for the service account" "$(cat "$lowercase_conf")" +[[ $(grep -ciE '^[[:space:]]*peercred\b' "$lowercase_conf") == 1 ]] || + fail "printing setup leaves one PeerCred directive" "$(cat "$lowercase_conf")" +grep -qxF 'PeerCred on' "$lowercase_conf" || + fail "printing setup enables peer credentials whatever case they were written in" "$(cat "$lowercase_conf")" + +pass "printing setup rewrites directives cupsd reads case-insensitively" From b07374f03c72bb841c2e8dfe1e93b8a76a5ea737 Mon Sep 17 00:00:00 2001 From: omabot Date: Thu, 27 Aug 2026 19:55:36 +0200 Subject: [PATCH 4/5] Reserve the cups-browsed account name at install The username prompt already refuses the service accounts a desktop user must not claim, cups and lp among them. A user who took cups-browsed would get a primary group of that name, and the CUPS authorization written here puts that group in SystemGroup, handing that desktop user the passwordless administration the rest of this change removes. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- install/provisioning/setup-form.sh | 2 +- test/shell.d/setup-form-test.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/install/provisioning/setup-form.sh b/install/provisioning/setup-form.sh index 909668eb..f4265d1d 100644 --- a/install/provisioning/setup-form.sh +++ b/install/provisioning/setup-form.sh @@ -79,7 +79,7 @@ Turkish|trq Ukrainian|ua' OMARCHY_USERNAME_PATTERN='^[a-z_][a-z0-9_-]*[$]?$' -OMARCHY_RESERVED_USERNAMES='^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$' +OMARCHY_RESERVED_USERNAMES='^(root|bin|daemon|mail|ftp|http|nobody|dbus|systemd-coredump|systemd-network|systemd-oom|systemd-journal-remote|systemd-resolve|systemd-timesync|tss|uuidd|alpm|git|avahi|cups|cups-browsed|lp|_talkd|polkitd|rtkit|qemu|brltty|gluster|rpc|libvirt-qemu|pcscd|nvidia-persistenced|sddm)$' OMARCHY_HOSTNAME_PATTERN='^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$' OMARCHY_HOSTNAME_DEFAULT='omarchy' diff --git a/test/shell.d/setup-form-test.sh b/test/shell.d/setup-form-test.sh index 4c797e4f..2b9878eb 100755 --- a/test/shell.d/setup-form-test.sh +++ b/test/shell.d/setup-form-test.sh @@ -144,11 +144,12 @@ pass "keyboard prompt propagates Esc and Ctrl+C without dying under set -e" # Username -TAKEN_USERS=dhh run_prompt omarchy_prompt_username "0:Not A Username" "0:root" "0:dhh" "0:david" +TAKEN_USERS=dhh run_prompt omarchy_prompt_username "0:Not A Username" "0:root" "0:cups-browsed" "0:dhh" "0:david" assert_status 0 "username prompt accepts a valid name" [[ $(field username) == "david" ]] || fail "username prompt keeps re-asking until the name is valid" assert_notices "username prompt explains each rejection" "Username must be alphanumeric with no spaces Username is reserved for system +Username is reserved for system That username already exists on this machine" pass "username prompt rejects malformed, reserved, and taken names" From 74997fd523f4862964322fec38ae35dc71b9b822 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 29 Aug 2026 02:18:44 -0400 Subject: [PATCH 5/5] Ship CUPS authorization through settings package --- etc/cups/cups-files.conf | 90 ++++++++++++++++ install/config/all.sh | 1 - install/config/printing.sh | 83 --------------- install/post-install/pacman.sh | 10 +- migrations/1787815267.sh | 24 ++++- test/acceptance.d/system-test.sh | 2 +- test/shell.d/cups-hardening-test.sh | 153 +++++++++++----------------- 7 files changed, 180 insertions(+), 183 deletions(-) create mode 100644 etc/cups/cups-files.conf delete mode 100644 install/config/printing.sh diff --git a/etc/cups/cups-files.conf b/etc/cups/cups-files.conf new file mode 100644 index 00000000..e515117f --- /dev/null +++ b/etc/cups/cups-files.conf @@ -0,0 +1,90 @@ +# +# File/directory/user/group configuration file for the CUPS scheduler. +# See "man cups-files.conf" for a complete description of this file. +# + +# List of events that are considered fatal errors for the scheduler... +#FatalErrors config + +# Strip domain in local username? +#StripUserDomain No + +# Do we call fsync() after writing configuration or status files? +#SyncOnClose No + +# Default user and group for filters/backends/helper programs; this cannot be +# any user or group that resolves to ID 0 for security reasons... +User 209 +Group 209 + +# Administrator user group, used to match @SYSTEM in cupsd.conf policy rules... +# This cannot contain the Group value for security reasons... +SystemGroup cups-browsed sys root + + +# Are Unix domain socket peer credentials used for authorization? +PeerCred on + +# User that is substituted for unauthenticated (remote) root accesses... +#RemoteRoot remroot + +# Do we allow file: device URIs other than to /dev/null? +#FileDevice No + +# Permissions for configuration and log files... +#ConfigFilePerm 0640 +#LogFilePerm 0644 + +# Location of the file logging all access to the scheduler; may be the name +# "syslog". If not an absolute path, the value of ServerRoot is used as the +# root directory. Also see the "AccessLogLevel" directive in cupsd.conf. +AccessLog /var/log/cups/access_log + +# Location of cache files used by the scheduler... +#CacheDir /var/cache/cups + +# Location of data files used by the scheduler... +#DataDir /usr/share/cups + +# Location of the static web content served by the scheduler... +#DocumentRoot /usr/share/cups/doc + +# Location of the file logging all messages produced by the scheduler and any +# helper programs; may be the name "syslog". If not an absolute path, the value +# of ServerRoot is used as the root directory. Also see the "LogLevel" +# directive in cupsd.conf. +ErrorLog /var/log/cups/error_log + +# Location of the file logging all pages printed by the scheduler and any +# helper programs; may be the name "syslog". If not an absolute path, the value +# of ServerRoot is used as the root directory. Also see the "PageLogFormat" +# directive in cupsd.conf. +PageLog /var/log/cups/page_log + +# Location of the file listing all of the local printers... +#Printcap /etc/printcap + +# Format of the Printcap file... +#PrintcapFormat bsd +#PrintcapFormat plist +#PrintcapFormat solaris + +# Location of all spool files... +#RequestRoot /var/spool/cups + +# Location of helper programs... +#ServerBin /usr/lib/cups + +# SSL/TLS keychain for the scheduler... +#ServerKeychain ssl + +# Location of other configuration files... +#ServerRoot /etc/cups + +# Location of scheduler state files... +#StateDir /run/cups + +# Location of scheduler/helper temporary files. This directory is emptied on +# scheduler startup and cannot be one of the standard (public) temporary +# directory locations for security reasons... +#TempDir /var/spool/cups/tmp diff --git a/install/config/all.sh b/install/config/all.sh index aa1044f1..91256dc7 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -7,6 +7,5 @@ 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" diff --git a/install/config/printing.sh b/install/config/printing.sh deleted file mode 100644 index 31c969ea..00000000 --- a/install/config/printing.sh +++ /dev/null @@ -1,83 +0,0 @@ -# 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 (tolower($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 - } - - tolower($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 - } - - tolower($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 diff --git a/install/post-install/pacman.sh b/install/post-install/pacman.sh index 23580b3c..327b812f 100644 --- a/install/post-install/pacman.sh +++ b/install/post-install/pacman.sh @@ -3,11 +3,13 @@ cp -f "$OMARCHY_PATH/default/pacman/pacman-${OMARCHY_MIRROR:-stable}.conf" /etc/pacman.conf cp -f "$OMARCHY_PATH/default/pacman/mirrorlist-${OMARCHY_MIRROR:-stable}" /etc/pacman.d/mirrorlist -# omarchy-settings skips this override until cups-browsed is actually present -# to avoid pacman creating cups-browsed.conf.pacnew during ISO package install. -if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -d /etc/cups ]]; then +# omarchy-settings skips these overrides until CUPS is actually present to +# avoid pacman creating .pacnew files during ISO package installation. +if [[ -f $OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf && -f /etc/cups/cups-files.conf ]]; then + systemd-sysusers /etc/sysusers.d/omarchy-cups-browsed.conf cp -f "$OMARCHY_PATH/etc-overrides/cups-cups-browsed.conf" /etc/cups/cups-browsed.conf - rm -f /etc/cups/cups-browsed.conf.pacnew + install -m 0640 -o root -g cups "$OMARCHY_PATH/etc-overrides/cups-cups-files.conf" /etc/cups/cups-files.conf + rm -f /etc/cups/cups-browsed.conf.pacnew /etc/cups/cups-files.conf.pacnew fi source "$OMARCHY_INSTALL/hardware/pacman.sh" diff --git a/migrations/1787815267.sh b/migrations/1787815267.sh index 31a8e797..b3f9282a 100644 --- a/migrations/1787815267.sh +++ b/migrations/1787815267.sh @@ -4,6 +4,28 @@ machine_marker="${OMARCHY_CUPS_MIGRATION_MARKER:-/var/lib/omarchy/migrations/178 [[ ! -e $machine_marker ]] || exit 0 +# Existing releases allowed a desktop user or shared group named cups-browsed, +# which systemd-sysusers would silently reuse for passwordless CUPS access. +if omarchy-pkg-present cups; then + cups_browsed_account=$(getent passwd cups-browsed || true) + cups_browsed_group=$(getent group cups-browsed || true) + + if [[ -n $cups_browsed_account || -n $cups_browsed_group ]]; then + IFS=: read -r _ _ cups_browsed_uid cups_browsed_gid cups_browsed_description cups_browsed_home cups_browsed_shell <<<"$cups_browsed_account" + IFS=: read -r _ _ cups_browsed_group_gid cups_browsed_group_members <<<"$cups_browsed_group" + other_primary_user=$(getent passwd | awk -F: -v gid="$cups_browsed_gid" '$1 != "cups-browsed" && $4 == gid { print $1; exit }') + + if [[ ! $cups_browsed_uid =~ ^[0-9]+$ || ! $cups_browsed_group_gid =~ ^[0-9]+$ ]] || + ((cups_browsed_uid <= 0 || cups_browsed_uid >= 1000)) || + [[ $cups_browsed_gid != $cups_browsed_group_gid ]] || + [[ $cups_browsed_description != "CUPS printer discovery" || $cups_browsed_home != "/" || $cups_browsed_shell != "/usr/bin/nologin" ]] || + [[ -n $cups_browsed_group_members || -n $other_primary_user ]]; then + echo "Cannot harden printer discovery: the existing cups-browsed user or group is not a dedicated system account." >&2 + false + fi + fi +fi + # 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 @@ -20,8 +42,6 @@ if systemctl is-active --quiet cups-browsed.service 2>/dev/null; then 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 diff --git a/test/acceptance.d/system-test.sh b/test/acceptance.d/system-test.sh index c5c8da5c..899cc6e7 100644 --- a/test/acceptance.d/system-test.sh +++ b/test/acceptance.d/system-test.sh @@ -86,7 +86,7 @@ verify_printing_security() { [[ $(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 || + [[ " $(id -nG cups-browsed) " != *" cups "* ]] || fail "cups-browsed is separate from the print-filter group" if lpinfo_output=$(LC_ALL=C timeout 10 lpinfo -v &1); then diff --git a/test/shell.d/cups-hardening-test.sh b/test/shell.d/cups-hardening-test.sh index 2f0bc96a..96110fff 100644 --- a/test/shell.d/cups-hardening-test.sh +++ b/test/shell.d/cups-hardening-test.sh @@ -6,6 +6,7 @@ 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" +cups_files_conf="$ROOT/etc/cups/cups-files.conf" sysusers_conf="$ROOT/etc/sysusers.d/omarchy-cups-browsed.conf" service_dropin="$ROOT/etc/systemd/system/cups-browsed.service.d/10-omarchy.conf" @@ -26,6 +27,21 @@ grep -qxF 'CreateRemoteCUPSPrinterQueues No' "$cups_browsed_conf" || pass "cups-browsed uses explicit supported discovery policy and an isolated cache" +grep -qxF 'SystemGroup cups-browsed sys root' "$cups_files_conf" || + fail "only the printer discovery account receives passwordless CUPS administration" +grep -qxF 'PeerCred on' "$cups_files_conf" || + fail "the packaged CUPS policy enables peer credentials" +[[ $(grep -ciE '^[[:space:]]*SystemGroup[[:space:]]' "$cups_files_conf") == 1 ]] || + fail "the packaged CUPS policy has one SystemGroup directive" +[[ $(grep -ciE '^[[:space:]]*PeerCred[[:space:]]' "$cups_files_conf") == 1 ]] || + fail "the packaged CUPS policy has one PeerCred directive" +[[ ! -e $ROOT/install/config/printing.sh ]] || + fail "printing policy is not rewritten by an install script" +! grep -q 'config/printing.sh' "$ROOT/install/config/all.sh" "$ROOT/migrations/1787815267.sh" || + fail "neither install nor update invokes a printing rewrite script" + +pass "CUPS authorization ships as a canonical package override" + grep -qxF 'u cups-browsed - "CUPS printer discovery" / -' "$sysusers_conf" || fail "a locked cups-browsed system account is declared" @@ -53,15 +69,25 @@ 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" +mkdir -p "$mock_bin" "$test_tmp/var/lib/omarchy/migrations" -cat >"$mock_bin/systemd-sysusers" <<'SH' +passwd_db="$test_tmp/passwd" +group_db="$test_tmp/group" +touch "$passwd_db" "$group_db" + +cat >"$mock_bin/getent" <<'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" +case "$1" in + passwd) database="$OMARCHY_CUPS_TEST_PASSWD" ;; + group) database="$OMARCHY_CUPS_TEST_GROUP" ;; + *) exit 2 ;; +esac + +if (($# == 1)); then + cat "$database" +else + awk -F: -v name="$2" '$1 == name { print; found = 1 } END { exit !found }' "$database" +fi SH cat >"$mock_bin/omarchy-pkg-present" <<'SH' #!/bin/bash @@ -85,62 +111,40 @@ 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" +touch "$log" export OMARCHY_CUPS_TEST_LOG="$log" +export OMARCHY_CUPS_TEST_PASSWD="$passwd_db" +export OMARCHY_CUPS_TEST_GROUP="$group_db" -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" +printf 'cups-browsed:x:1000:1000:Desktop user:/home/cups-browsed:/usr/bin/bash\n' >"$passwd_db" +printf 'cups-browsed:x:1000:\n' >"$group_db" 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" + OMARCHY_PATH="$ROOT" \ + OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/desktop-collision-marker" \ + bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then + fail "the migration accepts an existing desktop user named cups-browsed" fi +[[ ! -s $log ]] || fail "an account collision stops the migration before changing the system" -pass "printing setup refuses to rewrite a symlinked privileged configuration" +printf 'alice:x:1000:947:Desktop user:/home/alice:/usr/bin/bash\n' >"$passwd_db" +printf 'cups-browsed:x:947:alice\n' >"$group_db" +if PATH="$mock_bin:$PATH" \ + OMARCHY_PATH="$ROOT" \ + OMARCHY_CUPS_MIGRATION_MARKER="$test_tmp/group-collision-marker" \ + bash -euo pipefail "$ROOT/migrations/1787815267.sh" 2>/dev/null; then + fail "the migration accepts an existing cups-browsed group with members" +fi +[[ ! -s $log ]] || fail "a group collision stops the migration before changing the system" + +printf 'cups-browsed:x:947:947:CUPS printer discovery:/:/usr/bin/nologin\n' >"$passwd_db" +printf 'cups-browsed:x:947:\n' >"$group_db" + +pass "the migration rejects account and group collisions before changing printing" 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" @@ -153,7 +157,7 @@ grep -qxF $'systemctl\tstop cups-browsed.service' "$log" || 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" + fail "the migration reloads the packaged 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" @@ -168,10 +172,8 @@ PATH="$mock_bin:$PATH" \ pass "the migration safely converts an active existing installation once" -# An interrupted earlier run leaves cups-browsed stopped, so the retry that -# follows finds it inactive. It must still be restarted: the retry records the -# machine-wide marker either way, so a restart skipped here would leave printer -# discovery off until the next reboot with nothing left to run. +# An interrupted earlier run leaves cups-browsed stopped. A retry still needs +# to resume an enabled service before recording completion. cat >"$mock_bin/systemctl" <<'SH' #!/bin/bash printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG" @@ -186,8 +188,6 @@ retry_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-retry" OMARCHY_CUPS_TEST_LOG="$retry_log" \ PATH="$mock_bin:$PATH" \ OMARCHY_PATH="$ROOT" \ - OMARCHY_CUPS_FILES_CONF="$authorization_conf" \ - OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ OMARCHY_CUPS_MIGRATION_MARKER="$retry_marker" \ bash -euo pipefail "$ROOT/migrations/1787815267.sh" @@ -196,8 +196,7 @@ grep -qxF $'systemctl\trestart cups-browsed.service' "$retry_log" || pass "a run following an interrupted one still resumes printer discovery" -# A unit the user masked or disabled reports not-enabled, and restarting it -# would fail and abort the migration before it records completion. +# A masked or disabled unit is deliberately left alone. cat >"$mock_bin/systemctl" <<'SH' #!/bin/bash printf 'systemctl\t%s\n' "$*" >>"$OMARCHY_CUPS_TEST_LOG" @@ -212,8 +211,6 @@ masked_marker="$test_tmp/var/lib/omarchy/migrations/1787815267-masked" OMARCHY_CUPS_TEST_LOG="$masked_log" \ PATH="$mock_bin:$PATH" \ OMARCHY_PATH="$ROOT" \ - OMARCHY_CUPS_FILES_CONF="$authorization_conf" \ - OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ OMARCHY_CUPS_MIGRATION_MARKER="$masked_marker" \ bash -euo pipefail "$ROOT/migrations/1787815267.sh" @@ -222,31 +219,3 @@ OMARCHY_CUPS_TEST_LOG="$masked_log" \ [[ -f $masked_marker ]] || fail "the migration completes with cups-browsed masked" pass "a masked or disabled cups-browsed is left alone and does not fail the migration" - -# cupsd compares directive names case-insensitively, so a hand-edited lowercase -# directive is live configuration. Matching it exactly would skip the line and -# append a second one, and cupsd accumulates the groups of every SystemGroup -# directive it reads -- leaving wheel with passwordless administration. -lowercase_conf="$test_tmp/etc/cups/lowercase.conf" -cat >"$lowercase_conf" <<'CONF' -systemgroup sys root wheel -peercred off -CONF - -PATH="$mock_bin:$PATH" \ - OMARCHY_CUPS_FILES_CONF="$lowercase_conf" \ - OMARCHY_CUPS_BROWSED_SYSUSERS_CONF="$sysusers_conf" \ - bash -euo pipefail "$ROOT/install/config/printing.sh" - -! grep -qiE '^[[:space:]]*systemgroup\b.*\bwheel\b' "$lowercase_conf" || - fail "printing setup removes wheel from a lowercase SystemGroup directive" "$(cat "$lowercase_conf")" -[[ $(grep -ciE '^[[:space:]]*systemgroup\b' "$lowercase_conf") == 1 ]] || - fail "printing setup leaves one SystemGroup directive whatever case it was written in" "$(cat "$lowercase_conf")" -grep -qxF 'SystemGroup sys root cups-browsed' "$lowercase_conf" || - fail "printing setup reserves administration for the service account" "$(cat "$lowercase_conf")" -[[ $(grep -ciE '^[[:space:]]*peercred\b' "$lowercase_conf") == 1 ]] || - fail "printing setup leaves one PeerCred directive" "$(cat "$lowercase_conf")" -grep -qxF 'PeerCred on' "$lowercase_conf" || - fail "printing setup enables peer credentials whatever case they were written in" "$(cat "$lowercase_conf")" - -pass "printing setup rewrites directives cupsd reads case-insensitively"