From 74997fd523f4862964322fec38ae35dc71b9b822 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Sat, 29 Aug 2026 02:18:44 -0400 Subject: [PATCH] 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"