diff --git a/hosts/guard/README.md b/hosts/guard/README.md deleted file mode 100644 index 15a6809..0000000 --- a/hosts/guard/README.md +++ /dev/null @@ -1,31 +0,0 @@ -Hosts Guard Components -====================== - -This directory contains templates for hardening /etc/hosts against impulsive tampering by adding friction, NOT providing absolute security against a determined root user. - -Components: -1. enforce-hosts.sh – Idempotent script that: compares /etc/hosts with canonical copy at /usr/local/share/locked-hosts and restores if different; reapplies immutable attribute. -2. systemd units (to be installed under /etc/systemd/system): - - hosts-guard.service (oneshot enforcement) - - hosts-guard.path (triggers on PathChanged=/etc/hosts) - - hosts-bind-mount.service (bind mounts /etc/hosts read-only after boot) -3. psychological/ directory – scripts that add delay + journaling before allowing a maintenance/unlock operation. -4. pacman hooks – automatically unlock/re-lock /etc/hosts around package transactions so pacman never fails due to the read-only bind mount. - -Install Flow (suggested): -1. After generating /etc/hosts via your existing hosts/install.sh, copy it to /usr/local/share/locked-hosts. -2. Install enforce-hosts.sh to /usr/local/sbin/ (chmod 755). -3. Place units and enable: - systemctl daemon-reload - systemctl enable --now hosts-guard.path - systemctl enable --now hosts-bind-mount.service -4. (Optional) Use psychological/unlock-hosts.sh as the ONLY sanctioned way to modify hosts (it removes protections temporarily, launches an editor after a delay, and re-enforces on close). -5. Make pacman automatic (recommended): - ./install_pacman_hooks.sh - This installs hooks under /etc/pacman.d/hooks that: - - PreTransaction: temporarily disable guard and make /etc/hosts writable - - PostTransaction: re-run enforcement and re-enable guard (bind mount + path watcher) - -Limitations: -- A root user can still disable units, remount, remove attributes. -- Purpose is to interrupt habit loops and create intentional friction. diff --git a/hosts/guard/README_FOR_LLM.md b/hosts/guard/README_FOR_LLM.md deleted file mode 100644 index a69183c..0000000 --- a/hosts/guard/README_FOR_LLM.md +++ /dev/null @@ -1,205 +0,0 @@ -# Hosts Guard System - LLM Reference Guide - -> **For AI assistants**: This document explains how the hosts guard system works so you can make correct modifications. - -## System Purpose - -Prevent tampering with `/etc/hosts` to maintain website blocking (YouTube, social media, etc.) as part of a digital wellbeing system. - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ PROTECTION LAYERS │ -├─────────────────────────────────────────────────────────────────────┤ -│ │ -│ Layer 1: Immutable Attribute │ -│ ───────────────────────────── │ -│ /etc/hosts has chattr +i (cannot be modified even by root) │ -│ │ -│ Layer 2: Canonical Copy │ -│ ─────────────────────── │ -│ /usr/local/share/locked-hosts contains the "true" version │ -│ If /etc/hosts differs, it gets overwritten from this copy │ -│ │ -│ Layer 3: Path Watcher (systemd) │ -│ ────────────────────────────── │ -│ hosts-guard.path watches /etc/hosts for ANY change │ -│ hosts-guard.service runs enforce-hosts.sh when triggered │ -│ │ -│ Layer 4: Read-Only Bind Mount │ -│ ──────────────────────────── │ -│ hosts-bind-mount.service mounts /etc/hosts read-only │ -│ Even if chattr is removed, write operations fail │ -│ │ -│ Layer 5: Custom Entries Protection │ -│ ───────────────────────────────── │ -│ /etc/hosts.custom-entries.state tracks blocked domains │ -│ Prevents removal of domains from install.sh │ -│ │ -│ Layer 6: nsswitch.conf Protection (NEW) │ -│ ─────────────────────────────────────── │ -│ Prevents bypass via /etc/nsswitch.conf manipulation │ -│ Ensures "files" always appears in hosts: line before "dns" │ -│ nsswitch-guard.path watches for changes │ -│ Canonical copy at /usr/local/share/locked-nsswitch.conf │ -│ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -## File Locations - -| File | Purpose | Protection | -|------|---------|------------| -| `/etc/hosts` | Active hosts file | chattr +i, bind mount | -| `/usr/local/share/locked-hosts` | Canonical source of truth | chattr +i | -| `/etc/hosts.custom-entries.state` | Tracks custom blocked domains | chattr +i | -| `/etc/hosts.stevenblack` | Cached upstream hosts file | None | -| `/etc/nsswitch.conf` | Name service switch config | chattr +i, path watcher | -| `/usr/local/share/locked-nsswitch.conf` | Canonical nsswitch copy | chattr +i | -| `/usr/local/sbin/enforce-hosts.sh` | Restoration script | File permissions | -| `/usr/local/sbin/enforce-nsswitch.sh` | nsswitch enforcement | File permissions | -| `/usr/local/sbin/unlock-hosts` | Psychological unlock script | File permissions | -| `/etc/systemd/system/hosts-guard.path` | Path watcher unit | systemd | -| `/etc/systemd/system/hosts-guard.service` | Enforcement service | systemd | -| `/etc/systemd/system/hosts-bind-mount.service` | RO bind mount | systemd | -| `/etc/systemd/system/nsswitch-guard.path` | nsswitch watcher | systemd | -| `/etc/systemd/system/nsswitch-guard.service` | nsswitch enforce | systemd | - -## Key Scripts - -### hosts/install.sh -- Downloads StevenBlack hosts list (cached at `/etc/hosts.stevenblack`) -- Adds custom blocking entries (YouTube, etc.) -- Comments out allowed sites (4chan, Facebook) -- Runs protection check for custom entries -- Sets up initial immutable attribute - -### hosts/guard/setup_hosts_guard.sh -Installs all protection layers: -- Creates canonical snapshot -- Installs enforce-hosts.sh and unlock-hosts scripts -- Enables systemd path watcher -- Enables bind mount service -- Installs shell history suppression hooks - -### hosts/guard/enforce-hosts.sh -Called when tampering detected: -```bash -# Compares /etc/hosts to canonical -# If different: restores from canonical, logs event -# Re-applies chattr +i -``` - -### hosts/guard/psychological/unlock-hosts.sh -Legitimate edit workflow: -1. Prompts for reason (logged) -2. Stops protection services -3. Waits 45 seconds (cooling off) -4. Opens editor -5. Updates canonical if changes made -6. Re-enables all protections - -## Pacman Integration - -The pacman wrapper calls these hooks during package transactions: -- `/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh` - Before transaction -- `/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh` - After transaction - -These temporarily unlock hosts for package manager operations. - -## Common Tasks - -### Adding a New Blocked Domain - -1. Edit `hosts/install.sh` -2. Find the heredoc section after `# Custom blocking entries` -3. Add line: `0.0.0.0 newdomain.com` -4. Run: `sudo ~/linux-configuration/hosts/install.sh` - -```bash -# Example: Block example.com -# In hosts/install.sh, add to heredoc: -0.0.0.0 example.com -0.0.0.0 www.example.com -``` - -### Allowing a Previously Blocked Domain - -**This is intentionally difficult.** You must: -1. Remove entry from install.sh heredoc -2. Remove protection: `sudo chattr -i /etc/hosts.custom-entries.state` -3. Edit state file to remove domain -4. Re-run install.sh - -### Checking Protection Status - -```bash -# Check immutable attribute -lsattr /etc/hosts -# Should show: ----i--------e-- /etc/hosts - -# Check services -systemctl status hosts-guard.path hosts-guard.service hosts-bind-mount.service - -# Check canonical exists -ls -la /usr/local/share/locked-hosts -``` - -### Legitimate Editing - -```bash -sudo /usr/local/sbin/unlock-hosts -# Enter reason when prompted -# Wait 45 seconds -# Edit in your $EDITOR -# Changes auto-saved to canonical -``` - -## nsswitch.conf Protection (Layer 6) - -**Why this matters:** A user could bypass ALL /etc/hosts protections by simply editing `/etc/nsswitch.conf` and removing `files` from the `hosts:` line. This protection layer prevents that. - -### How it works: -- `nsswitch-guard.path` watches `/etc/nsswitch.conf` for changes -- `nsswitch-guard.service` runs `enforce-nsswitch.sh` when triggered -- Canonical copy stored at `/usr/local/share/locked-nsswitch.conf` -- Validates that `hosts:` line contains `files` before `dns` -- Auto-restores from canonical if tampered - -### Check nsswitch protection status: -```bash -lsattr /etc/nsswitch.conf -systemctl status nsswitch-guard.path -``` - -## Troubleshooting - -### "Cannot modify /etc/hosts" -This is expected! Use the unlock script: -```bash -sudo /usr/local/sbin/unlock-hosts -``` - -### Path watcher not running -```bash -sudo systemctl start hosts-guard.path -sudo systemctl enable hosts-guard.path -``` - -### Bind mount preventing access -```bash -# Temporarily disable (not recommended) -sudo systemctl stop hosts-bind-mount.service -``` - -### Custom entries protection blocking install -The protection mechanism detected you're trying to remove previously blocked domains. This is intentional. To proceed, manually edit the state file (see "Allowing a Previously Blocked Domain"). - -## DO NOT - -1. ❌ Edit `/etc/nsswitch.conf` to bypass hosts (this defeats the purpose) -2. ❌ Stop `hosts-guard.path` without understanding consequences -3. ❌ Delete `/usr/local/share/locked-hosts` (breaks restoration) -4. ❌ Remove entries from install.sh without updating state file -5. ❌ Use `chattr -i` without going through unlock-hosts diff --git a/hosts/guard/enforce-hosts.sh b/hosts/guard/enforce-hosts.sh deleted file mode 100755 index 2eba90a..0000000 --- a/hosts/guard/enforce-hosts.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Template guard script to enforce canonical /etc/hosts -# This will be installed into /usr/local/sbin/enforce-hosts.sh by a setup script. - -set -euo pipefail - -CANONICAL_SOURCE="/usr/local/share/locked-hosts" -TARGET="/etc/hosts" -LOG_FILE="/var/log/hosts-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 hosts not found at $CANONICAL_SOURCE; aborting enforcement" - exit 0 -fi - -if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then - log "Difference detected – restoring $TARGET from canonical copy" - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" -else - log "No drift detected (contents identical)" -fi - -# Re-apply protective attributes: immutable first, then read-only bind mount handled by separate unit -chattr -i -a "$TARGET" 2> /dev/null || true -chattr +i "$TARGET" || log "Failed to set immutable attribute" - -log "Enforcement complete" diff --git a/hosts/guard/enforce-nsswitch.sh b/hosts/guard/enforce-nsswitch.sh deleted file mode 100644 index ec01fd0..0000000 --- a/hosts/guard/enforce-nsswitch.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash -# Template guard script to enforce canonical /etc/nsswitch.conf -# Ensures "hosts:" line always contains "files" before "dns" -# This prevents bypassing /etc/hosts by removing "files" from nsswitch.conf -# Installed to /usr/local/sbin/enforce-nsswitch.sh by setup_hosts_guard.sh - -set -euo pipefail - -CANONICAL_SOURCE="/usr/local/share/locked-nsswitch.conf" -TARGET="/etc/nsswitch.conf" -LOG_FILE="/var/log/nsswitch-guard.log" - -log() { - printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2 -} - -# Function to validate that "hosts:" line has correct format -# Must contain "files" before "dns" (or "dns" not present) -validate_hosts_line() { - local line="$1" - - # Check if "files" is present - if ! echo "$line" | grep -qw "files"; then - return 1 - fi - - # If dns is present, files must come before it - 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 -} - -# Check current nsswitch.conf hosts line -current_hosts_line=$(grep '^hosts:' "$TARGET" 2>/dev/null || echo "") - -if [[ -z "$current_hosts_line" ]]; then - log "CRITICAL: No 'hosts:' line found in $TARGET - restoring from canonical" - if [[ -f "$CANONICAL_SOURCE" ]]; then - chattr -i "$TARGET" 2>/dev/null || true - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" - chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET" - log "Restored $TARGET from canonical copy" - else - log "ERROR: Canonical source not found at $CANONICAL_SOURCE" - exit 1 - fi - exit 0 -fi - -if ! validate_hosts_line "$current_hosts_line"; then - log "TAMPERING DETECTED: 'hosts:' line is invalid or missing 'files' before 'dns'" - log "Current line: $current_hosts_line" - - if [[ -f "$CANONICAL_SOURCE" ]]; then - chattr -i "$TARGET" 2>/dev/null || true - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" - chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET" - log "Restored $TARGET from canonical copy" - else - log "ERROR: Canonical source not found at $CANONICAL_SOURCE" - # Emergency fix: add "files" back to hosts line - chattr -i "$TARGET" 2>/dev/null || true - sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$TARGET" - chattr +i "$TARGET" 2>/dev/null || true - log "Emergency fix applied: added 'files' before 'dns'" - fi - exit 0 -fi - -# If canonical exists, check for any drift -if [[ -f "$CANONICAL_SOURCE" ]]; then - if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then - log "Drift detected in $TARGET (but hosts line valid) - restoring canonical" - chattr -i "$TARGET" 2>/dev/null || true - cp "$CANONICAL_SOURCE" "$TARGET" - chmod 644 "$TARGET" - chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute" - log "Restored $TARGET from canonical copy" - else - log "No drift detected in $TARGET" - fi -else - log "Creating initial canonical snapshot" - mkdir -p "$(dirname "$CANONICAL_SOURCE")" - cp "$TARGET" "$CANONICAL_SOURCE" - chmod 644 "$CANONICAL_SOURCE" - chattr +i "$CANONICAL_SOURCE" 2>/dev/null || log "Failed to protect canonical copy" -fi - -# Ensure immutable attribute is set -chattr -i "$TARGET" 2>/dev/null || true -chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET" - -log "nsswitch.conf enforcement complete" diff --git a/hosts/guard/hosts-bind-mount.service b/hosts/guard/hosts-bind-mount.service deleted file mode 100644 index 998c2f4..0000000 --- a/hosts/guard/hosts-bind-mount.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=Bind mount /etc/hosts over itself as read-only (friction layer) -After=local-fs.target -Before=network.target - -[Service] -Type=oneshot -ExecStart=/bin/mount --bind /etc/hosts /etc/hosts -ExecStart=/bin/mount -o remount,ro,bind /etc/hosts -ExecStartPost=/usr/bin/logger -t hosts-bind-mount "Hosts file bind-mounted read-only" -RemainAfterExit=yes - -[Install] -WantedBy=multi-user.target diff --git a/hosts/guard/hosts-guard.path b/hosts/guard/hosts-guard.path deleted file mode 100644 index 0e3371c..0000000 --- a/hosts/guard/hosts-guard.path +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Watch /etc/hosts and trigger enforcement - -[Path] -PathChanged=/etc/hosts -Unit=hosts-guard.service - -[Install] -WantedBy=multi-user.target diff --git a/hosts/guard/hosts-guard.service b/hosts/guard/hosts-guard.service deleted file mode 100644 index 0cf52b9..0000000 --- a/hosts/guard/hosts-guard.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Enforce canonical /etc/hosts contents -After=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/enforce-hosts.sh -Nice=10 -IOSchedulingClass=idle - -[Install] -WantedBy=multi-user.target diff --git a/hosts/guard/install_pacman_hooks.sh b/hosts/guard/install_pacman_hooks.sh deleted file mode 100755 index 4a1169c..0000000 --- a/hosts/guard/install_pacman_hooks.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } -require_root "$@" - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOKS_DIR="/etc/pacman.d/hooks" - -install -d -m 755 "$HOOKS_DIR" - -# Pre-transaction hook -cat > "$HOOKS_DIR/10-unlock-etc-hosts.hook" << 'HOOK' -[Trigger] -Operation = Upgrade -Operation = Install -Operation = Remove -Type = Package -Target = * - -[Action] -Description = Temporarily unlocking /etc/hosts for transaction -When = PreTransaction -Exec = /bin/bash /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh -NeedsTargets -HOOK - -# Post-transaction hook -cat > "$HOOKS_DIR/90-relock-etc-hosts.hook" << 'HOOK' -[Trigger] -Operation = Upgrade -Operation = Install -Operation = Remove -Type = Package -Target = * - -[Action] -Description = Re-locking /etc/hosts after transaction -When = PostTransaction -Exec = /bin/bash /usr/local/share/hosts-guard/pacman-post-relock-hosts.sh -NeedsTargets -HOOK - -# Place helper scripts into a shared location -install -d -m 755 /usr/local/share/hosts-guard -install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-pre-unlock-hosts.sh" /usr/local/share/hosts-guard/ -install -m 755 "$SCRIPT_DIR/pacman-hooks/pacman-post-relock-hosts.sh" /usr/local/share/hosts-guard/ - -echo "Pacman hooks installed into $HOOKS_DIR." diff --git a/hosts/guard/nsswitch-guard.path b/hosts/guard/nsswitch-guard.path deleted file mode 100644 index 769d698..0000000 --- a/hosts/guard/nsswitch-guard.path +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Watch /etc/nsswitch.conf for tampering (hosts bypass protection) - -[Path] -PathChanged=/etc/nsswitch.conf -Unit=nsswitch-guard.service - -[Install] -WantedBy=multi-user.target diff --git a/hosts/guard/nsswitch-guard.service b/hosts/guard/nsswitch-guard.service deleted file mode 100644 index 448b5e3..0000000 --- a/hosts/guard/nsswitch-guard.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Enforce canonical /etc/nsswitch.conf (prevents hosts bypass) -After=local-fs.target - -[Service] -Type=oneshot -ExecStart=/usr/local/sbin/enforce-nsswitch.sh -Nice=10 -IOSchedulingClass=idle - -[Install] -WantedBy=multi-user.target diff --git a/hosts/guard/pacman-hooks/hosts-guard-common.sh b/hosts/guard/pacman-hooks/hosts-guard-common.sh deleted file mode 100755 index 9872cc3..0000000 --- a/hosts/guard/pacman-hooks/hosts-guard-common.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/env bash -# Shared functions for hosts-guard pacman hooks -# This file is sourced by pacman-pre-unlock-hosts.sh and pacman-post-relock-hosts.sh - -TARGET=/etc/hosts -LOGTAG=hosts-guard-hook - -# Check if target has a read-only mount -is_ro_mount() { - findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro -} - -# Count mount layers for the target -mount_layers_count() { - awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0 -} - -# Collapse all bind mount layers -collapse_mounts() { - local i=0 - if command -v mountpoint >/dev/null 2>&1; then - while mountpoint -q "$TARGET"; do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - done - else - local cnt - cnt=$(mount_layers_count) - while ((cnt > 1)); do - umount -l "$TARGET" >/dev/null 2>&1 || break - i=$((i + 1)) - ((i > 20)) && break - cnt=$(mount_layers_count) - done - fi -} - -# Stop systemd units related to hosts guard -stop_units_if_present() { - local units=(hosts-bind-mount.service hosts-guard.path) - for u in "${units[@]}"; do - if command -v systemctl >/dev/null 2>&1; then - if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then - systemctl stop "$u" >/dev/null 2>&1 || true - fi - fi - done -} - -# Remove immutable/append-only attributes -remove_host_attrs() { - if command -v lsattr >/dev/null 2>&1; then - local attrs - attrs=$(lsattr -d "$TARGET" 2>/dev/null || true) - if echo "$attrs" | grep -q " i "; then - chattr -i "$TARGET" >/dev/null 2>&1 || true - fi - if echo "$attrs" | grep -q " a "; then - chattr -a "$TARGET" >/dev/null 2>&1 || true - fi - fi -} - -# Apply immutable attribute -apply_immutable() { - if command -v chattr >/dev/null 2>&1; then - chattr +i "$TARGET" >/dev/null 2>&1 || true - fi -} - -# Apply a single read-only bind mount layer -apply_ro_bind_mount() { - mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true - mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true -} - -# Start the path watcher service -start_path_watcher() { - if command -v systemctl >/dev/null 2>&1; then - systemctl start hosts-guard.path >/dev/null 2>&1 || true - fi -} - -# Log to system logger and run log file -log_hook() { - local phase="$1" - local state="$2" - logger -t "$LOGTAG" "$phase: $state" - echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true -} diff --git a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh b/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh deleted file mode 100755 index 9f6cee2..0000000 --- a/hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# pacman-post-relock-hosts.sh - Re-apply hosts guard protections after pacman -set -euo pipefail - -# Source shared functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=hosts-guard-common.sh -source "$SCRIPT_DIR/hosts-guard-common.sh" - -ENFORCE=/usr/local/sbin/enforce-hosts.sh - -log_hook "post" "relocking(start)" - -# Collapse any stacked mounts first -collapse_mounts - -# Run enforcement script if available -if [[ -x $ENFORCE ]]; then - "$ENFORCE" >/dev/null 2>&1 || true -fi - -# Apply protections -apply_immutable -apply_ro_bind_mount - -# Start the path watcher -start_path_watcher - -log_hook "post" "relocking(done)" - -exit 0 diff --git a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh b/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh deleted file mode 100755 index 0cb3331..0000000 --- a/hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# pacman-pre-unlock-hosts.sh - Temporarily unlock /etc/hosts before pacman -set -euo pipefail - -# Source shared functions -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# shellcheck source=hosts-guard-common.sh -source "$SCRIPT_DIR/hosts-guard-common.sh" - -# Remove protective attributes -remove_host_attrs -sudo rm /etc/hosts - -# Stop guard services -stop_units_if_present - -log_hook "pre" "unlocking(start)" - -# Collapse any existing mount layers -collapse_mounts - -# Ensure writable by remounting if still read-only -if is_ro_mount; then - mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts -fi - -log_hook "pre" "unlocking(done)" - -exit 0 diff --git a/hosts/guard/psychological/unlock-hosts.sh b/hosts/guard/psychological/unlock-hosts.sh deleted file mode 100755 index 62a781c..0000000 --- a/hosts/guard/psychological/unlock-hosts.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# Guided, delayed unlock procedure to intentionally slow down impulsive edits. -set -euo pipefail - -TARGET=/etc/hosts -CANON=/usr/local/share/locked-hosts -LOG=/var/log/hosts-guard.log -SYSLOG_TAG=hosts-unlock -EDITOR_CMD=${EDITOR:-nano} -DELAY_SECONDS=45 - -log() { printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG" >&2; } - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } -require_root "$@" - -echo "Reason for editing /etc/hosts (will be logged):" >&2 -read -r -p "Enter reason: " REASON -if [[ -z ${REASON// /} ]]; then - echo "Empty reason not allowed. Aborting." >&2 - exit 1 -fi -log "Requested intentional /etc/hosts modification session. Reason: $REASON" -logger -t "$SYSLOG_TAG" "session_start user=${SUDO_USER:-$USER} reason='$REASON'" -echo "This action is logged. A cooling-off delay of $DELAY_SECONDS seconds applies." >&2 - -for s in hosts-bind-mount.service hosts-guard.path; do - if systemctl is-active --quiet "$s"; then - log "Stopping $s" - systemctl stop "$s" || true - fi - if systemctl is-enabled --quiet "$s"; then - log "(Will re-enable later)" - fi -done - -# Remove attributes to allow edit -chattr -i -a "$TARGET" 2>/dev/null || true - -echo "Countdown:" >&2 -for ((i = DELAY_SECONDS; i > 0; i--)); do - printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2 - sleep 1 -done -echo >&2 - -# Launch editor -sha_before=$(sha256sum "$TARGET" | awk '{print $1}') -"$EDITOR_CMD" "$TARGET" -sha_after=$(sha256sum "$TARGET" | awk '{print $1}') - -if [[ $sha_before == "$sha_after" ]]; then - log "No changes made to $TARGET. Reason: $REASON" - logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'" -else - log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON" - logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'" - cp "$TARGET" "$CANON" -fi - -# Re-run enforcement -/usr/local/sbin/enforce-hosts.sh || log "Enforcement script returned non-zero" - -# Restart watchers and bind mount -systemctl start hosts-guard.path || true -systemctl start hosts-bind-mount.service || true - -log "Unlock session complete. Reason: $REASON" -logger -t "$SYSLOG_TAG" "session_end user=${SUDO_USER:-$USER} reason='$REASON'" -echo "Done." >&2 diff --git a/hosts/guard/setup_hosts_guard.sh b/hosts/guard/setup_hosts_guard.sh deleted file mode 100755 index e31dbde..0000000 --- a/hosts/guard/setup_hosts_guard.sh +++ /dev/null @@ -1,452 +0,0 @@ -#!/bin/bash -# One-shot installer for hosts guard + psychological friction + read-only bind mount -# Layers implemented: -# - Canonical snapshot of /etc/hosts at /usr/local/share/locked-hosts -# - Enforcement script (/usr/local/sbin/enforce-hosts.sh) -# - Systemd path-based auto-revert (hosts-guard.path + hosts-guard.service) -# - Read-only bind mount (hosts-bind-mount.service) -# - Delayed edit workflow (/usr/local/sbin/unlock-hosts) -# -# This script is idempotent. Re-running updates installed artifacts safely. -# -# Usage: -# sudo ./setup_hosts_guard.sh [options] -# Options: -# --force-snapshot Overwrite canonical snapshot even if it exists -# --no-snapshot Skip creating canonical snapshot (assume already present) -# --skip-bind Do not enable read-only bind mount service -# --skip-path-watch Do not enable path watch auto-revert -# --delay N Set unlock delay seconds (default 45) -# --dry-run Show actions without performing changes -# --uninstall Remove installed units/scripts (does NOT restore original hosts) -# -h|--help Show help -# -# Exit codes: -# 0 success, 1 generic failure, 2 argument error - -set -euo pipefail - -###################################################################### -# Defaults / Config -###################################################################### -FORCE_SNAPSHOT=0 -DO_SNAPSHOT=1 -ENABLE_BIND=1 -ENABLE_PATH=1 -ENABLE_NSSWITCH=1 -UNINSTALL=0 -DELAY=45 -DRY_RUN=0 -INSTALL_SHELL_HOOKS=1 -INSTALL_AUDIT_RULE=1 -ADD_ALIAS_STUB=1 - -###################################################################### -# Helpers -###################################################################### -msg() { printf '\e[1;32m[+]\e[0m %s\n' "$*"; } -note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; } -warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; } -err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; } -run() { - if [[ $DRY_RUN -eq 1 ]]; then - printf 'DRY-RUN:' - if [ "$#" -gt 0 ]; then - printf ' %q' "$@" - fi - printf '\n' - else - "$@" - fi -} - -require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; } - -usage() { sed -n '1,/^set -euo pipefail/p' "$0" | sed 's/^# \{0,1\}//'; } - -###################################################################### -# Parse args -###################################################################### -while [[ $# -gt 0 ]]; do - case "$1" in - --force-snapshot) - FORCE_SNAPSHOT=1 - shift - ;; - --no-snapshot) - DO_SNAPSHOT=0 - shift - ;; - --skip-bind) - ENABLE_BIND=0 - shift - ;; - --skip-path-watch) - ENABLE_PATH=0 - shift - ;; - --skip-nsswitch) - ENABLE_NSSWITCH=0 - shift - ;; - --delay) - DELAY=${2:-} - [[ -z ${DELAY} ]] && { - err '--delay requires value' - exit 2 - } - shift 2 - ;; - --dry-run) - DRY_RUN=1 - shift - ;; - --no-shell-hooks) - INSTALL_SHELL_HOOKS=0 - shift - ;; - --shell-hooks) - INSTALL_SHELL_HOOKS=1 - shift - ;; - --no-audit) - INSTALL_AUDIT_RULE=0 - shift - ;; - --audit) - INSTALL_AUDIT_RULE=1 - shift - ;; - --no-alias-stub) - ADD_ALIAS_STUB=0 - shift - ;; - --alias-stub) - ADD_ALIAS_STUB=1 - shift - ;; - --uninstall) - UNINSTALL=1 - shift - ;; - -h | --help) - usage - exit 0 - ;; - *) - err "Unknown argument: $1" - usage - exit 2 - ;; - esac -done - -require_root "$@" - -###################################################################### -# Paths -###################################################################### -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" - -TEMPLATE_ENFORCE="$SCRIPT_DIR/enforce-hosts.sh" -TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh" -UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service" -UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path" -UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service" -TEMPLATE_ENFORCE_NSSWITCH="$SCRIPT_DIR/enforce-nsswitch.sh" -UNIT_NSSWITCH_SERVICE="$SCRIPT_DIR/nsswitch-guard.service" -UNIT_NSSWITCH_PATH="$SCRIPT_DIR/nsswitch-guard.path" - -INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh" -INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts" -INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh" -CANON="/usr/local/share/locked-hosts" -CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf" -HOSTS="/etc/hosts" -NSSWITCH="/etc/nsswitch.conf" - -# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d) -ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh" -BASH_FILTER_SNIPPET="/etc/profile.d/hosts_guard_history_filter.sh" - -SYSTEMD_DIR="/etc/systemd/system" - -###################################################################### -# Uninstall flow -###################################################################### -if [[ $UNINSTALL -eq 1 ]]; then - note "Uninstalling hosts guard components ( protections removed )" - for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service; do - if systemctl list-unit-files | grep -q "^$u"; then - run systemctl disable --now "$u" || true - fi - done - for f in \ - "$INSTALL_ENFORCE" \ - "$INSTALL_UNLOCK" \ - "$INSTALL_ENFORCE_NSSWITCH" \ - "$SYSTEMD_DIR/hosts-guard.service" \ - "$SYSTEMD_DIR/hosts-guard.path" \ - "$SYSTEMD_DIR/hosts-bind-mount.service" \ - "$SYSTEMD_DIR/nsswitch-guard.service" \ - "$SYSTEMD_DIR/nsswitch-guard.path" \ - "$ZSH_FILTER_SNIPPET" \ - "$BASH_FILTER_SNIPPET"; do - if [[ -e $f ]]; then run rm -f "$f"; fi - done - note "Leaving canonical snapshots at $CANON and $CANON_NSSWITCH (remove manually if undesired)." - if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi - msg "Uninstall complete" - exit 0 -fi - -###################################################################### -# Pre-flight checks -###################################################################### -note "Script directory: $SCRIPT_DIR" -note "Repository root: $REPO_ROOT" - -for req in "$TEMPLATE_ENFORCE" "$TEMPLATE_UNLOCK" "$UNIT_GUARD_SERVICE"; do - [[ -f $req ]] || { - err "Missing template: $req" - exit 1 - } -done - -if [[ ! -f $HOSTS ]]; then - err "$HOSTS does not exist. Run your hosts/install.sh first." - exit 1 -fi - -###################################################################### -# Snapshot -###################################################################### -if [[ $DO_SNAPSHOT -eq 1 ]]; then - if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then - note "Canonical snapshot exists (use --force-snapshot to overwrite)" - else - msg "Creating canonical snapshot at $CANON" - run install -m 644 -D "$HOSTS" "$CANON" - fi -else - note "Skipping snapshot creation (--no-snapshot)" -fi - -###################################################################### -# Install scripts -###################################################################### -msg "Installing enforcement script -> $INSTALL_ENFORCE" -run install -m 755 "$TEMPLATE_ENFORCE" "$INSTALL_ENFORCE" - -msg "Installing unlock script -> $INSTALL_UNLOCK" -run install -m 755 "$TEMPLATE_UNLOCK" "$INSTALL_UNLOCK" - -# Adjust delay in unlock script if different from default -if [[ $DELAY -ne 45 ]]; then - msg "Adjusting unlock delay to $DELAY seconds" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would patch $INSTALL_UNLOCK" - else - # Replace DELAY_SECONDS=... line - sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay" - fi -fi - -###################################################################### -# Install shell history filters (optional) -###################################################################### -if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then - msg "Installing shell history suppression hooks for unlock command" - # Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot - # Zsh: use zshaddhistory function - if command -v zsh >/dev/null 2>&1; then - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET" - else - cat >"$ZSH_FILTER_SNIPPET" <<'ZEOF' -# Added by hosts guard setup – suppress unlock-hosts commands from Zsh history -autoload -Uz add-zsh-hook 2>/dev/null || true -_hosts_guard_history_filter() { - emulate -L zsh - setopt extendedglob - local line="$1" - local _pattern='(^|;|&&|\|\|)\s*(sudo\s+)?(/usr/local/sbin/)?unlock-hosts(\s|;|$)' - if [[ $line =~ ${_pattern} ]]; then - return 1 - fi - return 0 -} -if typeset -f add-zsh-hook >/dev/null 2>&1; then - add-zsh-hook zshaddhistory _hosts_guard_history_filter 2>/dev/null || true -else - zshaddhistory() { _hosts_guard_history_filter "$1"; } -fi -ZEOF - chmod 644 "$ZSH_FILTER_SNIPPET" - fi - fi - - # Bash: rely on HISTCONTROL and PROMPT_COMMAND filter - if command -v bash >/dev/null 2>&1; then - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $BASH_FILTER_SNIPPET" - else - cat >"$BASH_FILTER_SNIPPET" <<'BEOF' -# Added by hosts guard setup – suppress unlock-hosts commands from Bash history -export HISTCONTROL=ignoredups:erasedups -_hosts_guard_hist_filter() { - local last_cmd - local _pattern='(^|;|&&|\|\|)\s*(sudo\s+)?(/usr/local/sbin/)?unlock-hosts(\s|;|$)' - last_cmd=$(history 1 2>/dev/null | sed -E 's/^ *[0-9]+ +//') - if [[ -n $last_cmd && $last_cmd =~ ${_pattern} ]]; then - local id - id=$(history 1 2>/dev/null | awk '{print $1}') - if [[ -n $id ]]; then history -d $id 2>/dev/null || true; fi - history -w 2>/dev/null || true - history -c || true - history -r 2>/dev/null || true - fi -} -case :${PROMPT_COMMAND-}: in - *:_hosts_guard_hist_filter:* ) ;; - * ) PROMPT_COMMAND="_hosts_guard_hist_filter${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;; -esac -BEOF - chmod 644 "$BASH_FILTER_SNIPPET" - fi - fi -else - note "Skipping shell history hooks (--no-shell-hooks)" -fi - -###################################################################### -# Add alias stub to discourage raw invocation (shell-level friction) -###################################################################### -if [[ $ADD_ALIAS_STUB -eq 1 ]]; then - PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create $PROFILE_STUB" - else - cat >"$PROFILE_STUB" <<'ASTUB' -# Added by hosts guard setup – discourages casual use of unlock-hosts name -if command -v unlock-hosts >/dev/null 2>&1; then - alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"' -fi -ASTUB - chmod 644 "$PROFILE_STUB" - fi -fi - -###################################################################### -# Audit rule to record executions (requires auditd) -###################################################################### -if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then - if command -v auditctl >/dev/null 2>&1; then - audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock" - audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock) - if auditctl -l 2>/dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then - note "Audit rule already present" - else - run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)" - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules" - else - echo "$audit_rule_str" >/etc/audit/rules.d/hosts_unlock.rules - fi - fi - else - warn "auditctl not found; skipping audit rule (install auditd to enable)" - fi -fi - -###################################################################### -# Install systemd units -###################################################################### -msg "Deploying systemd units" -run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service" -run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path" -run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service" -run install -m 644 "$UNIT_NSSWITCH_SERVICE" "$SYSTEMD_DIR/nsswitch-guard.service" -run install -m 644 "$UNIT_NSSWITCH_PATH" "$SYSTEMD_DIR/nsswitch-guard.path" - -if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi - -###################################################################### -# Enable / Start -###################################################################### -if [[ $ENABLE_PATH -eq 1 ]]; then - msg "Enabling path watch (auto-revert)" - run systemctl enable --now hosts-guard.path -else - note "Skipping path watch (--skip-path-watch)" -fi - -if [[ $ENABLE_BIND -eq 1 ]]; then - msg "Enabling read-only bind mount" - run systemctl enable --now hosts-bind-mount.service -else - note "Skipping bind mount (--skip-bind)" -fi - -if [[ $ENABLE_NSSWITCH -eq 1 ]]; then - msg "Enabling nsswitch.conf protection (hosts bypass prevention)" - msg "Installing nsswitch enforcement script -> $INSTALL_ENFORCE_NSSWITCH" - run install -m 755 "$TEMPLATE_ENFORCE_NSSWITCH" "$INSTALL_ENFORCE_NSSWITCH" - - # Create nsswitch canonical snapshot if needed - if [[ -f "$NSSWITCH" ]]; then - if [[ ! -f "$CANON_NSSWITCH" ]]; then - msg "Creating canonical nsswitch.conf snapshot at $CANON_NSSWITCH" - run cp "$NSSWITCH" "$CANON_NSSWITCH" - run chmod 644 "$CANON_NSSWITCH" - chattr +i "$CANON_NSSWITCH" 2>/dev/null || warn "Failed to protect canonical nsswitch copy" - fi - fi - - run systemctl enable --now nsswitch-guard.path - - # Perform initial nsswitch enforcement - if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would run $INSTALL_ENFORCE_NSSWITCH" - else - "$INSTALL_ENFORCE_NSSWITCH" || warn "nsswitch enforcement returned non-zero" - fi -else - note "Skipping nsswitch protection (--skip-nsswitch)" -fi - -msg "Performing initial hosts enforcement" -if [[ $DRY_RUN -eq 1 ]]; then - echo "DRY-RUN: would run $INSTALL_ENFORCE" -else - "$INSTALL_ENFORCE" || warn "Enforcement returned non-zero" -fi - -###################################################################### -# Summary -###################################################################### -echo -msg "Hosts guard setup complete" -echo "Canonical hosts copy: $CANON" -echo "Canonical nsswitch copy: $CANON_NSSWITCH" -echo "Enforce script: $INSTALL_ENFORCE" -echo "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH" -echo "Unlock command: sudo $INSTALL_UNLOCK" -echo "Delay (seconds): $DELAY" -echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)" -echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)" -echo "nsswitch protection: $([[ $ENABLE_NSSWITCH -eq 1 ]] && echo enabled || echo disabled)" -echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)" -echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)" -echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)" -echo -echo "Test flow:" -echo " sudo sed -i '1s/.*/# tamper test/' /etc/hosts # Should revert automatically" -echo " sudo $INSTALL_UNLOCK # Intentional edit workflow" -echo -echo "Uninstall:" -echo " sudo $0 --uninstall" -echo "(Optional) Skip shell history hooks: --no-shell-hooks" -echo -exit 0