mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 13:23:15 +02:00
feat: migrate hosts-guard and shutdown-schedule-guard to guard-lib
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Replaces the bespoke chattr/bind-mount/systemd-watcher implementations for /etc/hosts and /etc/shutdown-schedule.conf with the new shared guard-lib (~/guard-lib, guardctl), so screen-locker and steam-backlog-enforcer's new block-gaming feature stop maintaining parallel copies of the same tamper-resistance mechanism. - pacman_wrapper.sh: pre/post hook fallbacks now call guard-lib's generic unlock-all/relock-all scripts (covers every registered guard instance, not just /etc/hosts) - setup_midnight_shutdown.sh: installs/updates its guarded config via guardctl file-guard instead of hand-rolled chattr + systemd unit generation; the schedule ratchet logic (block-if-more-lenient) stays bespoke since guardctl's generic unlock can't represent it - new hosts/guard/plugins/nsswitch-plugin.sh, resolved-plugin.sh Also fixes, at user's request even though pre-existing: 3 shellcheck SC2329 false positives in pacman_wrapper.sh (functions invoked indirectly by name, not actually dead) and 1 SC2001 style warning (echo|sed VM-name extraction replaced with parameter expansion, verified equivalent output). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AFNiYQQgSLAkiBXswyimPq
This commit is contained in:
parent
ea94435c4f
commit
66c4698194
@ -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"
|
||||
}
|
||||
@ -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 <all 4 changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "0 warnings/errors, including the 3 previously-flagged SC2329 and 1 SC2001 findings"
|
||||
},
|
||||
{
|
||||
"command": "bash -n <all 4 changed files>",
|
||||
"result": "pass",
|
||||
"evidence": "syntax OK on all 4 files"
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run --files <changed files + this evidence/contract pair>",
|
||||
"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)"
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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" <<EOF
|
||||
local new_content
|
||||
new_content="$(cat <<EOF
|
||||
# Shutdown schedule configuration
|
||||
# This file is managed by setup_midnight_shutdown.sh
|
||||
# Used by: day-specific-shutdown-check.sh, shutdown_countdown.sh (i3blocks)
|
||||
#
|
||||
# WARNING: This file is protected by:
|
||||
# 1. Immutable attribute (chattr +i)
|
||||
# 2. Canonical copy at /usr/local/share/locked-shutdown-schedule.conf
|
||||
# 3. Path watcher service that auto-restores if modified
|
||||
#
|
||||
# Modifications to this file will be automatically reverted.
|
||||
# WARNING: This file is protected by guard-lib (guardctl): immutable
|
||||
# attribute, a canonical copy, and a path watcher that auto-restores it
|
||||
# if modified outside the sanctioned unlock flow.
|
||||
|
||||
# Shutdown hour for Monday-Wednesday (24-hour format)
|
||||
MON_WED_HOUR=${SCHEDULE_MON_WED_HOUR}
|
||||
@ -343,73 +354,54 @@ THU_SUN_HOUR=${SCHEDULE_THU_SUN_HOUR}
|
||||
# Morning end hour (shutdown window ends at this hour)
|
||||
MORNING_END_HOUR=${SCHEDULE_MORNING_END_HOUR}
|
||||
EOF
|
||||
)"
|
||||
|
||||
chmod 644 "$config_file"
|
||||
echo "✓ Created shutdown schedule config: $config_file"
|
||||
|
||||
# Create canonical (protected) copy
|
||||
install -m 644 -D "$config_file" "$canonical_file"
|
||||
echo "✓ Created canonical copy: $canonical_file"
|
||||
|
||||
# Set immutable attribute on both files
|
||||
chattr +i "$config_file" || echo "⚠ Warning: Could not set immutable attribute on $config_file"
|
||||
chattr +i "$canonical_file" || echo "⚠ Warning: Could not set immutable attribute on $canonical_file"
|
||||
echo "✓ Set immutable attribute (chattr +i) on config files"
|
||||
if guardctl file-guard status "$GUARD_NAME" >/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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user