Files
omarchycn/test/shell.d/cups-hardening-test.sh
T
68fc0cf6e6 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) <noreply@anthropic.com>
Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-08-27 19:55:36 +02:00

253 lines
10 KiB
Bash

#!/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"
# 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"
# 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"