diff --git a/docs/superpowers/contracts/guard-lib-migration-2026-07-04.json b/docs/superpowers/contracts/guard-lib-migration-2026-07-04.json new file mode 100644 index 0000000..80f176f --- /dev/null +++ b/docs/superpowers/contracts/guard-lib-migration-2026-07-04.json @@ -0,0 +1,16 @@ +{ + "title": "pacman_wrapper.sh + setup_midnight_shutdown.sh: migrate to guard-lib", + "objective": "Replace the bespoke hosts-guard and shutdown-schedule-guard chattr/bind-mount/systemd-watcher implementations with the new shared guard-lib (~/guard-lib, guardctl) primitives, so multiple projects (screen-locker, steam-backlog-enforcer) stop maintaining parallel copies of the same tamper-resistance mechanism.", + "acceptance_criteria": [ + "pacman_wrapper.sh's pre/post hook fallback functions call guard-lib's generic unlock-all/relock-all scripts instead of the old hosts-only ones", + "setup_midnight_shutdown.sh installs and updates its guarded config via guardctl file-guard instead of hand-rolled chattr + systemd unit generation", + "New nsswitch-plugin.sh / resolved-plugin.sh guard hooks exist for the hosts-file guard's dependent configs", + "shellcheck clean on all changed files (pre-existing SC2329/SC2001 warnings in pacman_wrapper.sh fixed alongside, at user's request, even though unrelated to this migration)" + ], + "out_of_scope": [ + "steam-backlog-enforcer's block-gaming feature (separate repo/commit)", + "guard-lib's own implementation (separate repo/commit)", + "phone_focus_mode, code_tutor, and other unrelated in-progress work present in the working tree" + ], + "verifier": "pre-commit run --files linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh linux_configuration/scripts/periodic_background/hosts/guard/plugins/*.sh && live verification via steam-backlog-enforcer's block-gaming test run" +} diff --git a/docs/superpowers/evidence/guard-lib-migration-2026-07-04.json b/docs/superpowers/evidence/guard-lib-migration-2026-07-04.json new file mode 100644 index 0000000..973a3d5 --- /dev/null +++ b/docs/superpowers/evidence/guard-lib-migration-2026-07-04.json @@ -0,0 +1,46 @@ +{ + "intent": "Replace hosts-guard/shutdown-schedule-guard's bespoke chattr/bind-mount/systemd-watcher implementations with the shared guard-lib (guardctl) primitives.", + "scope": [ + "linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh", + "linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh", + "linux_configuration/scripts/periodic_background/hosts/guard/plugins/nsswitch-plugin.sh (new)", + "linux_configuration/scripts/periodic_background/hosts/guard/plugins/resolved-plugin.sh (new)" + ], + "changes": [ + "Renamed pacman_wrapper.sh's hosts-only pre/post hook fallback functions to call guard-lib's generic unlock-all/relock-all scripts (covers hosts, nsswitch, resolved, shutdown-schedule from one place)", + "setup_midnight_shutdown.sh now installs/updates its guarded config via guardctl file-guard (install, canonical-path, status) instead of hand-writing chattr calls and systemd path/service units", + "Added nsswitch-plugin.sh / resolved-plugin.sh guard hooks for the hosts-file guard's dependent configs", + "Fixed 3 pre-existing shellcheck SC2329 false positives in pacman_wrapper.sh (load_policy_lists, is_blocked_package_name, is_greylisted_package_name, is_steam_package are invoked indirectly by name, not misses) with disable comments explaining the indirect-call pattern", + "Fixed a pre-existing SC2001 style warning: replaced an echo|sed VM-name extraction with bash parameter expansion (${line#\"}/${vm_name%%\"*}), verified to produce identical output" + ], + "verification": [ + { + "command": "shellcheck ", + "result": "pass", + "evidence": "0 warnings/errors, including the 3 previously-flagged SC2329 and 1 SC2001 findings" + }, + { + "command": "bash -n ", + "result": "pass", + "evidence": "syntax OK on all 4 files" + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "All hooks passed (ai-evidence-contract, ai-multifile-contract, shellcheck, codespell, secret scan, etc.)" + }, + { + "command": "live verification of the guard-lib migration end-to-end", + "result": "pass", + "evidence": "Performed in steam-backlog-enforcer's block-gaming feature session: guardctl file-guard sync/pacman-unlock round-trips confirmed live against /etc/hosts, and a real 1-day block-gaming test run exercised the shared guard-lib package-block + file-guard primitives this migration depends on" + } + ], + "risks": [ + "setup_midnight_shutdown.sh's create_config_guard() now hard-requires guardctl on PATH; if ~/guard-lib isn't installed on a fresh machine, first run will error instead of silently falling back", + "test_pacman_wrapper_security.sh already referenced a stale pre-reorg path (scripts/digital_wellbeing/... instead of scripts/periodic_background/digital_wellbeing/...) before this change and still does - not fixed here, out of scope" + ], + "rollback": [ + "git revert the commit", + "Re-run setup_midnight_shutdown.sh's old chattr-based path if guard-lib is removed (not needed unless guard-lib itself is rolled back)" + ] +} diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh index 24465e7..c5ddbb0 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/pacman/pacman_wrapper.sh @@ -63,6 +63,7 @@ verify_policy_integrity() { return 0 } +# shellcheck disable=SC2329 # invoked indirectly, see is_blocked_package_name/is_greylisted_package_name callers below load_policy_lists() { if [[ $POLICY_LISTS_LOADED -eq 1 ]]; then return @@ -139,11 +140,11 @@ needs_unlock() { return 1 } -pacman_hooks_manage_hosts_guard() { - local pre_hook="/etc/pacman.d/hooks/10-unlock-etc-hosts.hook" - local post_hook="/etc/pacman.d/hooks/90-relock-etc-hosts.hook" - local pre_exec="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" - local post_exec="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" +pacman_hooks_manage_guard_lib() { + local pre_hook="/etc/pacman.d/hooks/10-guard-lib-unlock-all.hook" + local post_hook="/etc/pacman.d/hooks/90-guard-lib-relock-all.hook" + local pre_exec="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" + local post_exec="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" if [[ ! -f $pre_hook || ! -f $post_hook ]]; then return 1 @@ -152,32 +153,35 @@ pacman_hooks_manage_hosts_guard() { grep -Fq "$pre_exec" "$pre_hook" && grep -Fq "$post_exec" "$post_hook" } -should_use_wrapper_hosts_guard_fallback() { +should_use_wrapper_guard_lib_fallback() { if ! needs_unlock "$@"; then return 1 fi - if pacman_hooks_manage_hosts_guard; then + if pacman_hooks_manage_guard_lib; then return 1 fi return 0 } -# Run pre/post hooks for /etc/hosts guard if present -pre_unlock_hosts() { - local pre="/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh" +# Run guard-lib's own generic unlock-all/relock-all scripts directly if +# pacman's own hooks for them are missing (e.g. hooks disabled/misconfigured). +# These cover every registered file-guard instance (hosts, nsswitch, +# resolved, shutdown-schedule, ...), not just /etc/hosts. +pre_unlock_guard_lib() { + local pre="/etc/guard-lib/pacman-hooks/guard-lib-unlock-all.sh" if [[ -x $pre ]]; then - echo -e "${CYAN}[hosts-guard] Preparing /etc/hosts for transaction...${NC}" >&2 + echo -e "${CYAN}[guard-lib] Preparing guarded files for transaction...${NC}" >&2 /bin/bash "$pre" || true fi } -post_relock_hosts() { - local post="/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh" +post_relock_guard_lib() { + local post="/etc/guard-lib/pacman-hooks/guard-lib-relock-all.sh" if [[ -x $post ]]; then /bin/bash "$post" || true - echo -e "${CYAN}[hosts-guard] Protections re-applied to /etc/hosts.${NC}" >&2 + echo -e "${CYAN}[guard-lib] Protections re-applied to guarded files.${NC}" >&2 fi } @@ -339,6 +343,7 @@ function display_operation() { } # Helper: return 0 if the given package name is blocked by policy +# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) function is_blocked_package_name() { load_policy_lists local normalized="${1,,}" @@ -359,6 +364,7 @@ function is_blocked_package_name() { } # Helper: return 0 if the given package name is greylisted (challenge required) +# shellcheck disable=SC2329 # invoked indirectly by name (remove_installed_packages_matching, check_install_for) function is_greylisted_package_name() { load_policy_lists local normalized="${1,,}" @@ -573,6 +579,7 @@ function check_for_always_blocked() { } # Helper to check if a package name is steam +# shellcheck disable=SC2329 # invoked indirectly by name (check_install_for) function is_steam_package() { [[ $1 == "steam" ]] } @@ -830,19 +837,19 @@ if ! check_and_handle_db_lock "$@"; then exit 1 fi -manual_hosts_guard=0 +manual_guard_lib_fallback=0 -# Execute the real pacman command (with /etc/hosts guard handling) -if should_use_wrapper_hosts_guard_fallback "$@"; then - pre_unlock_hosts - manual_hosts_guard=1 +# Execute the real pacman command (with guard-lib fallback handling) +if should_use_wrapper_guard_lib_fallback "$@"; then + pre_unlock_guard_lib + manual_guard_lib_fallback=1 fi "$PACMAN_BIN" "$@" exit_code=$? -if [[ $manual_hosts_guard -eq 1 ]]; then - post_relock_hosts +if [[ $manual_guard_lib_fallback -eq 1 ]]; then + post_relock_guard_lib fi # Record end time for statistics @@ -951,7 +958,8 @@ auto_remove_virtualbox_vms() { while IFS= read -r line; do # VBoxManage list vms output format: "VM Name" {uuid} - vm_name=$(echo "$line" | sed 's/^"\(.*\)" {.*}$/\1/') + vm_name="${line#\"}" + vm_name="${vm_name%%\"*}" if [[ -z $vm_name ]]; then continue fi diff --git a/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh b/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh index 6e31f2a..da61312 100755 --- a/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh +++ b/linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh @@ -24,9 +24,24 @@ SCHEDULE_MORNING_END_HOUR=5 # If a canonical config already exists, the script compares against it and # BLOCKS installation if the new values would make the schedule MORE LENIENT # (i.e., later shutdown hours or earlier morning end). +# +# The mechanical protection (chattr, canonical snapshot, path watcher, +# pacman-hook) is provided by guard-lib (guardctl); this ratchet logic and +# the conditional-delay unlock flow below are specific to this one guard +# target and stay bespoke - guardctl's generic `unlock` can't represent +# "hard-block one field, delay only if lenient, no delay if stricter". # ============================================================================ -CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf" +GUARD_NAME="shutdown-schedule" +CONFIG_FILE="/etc/shutdown-schedule.conf" + +# Prints guard-lib's canonical path for our instance, or nothing if the +# instance isn't installed yet (first run on this machine). +canonical_config_path() { + if command -v guardctl >/dev/null 2>&1 && guardctl file-guard status "$GUARD_NAME" >/dev/null 2>&1; then + guardctl file-guard canonical-path "$GUARD_NAME" + fi +} # Validate that the schedule allows at least MIN_USAGE_HOURS of continuous PC usage. # The usable window is from SCHEDULE_MORNING_END_HOUR until each shutdown hour. @@ -78,15 +93,18 @@ validate_minimum_usage_window # Check if trying to make schedule more lenient (later shutdown / earlier morning end) check_schedule_protection() { - # Skip check if no canonical config exists (first install) - if [[ ! -f $CANONICAL_CONFIG ]]; then + local canonical_config + canonical_config="$(canonical_config_path)" + + # Skip check if no canonical config exists yet (first install) + if [[ -z $canonical_config ]] || [[ ! -f $canonical_config ]]; then return 0 fi # Load canonical values local canonical_mon_wed canonical_thu_sun canonical_morning_end # shellcheck source=/dev/null - source "$CANONICAL_CONFIG" 2>/dev/null || return 0 + source "$canonical_config" 2>/dev/null || return 0 canonical_mon_wed="${MON_WED_HOUR:-}" canonical_thu_sun="${THU_SUN_HOUR:-}" canonical_morning_end="${MORNING_END_HOUR:-}" @@ -259,15 +277,15 @@ show_current_status() { echo "" - # Check config file protection status + # Check config file protection status (via guard-lib) echo "Config File Protection Status:" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" + local canonical_file + canonical_file="$(canonical_config_path)" - if [[ -f $config_file ]]; then + if [[ -f $CONFIG_FILE ]]; then echo "✓ Config file exists" # Check immutable attribute - if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then + if lsattr "$CONFIG_FILE" 2>/dev/null | grep -q '^....i'; then echo "✓ Config file is immutable (chattr +i)" else echo "✗ Config file is NOT immutable" @@ -276,15 +294,15 @@ show_current_status() { echo "✗ Config file missing" fi - if [[ -f $canonical_file ]]; then - echo "✓ Canonical copy exists" + if [[ -n $canonical_file ]] && [[ -f $canonical_file ]]; then + echo "✓ Canonical copy exists ($canonical_file)" else - echo "✗ Canonical copy missing" + echo "✗ Canonical copy missing (guard-lib instance not installed?)" fi - if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then + if systemctl is-enabled "guard-file@${GUARD_NAME}.path" &>/dev/null; then echo "✓ Config path watcher is enabled" - if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then + if systemctl is-active "guard-file@${GUARD_NAME}.path" &>/dev/null; then echo "✓ Config path watcher is active" else echo "✗ Config path watcher is not active" @@ -308,31 +326,24 @@ show_current_status() { echo "" } -# Function to create shutdown schedule config file (shared with i3blocks countdown) -# Also creates a canonical (protected) copy and sets immutable attribute +# Function to create/update shutdown schedule config file (shared with +# i3blocks countdown). Mechanical protection (canonical snapshot, chattr, +# path watcher) is guard-lib's job via create_config_guard() below; this +# function only decides what content should exist. create_shutdown_config() { echo "" echo "1. Creating Shutdown Schedule Config..." echo "=======================================" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" - - # Remove immutable attribute if it exists (to allow update) - chattr -i "$config_file" 2>/dev/null || true - chattr -i "$canonical_file" 2>/dev/null || true - - cat >"$config_file" </dev/null 2>&1; then + # Already installed and this content already passed + # check_schedule_protection's ratchet check above - apply it + # directly, canonical first then target (same race-avoidance + # order adjust_shutdown_schedule.sh uses), then re-lock both. + local canonical_file + canonical_file="$(guardctl file-guard canonical-path "$GUARD_NAME")" + chattr -i "$canonical_file" 2>/dev/null || true + chattr -i "$CONFIG_FILE" 2>/dev/null || true + echo "$new_content" >"$canonical_file" + chmod 644 "$canonical_file" + chattr +i "$canonical_file" || echo "⚠ Warning: Could not set immutable attribute on $canonical_file" + echo "$new_content" >"$CONFIG_FILE" + chmod 644 "$CONFIG_FILE" + chattr +i "$CONFIG_FILE" || echo "⚠ Warning: Could not set immutable attribute on $CONFIG_FILE" + echo "✓ Updated config and canonical copy: $CONFIG_FILE" + else + # First install: guard-lib's install snapshots this content as + # the canonical copy, so just write the plain file here. + echo "$new_content" >"$CONFIG_FILE" + chmod 644 "$CONFIG_FILE" + echo "✓ Created shutdown schedule config: $CONFIG_FILE" + fi } -# Function to create config guard (path watcher + enforcement + unlock script) +# Function to install guard-lib protection (path watcher + enforcement) +# and the bespoke ratchet-aware unlock script. create_config_guard() { echo "" - echo "2. Creating Config Guard (Path Watcher + Enforcement)..." - echo "========================================================" + echo "2. Installing Config Guard (guard-lib + unlock script)..." + echo "==========================================================" + + command -v guardctl >/dev/null 2>&1 || { + echo "Error: guardctl not found on PATH. Set up ~/guard-lib first (run its install.sh)." >&2 + exit 1 + } + + if guardctl file-guard status "$GUARD_NAME" >/dev/null 2>&1; then + echo "✓ guard-lib instance '$GUARD_NAME' already installed (content applied above)" + else + guardctl file-guard install "$GUARD_NAME" --target "$CONFIG_FILE" + echo "✓ Installed guard-lib file-guard '$GUARD_NAME' (canonical snapshot, chattr +i, path watcher, initial enforcement)" + fi - local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh" # Obscure name for unlock script - not documented anywhere local unlock_script="/usr/local/sbin/.sd-sched-mgmt" - local guard_service="/etc/systemd/system/shutdown-schedule-guard.service" - local guard_path="/etc/systemd/system/shutdown-schedule-guard.path" - - # Create enforcement script - cat >"$enforce_script" <<'EOF' -#!/bin/bash -# Enforce canonical /etc/shutdown-schedule.conf contents -# This script restores the config from canonical copy if tampered - -set -euo pipefail - -CANONICAL_SOURCE="/usr/local/share/locked-shutdown-schedule.conf" -TARGET="/etc/shutdown-schedule.conf" -LOG_FILE="/var/log/shutdown-schedule-guard.log" - -log() { - printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2 -} - -if [[ ! -f $CANONICAL_SOURCE ]]; then - log "Canonical config not found at $CANONICAL_SOURCE; aborting enforcement" - exit 0 -fi - -# Remove immutable attr to check/restore -chattr -i -a "$TARGET" 2>/dev/null || true - -if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then - log "CONFIG TAMPERING DETECTED – restoring $TARGET from canonical copy" - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" - log "Config restored successfully" -else - log "No drift detected (contents identical)" -fi - -# Re-apply immutable attribute -chattr +i "$TARGET" || log "Failed to set immutable attribute" - -log "Enforcement complete" -EOF - - chmod +x "$enforce_script" - echo "✓ Created enforcement script: $enforce_script" # Create unlock script with psychological delay cat >"$unlock_script" <<'EOF' @@ -423,8 +415,8 @@ EOF set -euo pipefail DELAY_SECONDS=45 +GUARD_NAME="shutdown-schedule" CONFIG_FILE="/etc/shutdown-schedule.conf" -CANONICAL_FILE="/usr/local/share/locked-shutdown-schedule.conf" LOG_FILE="/var/log/shutdown-schedule-guard.log" EDITOR="${EDITOR:-nano}" TEMP_FILE="/tmp/shutdown-schedule-edit.$$" @@ -439,6 +431,12 @@ if [[ $EUID -ne 0 ]]; then exit 1 fi +CANONICAL_FILE="$(guardctl file-guard canonical-path "$GUARD_NAME")" +if [[ -z "$CANONICAL_FILE" ]]; then + echo "Error: guard-lib instance '$GUARD_NAME' is not installed (guardctl file-guard canonical-path returned empty)" >&2 + exit 1 +fi + # Log the unlock attempt log "=== UNLOCK ATTEMPT by $(logname 2>/dev/null || echo 'unknown') from TTY $(tty 2>/dev/null || echo 'unknown') ===" @@ -447,6 +445,7 @@ OLD_MON_WED="" OLD_THU_SUN="" OLD_MORNING_END="" if [[ -f "$CANONICAL_FILE" ]]; then + # shellcheck source=/dev/null source "$CANONICAL_FILE" 2>/dev/null || true OLD_MON_WED="${MON_WED_HOUR:-}" OLD_THU_SUN="${THU_SUN_HOUR:-}" @@ -468,8 +467,14 @@ echo " ⏳ Making shutdown LATER (lenient) = ${DELAY_SECONDS}s delay required" echo " ❌ Lowering MORNING_END_HOUR = BLOCKED (would shorten shutdown window)" echo "" -# Stop the path watcher temporarily -systemctl stop shutdown-schedule-guard.path 2>/dev/null || true +# Stop the path watcher temporarily. This is NOT optional: `chattr -i` +# below is itself enough to fire guard-file@shutdown-schedule.path (its +# PathModified reacts to attribute changes, not just content writes), and +# that watcher's enforce pass unconditionally re-locks the target at the +# end even when no drift is found - which would silently re-lock the file +# out from under us during the 45s delay below. Confirmed live: without +# this stop, the delayed-apply cp failed with "Operation not permitted". +systemctl stop "guard-file@${GUARD_NAME}.path" 2>/dev/null || true # Remove immutable attributes chattr -i -a "$CONFIG_FILE" 2>/dev/null || true @@ -488,6 +493,7 @@ $EDITOR "$TEMP_FILE" NEW_MON_WED="" NEW_THU_SUN="" NEW_MORNING_END="" +# shellcheck source=/dev/null source "$TEMP_FILE" 2>/dev/null || true NEW_MON_WED="${MON_WED_HOUR:-}" NEW_THU_SUN="${THU_SUN_HOUR:-}" @@ -514,7 +520,7 @@ if [[ -n "$OLD_MORNING_END" ]] && [[ -n "$NEW_MORNING_END" ]]; then # Re-apply protection chattr +i "$CONFIG_FILE" 2>/dev/null || true chattr +i "$CANONICAL_FILE" 2>/dev/null || true - systemctl start shutdown-schedule-guard.path 2>/dev/null || true + systemctl start "guard-file@${GUARD_NAME}.path" 2>/dev/null || true log "BLOCKED: User tried to lower MORNING_END_HOUR from $OLD_MORNING_END to $NEW_MORNING_END" exit 1 fi @@ -607,8 +613,8 @@ chmod 644 "$CANONICAL_FILE" chattr +i "$CONFIG_FILE" || echo "Warning: Could not set immutable attribute" chattr +i "$CANONICAL_FILE" || echo "Warning: Could not set immutable attribute" -# Restart path watcher -systemctl start shutdown-schedule-guard.path 2>/dev/null || true +# Restart path watcher (stopped near the start of this script) +systemctl start "guard-file@${GUARD_NAME}.path" 2>/dev/null || true log "Config updated and re-locked by user" @@ -618,6 +624,7 @@ echo "✓ Canonical copy updated" echo "✓ Path watcher re-enabled" echo "" echo "New schedule (will take effect on next timer check):" +# shellcheck source=/dev/null source "$CONFIG_FILE" 2>/dev/null || true echo " Monday-Wednesday: ${MON_WED_HOUR:-??}:00 - 0${MORNING_END_HOUR:-?}:00" echo " Thursday-Sunday: ${THU_SUN_HOUR:-??}:00 - 0${MORNING_END_HOUR:-?}:00" @@ -626,48 +633,6 @@ EOF chmod +x "$unlock_script" # Silently create unlock script - do not announce its existence - - # Create path watcher unit - cat >"$guard_path" <<'EOF' -[Unit] -Description=Watch /etc/shutdown-schedule.conf and trigger enforcement - -[Path] -PathChanged=/etc/shutdown-schedule.conf -Unit=shutdown-schedule-guard.service - -[Install] -WantedBy=multi-user.target -EOF - - echo "✓ Created path watcher: $guard_path" - - # Create enforcement service - cat >"$guard_service" <<'EOF' -[Unit] -Description=Enforce canonical /etc/shutdown-schedule.conf contents -After=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/enforce-shutdown-schedule.sh -Nice=10 -IOSchedulingClass=idle - -[Install] -WantedBy=multi-user.target -EOF - - echo "✓ Created guard service: $guard_service" - - # Reload and enable - systemctl daemon-reload - systemctl enable --now shutdown-schedule-guard.path - echo "✓ Enabled and started shutdown-schedule-guard.path" - - # Run initial enforcement - "$enforce_script" || echo "⚠ Warning: Initial enforcement returned non-zero" - echo "✓ Ran initial enforcement" } # Function to create the shutdown service @@ -1287,12 +1252,12 @@ test_setup() { echo "" echo "Config file protection status:" - local config_file="/etc/shutdown-schedule.conf" - local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" + local canonical_file + canonical_file="$(canonical_config_path)" - if [[ -f $config_file ]]; then + if [[ -f $CONFIG_FILE ]]; then echo "✓ Config file exists" - if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then + if lsattr "$CONFIG_FILE" 2>/dev/null | grep -q '^....i'; then echo "✓ Config file is immutable" else echo "✗ Config file is NOT immutable" @@ -1301,19 +1266,19 @@ test_setup() { echo "✗ Config file missing" fi - if [[ -f $canonical_file ]]; then + if [[ -n $canonical_file ]] && [[ -f $canonical_file ]]; then echo "✓ Canonical copy exists" else echo "✗ Canonical copy missing" fi - if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then + if systemctl is-enabled "guard-file@${GUARD_NAME}.path" &>/dev/null; then echo "✓ Config guard path watcher is enabled" else echo "✗ Config guard path watcher is not enabled" fi - if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then + if systemctl is-active "guard-file@${GUARD_NAME}.path" &>/dev/null; then echo "✓ Config guard path watcher is active" else echo "✗ Config guard path watcher is not active" diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/plugins/nsswitch-plugin.sh b/linux_configuration/scripts/periodic_background/hosts/guard/plugins/nsswitch-plugin.sh new file mode 100755 index 0000000..006599f --- /dev/null +++ b/linux_configuration/scripts/periodic_background/hosts/guard/plugins/nsswitch-plugin.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# guard-lib plugin for the "nsswitch" file-guard instance. +# Ensures /etc/nsswitch.conf's "hosts:" line always contains "files" +# before "dns", preventing bypass of /etc/hosts blocking. Translated from +# the pre-guard-lib enforce-nsswitch.sh - see that file's git history for +# the original standalone version. + +validate() { + local file="$1" + local line + line="$(grep '^hosts:' "$file" 2>/dev/null || true)" + [[ -n "$line" ]] || return 1 + + echo "$line" | grep -qw "files" || return 1 + + if echo "$line" | grep -qw "dns"; then + local files_pos dns_pos + files_pos=$(echo "$line" | grep -bo '\bfiles\b' | head -1 | cut -d: -f1) + dns_pos=$(echo "$line" | grep -bo '\bdns\b' | head -1 | cut -d: -f1) + if [[ -n "$files_pos" && -n "$dns_pos" && "$files_pos" -gt "$dns_pos" ]]; then + return 1 + fi + fi + + return 0 +} + +# Only called when no canonical copy exists yet to restore from instead. +emergency_fix() { + chattr -i "$TARGET" 2>/dev/null || true + if grep -q '^hosts:.*dns' "$TARGET"; then + sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$TARGET" + elif grep -q '^hosts:.*resolve' "$TARGET"; then + sed -i 's/^hosts:\(.*\)resolve/hosts: files\1resolve/' "$TARGET" + else + sed -i 's/^hosts:/hosts: files/' "$TARGET" + fi +} diff --git a/linux_configuration/scripts/periodic_background/hosts/guard/plugins/resolved-plugin.sh b/linux_configuration/scripts/periodic_background/hosts/guard/plugins/resolved-plugin.sh new file mode 100755 index 0000000..4b419b5 --- /dev/null +++ b/linux_configuration/scripts/periodic_background/hosts/guard/plugins/resolved-plugin.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# guard-lib plugin for the "resolved" file-guard instance. +# Ensures /etc/systemd/resolved.conf honours /etc/hosts (ReadEtcHosts=yes) +# and doesn't bypass it via DNS-over-TLS, and keeps the resolved.conf.d +# drop-in directory empty so a drop-in can't silently override these +# settings. Translated from the pre-guard-lib enforce-resolved.sh - see +# that file's git history for the original standalone version. + +RESOLVED_DROPIN_DIR="/etc/systemd/resolved.conf.d" + +# Called unconditionally at the start of every enforce pass (not just on +# drift), matching the original script's behavior of policing the +# drop-in directory every time regardless of resolved.conf's own state. +pre_action() { + if [[ -d "$RESOLVED_DROPIN_DIR" ]]; then + local count + count=$(find "$RESOLVED_DROPIN_DIR" -name '*.conf' -type f 2>/dev/null | wc -l) + if [[ "$count" -gt 0 ]]; then + chattr -i "$RESOLVED_DROPIN_DIR" 2>/dev/null || true + find "$RESOLVED_DROPIN_DIR" -name '*.conf' -type f -delete + fi + else + mkdir -p "$RESOLVED_DROPIN_DIR" + fi + chattr +i "$RESOLVED_DROPIN_DIR" 2>/dev/null || true +} + +validate() { + local file="$1" + + local read_hosts + read_hosts=$(grep -E '^\s*ReadEtcHosts\s*=' "$file" 2>/dev/null | tail -1 | + sed 's/.*=\s*//' | tr -d '[:space:]') + [[ "$read_hosts" == "yes" ]] || return 1 + + local dot + dot=$(grep -E '^\s*DNSOverTLS\s*=' "$file" 2>/dev/null | tail -1 | + sed 's/.*=\s*//' | tr -d '[:space:]') + if [[ -n "$dot" && "$dot" != "no" ]]; then + return 1 + fi + + return 0 +} + +# Only called when no canonical copy exists yet to restore from instead. +emergency_fix() { + chattr -i "$TARGET" 2>/dev/null || true + + if grep -qE '^\s*ReadEtcHosts\s*=' "$TARGET"; then + sed -i -E 's/^\s*ReadEtcHosts\s*=.*/ReadEtcHosts=yes/' "$TARGET" + elif grep -q '^\[Resolve\]' "$TARGET"; then + sed -i '/^\[Resolve\]/a ReadEtcHosts=yes' "$TARGET" + else + printf '\n[Resolve]\nReadEtcHosts=yes\n' >>"$TARGET" + fi + + if grep -qE '^\s*DNSOverTLS\s*=' "$TARGET"; then + sed -i -E 's/^\s*DNSOverTLS\s*=.*/#DNSOverTLS=no/' "$TARGET" + fi +} + +post_restore_action() { + systemctl restart systemd-resolved 2>/dev/null || true +}